Repository: localstack/localstack

Issue:
Support for Authorization Parameters in GET Parameters (instead of header)
[X] bug report
[ ] feature request

# Detailed description

The AWS API supports Authorization parameters as GET parameters, e.g. here's a GET request to SendMessage on SQS:

```
curl 'http://localhost:4566/000000000000/SomeQueue.fifo?Action=SendMessage&MessageGroupId=Default&MessageDeduplicationId=1615597859837&MessageBody=%7B%22gateway_id%22%3A%20%2204e73c80-3539-499c-92a4-a0b106c2f6ab%22%7D&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=test%2F20210313%2Fus-east-1%2Fsqs%2Faws4_request&X-Amz-Date=20210313T011059Z&X-Amz-Expires=86400000&X-Amz-SignedHeaders=host&X-Amz-Signature=2c652c7bc9a3b75579db3d987d1e6dd056f0ac776c1e1d4ec91e2ce84e5ad3ae'
```

## Expected behavior

Should send the message and return receipt information.

## Actual behavior

Returns...

```
<?xml version="1.0" encoding="UTF-8"?>
<Error>
    <Code>NoSuchBucket</Code>
    <Message>The specified bucket does not exist</Message>
    <BucketName>000000000000</BucketName>
    <RequestID>7a62c49f-347e-4fc4-9331-6e8eEXAMPLE</RequestID>
</Error>
```

Support for Authorization Parameters in GET Parameters (instead of header)
[X] bug report
[ ] feature request

# Detailed description

The AWS API supports Authorization parameters as GET parameters, e.g. here's a GET request to SendMessage on SQS:

```
curl 'http://localhost:4566/000000000000/SomeQueue.fifo?Action=SendMessage&MessageGroupId=Default&MessageDeduplicationId=1615597859837&MessageBody=%7B%22gateway_id%22%3A%20%2204e73c80-3539-499c-92a4-a0b106c2f6ab%22%7D&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=test%2F20210313%2Fus-east-1%2Fsqs%2Faws4_request&X-Amz-Date=20210313T011059Z&X-Amz-Expires=86400000&X-Amz-SignedHeaders=host&X-Amz-Signature=2c652c7bc9a3b75579db3d987d1e6dd056f0ac776c1e1d4ec91e2ce84e5ad3ae'
```

## Expected behavior

Should send the message and return receipt information.

## Actual behavior

Returns...

```
<?xml version="1.0" encoding="UTF-8"?>
<Error>
    <Code>NoSuchBucket</Code>
    <Message>The specified bucket does not exist</Message>
    <BucketName>000000000000</BucketName>
    <RequestID>7a62c49f-347e-4fc4-9331-6e8eEXAMPLE</RequestID>
</Error>
```



Discussion:
@usmangani1 I think I'll have a fix for this today.
@usmangani1 I think I'll have a fix for this today.

Resolving patch:
diff --git a/localstack/services/edge.py b/localstack/services/edge.py
--- a/localstack/services/edge.py
+++ b/localstack/services/edge.py
@@ -17,7 +17,8 @@
     PATH_USER_REQUEST, LOCALHOST, LOCALHOST_IP)
 from localstack.utils.common import (
     empty_context_manager, run, is_root, TMP_THREADS, to_bytes, truncate, to_str,
-    get_service_protocol, in_docker, safe_requests as requests)
+    get_service_protocol, in_docker, safe_requests as requests,
+    parse_request_data)
 from localstack.services.infra import PROXY_LISTENERS
 from localstack.utils.aws.aws_stack import Environment, is_internal_call_context, set_default_region_in_headers
 from localstack.services.generic_proxy import ProxyListener, start_proxy_server, modify_and_forward
@@ -52,7 +53,9 @@ def forward_request(self, method, path, data, headers):
         headers.get(HEADER_KILL_SIGNAL) and os._exit(0)
 
         target = headers.get('x-amz-target', '')
