Repository: localstack/localstack

Issue:
S3 GET Response header Content-Type should be 'binary/octet-stream' not 'text/html'
If you make a GET request to URL for a file that is publicly readable, the content-type is being set to `text/html`, but when you do the same for the real S3, the content-type is set to `binary/octet-stream`.  This allows the browser to know to just download it as a file, instead of trying to display it.

## To Reproduce:

```python
import boto3
import os
import requests
from botocore.config import Config
from StringIO import StringIO

host = 'localhost'
port = 4572
s3 = boto3.resource('s3',
 config=Config(s3={'addressing_style': 'path'}),
 endpoint_url="http://{}:{}".format(host, port)
)

s3.create_bucket(Bucket='test-bucket')
bucket = s3.Bucket('test-bucket')

key = 'blah.txt'
file_obj = StringIO('hello world')
bucket.upload_fileobj(file_obj, key, ExtraArgs={"ACL": 'public-read'})
url = s3.meta.client.generate_presigned_url(
    'get_object', ExpiresIn=0, Params={'Bucket': bucket.name, 'Key': key}
)
url = url.split('?')[0]

response = requests.get(url)
print response.headers['content-type']
```

When running this against AWS S3, the content-type for txt, pdf, csv and html filetype extensions is `binary/octet-stream`.    When running the same thing but connected to localstack, I always get `text/html; charset=utf-8`.


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
@@ -348,6 +348,11 @@ def forward_request(self, method, path, data, headers):
             if expanded_data is not original_data:
                 modified_data = expanded_data
 
+        # If no content-type is provided, 'binary/octet-stream' should be used
+        # src: https://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectPUT.html
+        if method == 'PUT' and not headers.get('content-type'):
+            headers['content-type'] = 'binary/octet-stream'
+
         # persist this API call to disk
         persistence.record('s3', method, path, data, headers)
 
@@ -487,10 +492,6 @@ def return_response(self, method, path, data, headers, response):
             # append CORS headers to response
             append_cors_headers(bucket_name, request_method=method, request_headers=headers, response=response)
 
-            if response._content:
-                # default content type; possibly overwritten with "text/xml" for API calls further below
-                response.headers['Content-Type'] = 'binary/octet-stream'
-
             response_content_str = None
             try:
                 response_content_str = to_str(response._content)


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
@@ -83,7 +83,10 @@ def test_s3_put_object_notification():
     s3_client.delete_bucket(Bucket=TEST_BUCKET_WITH_NOTIFICATION)
 
 
-def test_s3_get_response_content_type():
+def test_s3_get_response_default_content_type():
+    # When no content type is provided by a PUT request
+    # 'binary/octet-stream' should be used
+    # src: https://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectPUT.html
     bucket_name = 'test-bucket-%s' % short_uid()
     s3_client = aws_stack.connect_to_service('s3')
     s3_client.create_bucket(Bucket=bucket_name)
@@ -103,6 +106,26 @@ def test_s3_get_response_content_type():
     s3_client.delete_bucket(Bucket=bucket_name)
 
 
+def test_s3_get_response_content_type_same_as_upload():
+    bucket_name = 'test-bucket-%s' % short_uid()
+    s3_client = aws_stack.connect_to_service('s3')
+    s3_client.create_bucket(Bucket=bucket_name)
+
+    # put object
+    object_key = 'key-by-hostname'
+    s3_client.put_object(Bucket=bucket_name, Key=object_key, Body='something', ContentType='text/html; charset=utf-8')
+    url = s3_client.generate_presigned_url(
+        'get_object', Params={'Bucket': bucket_name, 'Key': object_key}
+    )
+
+    # get object and assert headers
+    response = requests.get(url, verify=False)
+    assert response.headers['content-type'] == 'text/html; charset=utf-8'
+    # clean up
+    s3_client.delete_objects(Bucket=bucket_name, Delete={'Objects': [{'Key': object_key}]})
+    s3_client.delete_bucket(Bucket=bucket_name)
+
+
 def test_s3_get_response_headers():
     bucket_name = 'test-bucket-%s' % short_uid()
     s3_client = aws_stack.connect_to_service('s3')