Skip to content

Commit 22cd2f9

Browse files
committed
Merged in feat/s3-cors-headers (pull request localstack#55)
Initial support for CORS headers for S3 objects
2 parents 4fa7b9c + 27fb405 commit 22cd2f9

4 files changed

Lines changed: 102 additions & 5 deletions

File tree

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<html>
2+
<head>
3+
<script src="../node_modules/jquery/dist/jquery.js"></script>
4+
</head>
5+
<body>
6+
<div id="result">test running...</div>
7+
<script>
8+
var url = "http://localhost:4572/test-bucket/test-object";
9+
10+
function onLoaded1 () {
11+
$.get(url, onLoaded2);
12+
}
13+
function onLoaded2 (data) {
14+
document.getElementById('result').innerHTML = 'Test succeeded, file content: ' +
15+
'<div><pre><code>\n' +
16+
('' + data).replace(/</g, '&lt;').replace(/>/g, '&gt;') +
17+
'\n</code></pre></div>';
18+
}
19+
function onErrored () {
20+
document.getElementById('result').innerHTML = 'Test failed, unable to load file from S3.';
21+
}
22+
23+
var req = new XMLHttpRequest();
24+
req.addEventListener("load", onLoaded1);
25+
req.addEventListener("error", onErrored);
26+
req.open("GET", url);
27+
req.send();
28+
</script>
29+
</body>
30+
</html>

localstack/mock/generic_proxy.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ def forward(self, method):
8282
forward_headers['host'] = urlparse(target_url).netloc
8383
try:
8484
response = None
85+
# update listener (pre-invocation)
8586
if self.proxy.update_listener:
8687
do_forward = self.proxy.update_listener(method=method, path=path,
8788
data=data, headers=self.headers, return_forward_info=True)
@@ -96,15 +97,16 @@ def forward(self, method):
9697
if response is None:
9798
response = self.method(proxy_url, data=self.data_string,
9899
headers=forward_headers, proxies=proxies)
100+
# update listener (post-invocation)
101+
if self.proxy.update_listener:
102+
self.proxy.update_listener(method=method, path=path,
103+
data=data, headers=self.headers, response=response)
104+
# copy headers and return response
99105
self.send_response(response.status_code)
100-
# copy headers from response
101106
for header_key, header_value in response.headers.iteritems():
102107
self.send_header(header_key, header_value)
103108
self.end_headers()
104109
self.wfile.write(response.content)
105-
if self.proxy.update_listener:
106-
self.proxy.update_listener(method=method, path=path,
107-
data=data, headers=self.headers, response=response)
108110
except Exception, e:
109111
if not self.proxy.quiet:
110112
LOGGER.error("Error forwarding request: %s" % traceback.format_exc(e))

localstack/mock/proxy/s3_listener.py

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import urlparse
33
import logging
44
import json
5+
import xmltodict
56
import xml.etree.ElementTree as ET
67
from requests.models import Response
78
from localstack.constants import *
@@ -11,6 +12,9 @@
1112
# mappings for S3 bucket notifications
1213
S3_NOTIFICATIONS = {}
1314

15+
# mappings for bucket CORS settings
16+
BUCKET_CORS = {}
17+
1418
# set up logger
1519
LOGGER = logging.getLogger(__name__)
1620

@@ -93,27 +97,87 @@ def get_xml_text(node, name, ns=None, default=None):
9397
return child.text
9498

9599

100+
def get_cors(bucket_name):
101+
response = Response()
102+
cors = BUCKET_CORS.get(bucket_name)
103+
if not cors:
104+
# TODO: check if bucket exists, otherwise return 404-like error
105+
cors = {
106+
'CORSConfiguration': []
107+
}
108+
body = xmltodict.unparse(cors)
109+
response._content = body
110+
response.status_code = 200
111+
return response
112+
113+
114+
def set_cors(bucket_name, cors):
115+
# TODO: check if bucket exists, otherwise return 404-like error
116+
if isinstance(cors, basestring):
117+
cors = xmltodict.parse(cors)
118+
BUCKET_CORS[bucket_name] = cors
119+
response = Response()
120+
response.status_code = 200
121+
return response
122+
123+
124+
def delete_cors(bucket_name):
125+
# TODO: check if bucket exists, otherwise return 404-like error
126+
BUCKET_CORS.pop(bucket_name, {})
127+
response = Response()
128+
response.status_code = 200
129+
return response
130+
131+
132+
def append_cors_headers(bucket_name, request_method, request_headers, response):
133+
cors = BUCKET_CORS.get(bucket_name)
134+
if not cors:
135+
return
136+
origin = request_headers.get('Origin', '')
137+
for rule in cors['CORSConfiguration']['CORSRule']:
138+
allowed_methods = rule.get('AllowedMethod', [])
139+
if request_method in allowed_methods:
140+
allowed_origins = rule.get('AllowedOrigin', [])
141+
for allowed in allowed_origins:
142+
if origin in allowed or re.match(allowed.replace('*', '.*'), origin):
143+
response.headers['Access-Control-Allow-Origin'] = origin
144+
break
145+
146+
96147
def update_s3(method, path, data, headers, response=None, return_forward_info=False):
97148
if return_forward_info:
98149
parsed = urlparse.urlparse(path)
99150
query = parsed.query
100151
path = parsed.path
152+
bucket = path.split('/')[1]
101153
query_map = urlparse.parse_qs(query)
102154
if method == 'PUT' and (query == 'notification' or 'notification' in query_map):
103155
tree = ET.fromstring(data)
104156
queue_config = tree.find('{%s}QueueConfiguration' % XMLNS_S3)
105157
if len(queue_config):
106-
bucket = path[1:]
107158
S3_NOTIFICATIONS[bucket] = {
108159
'Id': get_xml_text(queue_config, 'Id'),
109160
'Event': get_xml_text(queue_config, 'Event', ns=XMLNS_S3),
110161
'Queue': get_xml_text(queue_config, 'Queue', ns=XMLNS_S3),
111162
'Topic': get_xml_text(queue_config, 'Topic', ns=XMLNS_S3),
112163
'CloudFunction': get_xml_text(queue_config, 'CloudFunction', ns=XMLNS_S3)
113164
}
165+
if query == 'cors' or 'cors' in query_map:
166+
if method == 'GET':
167+
return get_cors(bucket)
168+
if method == 'PUT':
169+
return set_cors(bucket, data)
170+
if method == 'DELETE':
171+
return delete_cors(bucket)
114172
return True
173+
# get subscribers and send bucket notifications
115174
if method in ('PUT', 'DELETE') and '/' in path[1:]:
116175
parts = path[1:].split('/', 1)
117176
bucket_name = parts[0]
118177
object_path = '/%s' % parts[1]
119178
send_notifications(method, bucket_name, object_path)
179+
# append CORS headers to response
180+
if response:
181+
parsed = urlparse.urlparse(path)
182+
bucket_name = parsed.path.split('/')[0]
183+
append_cors_headers(bucket_name, request_method=method, request_headers=headers, response=response)

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ amazon_kclpy==1.4.1
33
awscli==1.11.75
44
boto3==1.4.0
55
coverage==4.0.3
6+
xmltodict==0.10.2
67
docopt==0.6.2
78
elasticsearch==5.3.0
89
flask==0.10.1

0 commit comments

Comments
 (0)