-        auth_header = headers.get('authorization', '')
+        auth_header = get_auth_string(method, path, headers, data)
+        if auth_header and not headers.get('authorization', ''):
+            headers['authorization'] = auth_header
         host = headers.get('host', '')
         headers[HEADER_LOCALSTACK_EDGE_URL] = 'https://%s' % host
 
@@ -147,6 +150,53 @@ def do_forward_request_network(port, method, path, data, headers):
     return response
 
 
+def get_auth_string(method, path, headers, data=None):
+    """
+    Get Auth header from Header (this is how aws client's like boto typically
+    provide it) or from query string or url encoded parameters (sometimes
+    happens with presigned requests. Always return in the Authorization Header
+    form.
+
+    Typically an auth string comes in as a header:
+
+        Authorization: AWS4-HMAC-SHA256 \
+        Credential=_not_needed_locally_/20210312/us-east-1/sqs/aws4_request, \
+        SignedHeaders=content-type;host;x-amz-date, \
+        Signature=9277c941f4ecafcc0f290728e50cd7a3fa0e41763fbd2373fcdd3faf2dbddc2e
+
+    Here's what Authorization looks like as part of an presigned GET request:
+
+       &X-Amz-Algorithm=AWS4-HMAC-SHA256\
+       &X-Amz-Credential=test%2F20210313%2Fus-east-1%2Fsqs%2Faws4_request\
+       &X-Amz-Date=20210313T011059Z&X-Amz-Expires=86400000&X-Amz-SignedHeaders=host\
+       &X-Amz-Signature=2c652c7bc9a3b75579db3d987d1e6dd056f0ac776c1e1d4ec91e2ce84e5ad3ae
+
+    """
+
+    auth_header = headers.get('authorization', '')
+
+    if auth_header:
+        return auth_header
+
+    if not data:
+        return ''
+
+    data_components = parse_request_data(method, path, data)
+    algorithm = data_components.get('X-Amz-Algorithm')
+    credential = data_components.get('X-Amz-Credential')
+    signature = data_components.get('X-Amz-Signature')
+    signed_headers = data_components.get('X-Amz-SignedHeaders')
+
+    if algorithm and credential and signature and signed_headers:
+        return (
+            f'{algorithm} Credential={credential}, ' +
+            f'SignedHeaders={signed_headers}, ' +
+            f'Signature={signature}'
+        )
+
+    return ''
+
+
 def get_api_from_headers(headers, method=None, path=None, data=None):
     """ Determine API and backend port based on Authorization headers. """
 
