Repository: localstack/localstack

Issue:
S3 requests with Range header broken when using SSL
Including the `Range` header on S3 requests triggers a `502 Bad Gateway` when localstack is run with SSL.

Sample test that demonstrates the issue:
```python
import logging
import io
import os

import boto3

logging.basicConfig(level=logging.INFO)


def test_range_s3():
    s3 = boto3.client('s3', endpoint_url='https://localhost:4572', use_ssl=True, verify=False)

    bucket = 'localstack-s3-range'
    obj_name = 'sample.bin'
    s3.create_bucket(Bucket=bucket)

    chunk_size = 1024

    with io.BytesIO() as data:
        data.write(os.urandom(chunk_size * 2))
        logging.info('Uploading object (size=%d)', data.getbuffer().nbytes)
        data.seek(0)
        s3.upload_fileobj(data, bucket, obj_name)

    range_header = f'bytes=0-{chunk_size - 1}'
    logging.info('Downloading using Range header: %s', range_header)
    
    # this will raise an exception
    # botocore.exceptions.ClientError: An error occurred (502) when calling the GetObject operation (reached max retries: 4): Bad Gateway
    resp = s3.get_object(Bucket=bucket, Key=obj_name, Range=range_header)
    
    content = resp['Body'].read()
    assert len(content) == chunk_size
```

This is a fairly recent regression:

Running against `localstack/localstack:latest` will fail using SSL but pass without SSL. 
Running against `localstack/localstack:0.10.6` will pass using SSL as well as without SSL.

I am guessing changes from either #1887 or #1906 broke this since those were related to `Range` functionality, but have not bisected to be sure.


Discussion:

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
@@ -857,8 +857,7 @@ def sub(match):
         path_new = re.sub(r'/([^?/]+)([?/].*)?', sub, path)
         if path == path_new:
             return
-        url = '%s://%s:%s%s' % (get_service_protocol(), constants.LOCALHOST,
-            constants.DEFAULT_PORT_S3_BACKEND, path_new)
+        url = 'http://%s:%s%s' % (constants.LOCALHOST, constants.DEFAULT_PORT_S3_BACKEND, path_new)
         return url
 
     def return_response(self, method, path, data, headers, response):
diff --git a/localstack/services/s3/s3_starter.py b/localstack/services/s3/s3_starter.py
--- a/localstack/services/s3/s3_starter.py
+++ b/localstack/services/s3/s3_starter.py
@@ -76,6 +76,8 @@ def s3_update_acls(self, request, query, bucket_name, key_name):
         if acl:
             key.set_acl(acl)
 
+    # patch _key_response_post(..)
+
     def s3_key_response_post(self, request, body, bucket_name, query, key_name, *args, **kwargs):
         result = s3_key_response_post_orig(request, body, bucket_name, query, key_name, *args, **kwargs)
         s3_update_acls(self, request, query, bucket_name, key_name)
@@ -85,6 +87,8 @@ def s3_key_response_post(self, request, body, bucket_name, query, key_name, *arg
     s3_responses.S3ResponseInstance._key_response_post = types.MethodType(
         s3_key_response_post, s3_responses.S3ResponseInstance)
 
+    # patch _key_response_put(..)
+
     def s3_key_response_put(self, request, body, bucket_name, query, key_name, headers, *args, **kwargs):
         result = s3_key_response_put_orig(request, body, bucket_name, query, key_name, headers, *args, **kwargs)
         s3_update_acls(self, request, query, bucket_name, key_name)


Test patch:
diff --git a/tests/integration/test_s3.py b/tests/integration/test_s3.py
--- a/tests/integration/test_s3.py
+++ b/tests/integration/test_s3.py
@@ -1,3 +1,4 @@
+import io
 import os
 import ssl
 import gzip
@@ -259,6 +260,28 @@ def test_s3_put_metadata_underscores(self):
         # clean up
         self._delete_bucket(bucket_name, [object_key])
 
+    def test_range_header_body_length(self):
+        # Test for https://github.com/localstack/localstack/issues/1952
+
+        object_key = 'sample.bin'
+        bucket_name = 'test-%s' % short_uid()
+        self.s3_client.create_bucket(Bucket=bucket_name)
+
+        chunk_size = 1024
+
+        with io.BytesIO() as data:
+            data.write(os.urandom(chunk_size * 2))
+            data.seek(0)
+            self.s3_client.upload_fileobj(data, bucket_name, object_key)
+
+        range_header = 'bytes=0-%s' % (chunk_size - 1)
+        resp = self.s3_client.get_object(Bucket=bucket_name, Key=object_key, Range=range_header)
+        content = resp['Body'].read()
+        self.assertEquals(len(content), chunk_size)
+
+        # clean up
+        self._delete_bucket(bucket_name, [object_key])
+
     def test_s3_get_response_content_type_same_as_upload_and_range(self):
         bucket_name = 'test-bucket-%s' % short_uid()
         self.s3_client.create_bucket(Bucket=bucket_name)
@@ -422,7 +445,7 @@ def test_bucket_exists(self):
 
     def test_s3_uppercase_names(self):
         # bucket name should be case-insensitive
-        bucket_name = 'TestBucket-%s' % short_uid()
+        bucket_name = 'TestUpperCase-%s' % short_uid()
         self.s3_client.create_bucket(Bucket=bucket_name)
 
         # key name should be case-sensitive