diff --git a/localstack/utils/common.py b/localstack/utils/common.py
--- a/localstack/utils/common.py
+++ b/localstack/utils/common.py
@@ -491,7 +491,7 @@ def wait_for_port_open(port, http_path=None, expect_success=True, retries=10, sl
         If 'http_path' is set, make a GET request to this path and assert a non-error response. """
     def check():
         if not is_port_open(port, http_path=http_path, expect_success=expect_success):
-            raise Exception()
+            raise Exception('Port %s (path: %s) was not open' % (port, http_path))
 
     return retry(check, sleep=sleep_time, retries=retries)
 


Test patch:
diff --git a/tests/integration/test_sqs.py b/tests/integration/test_sqs.py
--- a/tests/integration/test_sqs.py
+++ b/tests/integration/test_sqs.py
@@ -3,9 +3,17 @@
 import time
 import unittest
 import requests
+import datetime
 
 from botocore.exceptions import ClientError
-from localstack.constants import TEST_AWS_ACCOUNT_ID
+from botocore.awsrequest import AWSRequest
+from botocore.credentials import Credentials
+from botocore.auth import SigV4Auth, SIGV4_TIMESTAMP
+from localstack.constants import (
+    TEST_AWS_ACCOUNT_ID,
+    TEST_AWS_ACCESS_KEY_ID,
+    TEST_AWS_SECRET_ACCESS_KEY
+)
 from six.moves.urllib.parse import urlencode
 
 from localstack import config
@@ -861,6 +869,43 @@ def test_create_queue_with_slashes(self):
         result = self.client.list_queues()
         self.assertNotIn(queue_url.get('QueueUrl'), result.get('QueueUrls'))
 
+    def test_list_queue_with_auth_in_presigned_url(self):
+        base_url = '{}://{}:{}'.format(get_service_protocol(), config.LOCALSTACK_HOSTNAME, config.PORT_SQS)
+
+        req = AWSRequest(method='GET', url=base_url, params={
+            'Action': 'ListQueues',
+            'Version': '2012-11-05'
+        })
+
+        # boto doesn't support querystring-style auth, so we have to do some
+        # weird logic to use boto's signing functions, to understand what's
+        # going on here look at the internals of the SigV4Auth.add_auth
+        # method.
+        datetime_now = datetime.datetime.utcnow()
+        req.context['timestamp'] = datetime_now.strftime(SIGV4_TIMESTAMP)
+        signer = SigV4Auth(
+            Credentials(TEST_AWS_ACCESS_KEY_ID, TEST_AWS_SECRET_ACCESS_KEY),
+            'sqs',
+            aws_stack.get_region()
+        )
+        canonical_request = signer.canonical_request(req)
+        string_to_sign = signer.string_to_sign(req, canonical_request)
+
+        encoded_url = urlencode({
+            'Action': 'ListQueues',
+            'Version': '2012-11-05',
+            'X-Amz-Algorithm': 'AWS4-HMAC-SHA256',
+            'X-Amz-Credential': signer.scope(req),
+            'X-Amz-SignedHeaders': ';'.join(
+                signer.headers_to_sign(req).keys()
+            ),
+            'X-Amz-Signature': signer.signature(string_to_sign, req)
+        })
+
+        res = requests.post(url=base_url, data=encoded_url)
+        self.assertEqual(res.status_code, 200)
+        self.assertTrue(b'<ListQueuesResponse>' in res.content)
+
     # ---------------
     # HELPER METHODS
     # ---------------
diff --git a/tests/unit/test_edge.py b/tests/unit/test_edge.py
--- a/tests/unit/test_edge.py
+++ b/tests/unit/test_edge.py
@@ -1,5 +1,6 @@
 import unittest
-from localstack.services.edge import is_s3_form_data
+from localstack.services.edge import is_s3_form_data, get_auth_string
+from werkzeug.datastructures import Headers
 
 
 class EdgeServiceTest(unittest.TestCase):
@@ -57,3 +58,42 @@ def test_other_form_data_is_false(self):
         --28f72589b2be0c9de84386b52c615990--
         """
         self.assertEqual(is_s3_form_data(data_bytes), False)
+
+    def test_get_auth_string(self):
+        # Typical Header with Authorization
+        headers_with_auth = Headers(
+            [
+                ('X-Amz-Date', '20210313T160953Z'),
+                (
+                    'Authorization',
+                    (
+                        'AWS4-HMAC-SHA256 Credential='
+                        'test/20210313/us-east-1/sqs/aws4_request, '
+                        'SignedHeaders=content-type;host;x-amz-date, '
+                        'Signature='
+                        '3cba88ae6cbb8036126d2ba18ba8ded5'
+                        'eea9e5484d70822affce9dad03be5993'
+                    )
+                ),
+            ]
+        )
+
+        body_with_auth = (
+            b'X-Amz-Algorithm=AWS4-HMAC-SHA256&' +
+            b'X-Amz-Credential=' +
+            b'test%2F20210313%2Fus-east-1%2Fsqs%2Faws4_request&' +
+            b'X-Amz-Date=20210313T011059Z&' +
+            b'X-Amz-Expires=86400000&' +
+            b'X-Amz-SignedHeaders=content-type%3Bhost%3Bx-amz-date&' +
+            b'X-Amz-Signature=' +
+            b'3cba88ae6cbb8036126d2ba18ba8ded5eea9e5484d70822affce9dad03be5993'
+        )
+
+        self.assertEqual(
+            get_auth_string('POST', '/', headers_with_auth, b''),
+            headers_with_auth.get('authorization')
+        )
+        self.assertEqual(
+            get_auth_string('POST', '/', Headers(), body_with_auth),
+            headers_with_auth.get('authorization')
+        )