Permalink
Browse files

Merge pull request #98 from mozilla-services/cors-support

Add Cross-Origin Resource Sharing (CORS) support.
  • Loading branch information...
2 parents 7cf2da4 + 9a79e9d commit 3ffb747cd6b87a28299c31444968760bc62afa6b @almet almet committed Jan 26, 2013
Showing with 647 additions and 39 deletions.
  1. +1 −1 CHANGES.txt
  2. +128 −0 cornice/cors.py
  3. +23 −4 cornice/pyramidhook.py
  4. +155 −31 cornice/service.py
  5. +192 −0 cornice/tests/test_cors.py
  6. +148 −3 cornice/tests/test_service.py
View
@@ -1,7 +1,7 @@
0.13 - XXXX-XX-XX
=================
-- ???
+- Added Cross-Origin Resource Sharing (CORS) support.
0.12 - 2012-11-21
=================
View
@@ -0,0 +1,128 @@
+import fnmatch
+
+
+CORS_PARAMETERS = ('cors_headers', 'cors_enabled', 'cors_origins',
+ 'cors_credentials', 'cors_max_age',
+ 'cors_expose_all_headers')
+
+
+def get_cors_preflight_view(service):
+ """Return a view for the OPTION method.
+
+ Checks that the User-Agent is authorized to do a request to the server, and
+ to this particular service, and add the various checks that are specified
+ in http://www.w3.org/TR/cors/#resource-processing-model.
+ """
+
+ def _preflight_view(request):
+ response = request.response
+ origin = request.headers.get('Origin')
+ supported_headers = service.cors_supported_headers
+
+ if not origin:
+ request.errors.add('header', 'Origin',
+ 'this header is mandatory')
+
+ requested_method = request.headers.get('Access-Control-Request-Method')
+ if not requested_method:
+ request.errors.add('header', 'Access-Control-Request-Method',
+ 'this header is mandatory')
+
+ if not (requested_method and origin):
+ return
+
+ requested_headers = (
+ request.headers.get('Access-Control-Request-Headers', ()))
+
+ if requested_headers:
+ requested_headers = requested_headers.split(',')
+
+ if requested_method not in service.cors_supported_methods:
+ request.errors.add('header', 'Access-Control-Request-Method',
+ 'Method not allowed')
+
+ if not service.cors_expose_all_headers:
+ for h in requested_headers:
+ if not h.lower() in [s.lower() for s in supported_headers]:
+ request.errors.add(
+ 'header',
+ 'Access-Control-Request-Headers',
+ 'Header "%s" not allowed' % h)
+
+ supported_headers = set(supported_headers) | set(requested_headers)
+
+ response.headers['Access-Control-Allow-Headers'] = (
+ ','.join(supported_headers))
+
+ response.headers['Access-Control-Allow-Methods'] = (
+ ','.join(service.cors_supported_methods))
+
+ max_age = service.cors_max_age_for(requested_method)
+ if max_age is not None:
+ response.headers['Access-Control-Max-Age'] = str(max_age)
+
+ return 'ok'
+ return _preflight_view
+
+
+def _get_method(request):
+ """Return what's supposed to be the method for CORS operations.
+ (e.g if the verb is options, look at the A-C-Request-Method header,
+ otherwise return the HTTP verb).
+ """
+ if request.method == 'OPTIONS':
+ method = request.headers.get('Access-Control-Request-Method',
+ request.method)
+ else:
+ method = request.method
+ return method
+
+
+def get_cors_validator(service):
+ """Create a cornice validator to handle CORS-related verifications.
+
+ Checks, if an "Origin" header is present, that the origin is authorized
+ (and issue an error if not)
+ """
+
+ def _cors_validator(request):
+ response = request.response
+ method = _get_method(request)
+
+ # If we have an "Origin" header, check it's authorized and add the
+ # response headers accordingly.
+ origin = request.headers.get('Origin')
+ if origin:
+ if not any([fnmatch.fnmatchcase(origin, o)
+ for o in service.cors_origins_for(method)]):
+ request.errors.add('header', 'Origin',
+ '%s not allowed' % origin)
+ else:
+ response.headers['Access-Control-Allow-Origin'] = origin
+ return _cors_validator
+
+
+def get_cors_filter(service):
+ """Create a cornice filter to handle CORS-related post-request
+ things.
+
+ Add some response headers, such as the Expose-Headers and the
+ Allow-Credentials ones.
+ """
+
+ def _cors_filter(response, request):
+ method = _get_method(request)
+
+ if (service.cors_support_credentials(method) and
+ not 'Access-Control-Allow-Credentials' in response.headers):
+ response.headers['Access-Control-Allow-Credentials'] = 'true'
+
+ if request.method is not 'OPTIONS':
+ # Which headers are exposed?
+ supported_headers = service.cors_supported_headers
+ if supported_headers:
+ response.headers['Access-Control-Expose-Headers'] = (
+ ', '.join(supported_headers))
+
+ return response
+ return _cors_filter
View
@@ -3,13 +3,16 @@
# You can obtain one at http://mozilla.org/MPL/2.0/.
import json
import functools
+import copy
from pyramid.httpexceptions import HTTPMethodNotAllowed, HTTPNotAcceptable
from pyramid.exceptions import PredicateMismatch
from cornice.service import decorate_view
from cornice.errors import Errors
from cornice.util import to_list
+from cornice.cors import (get_cors_filter, get_cors_validator,
+ get_cors_preflight_view, CORS_PARAMETERS)
def match_accept_header(func, context, request):
@@ -52,7 +55,7 @@ def _fallback_view(request):
continue
if 'accept' in args:
acceptable.extend(
- service.get_acceptable(method, filter_callables=True))
+ service.get_acceptable(method, filter_callables=True))
if 'acceptable' in request.info:
for content_type in request.info['acceptable']:
if content_type not in acceptable:
@@ -85,7 +88,10 @@ def cornice_tween(request):
for _filter in kwargs.get('filters', []):
if isinstance(_filter, basestring) and ob is not None:
_filter = getattr(ob, _filter)
- response = _filter(response)
+ try:
+ response = _filter(response, request)
+ except TypeError:
+ response = _filter(response)
return response
return cornice_tween
@@ -117,16 +123,29 @@ def register_service_views(config, service):
# keep track of the registered routes
registered_routes = []
+ # before doing anything else, register a view for the OPTIONS method
+ # if we need to
+ if service.cors_enabled and 'OPTIONS' not in service.defined_methods:
+ service.add_view('options', view=get_cors_preflight_view(service))
+
# register the fallback view, which takes care of returning good error
# messages to the user-agent
+ cors_validator = get_cors_validator(service)
+ cors_filter = get_cors_filter(service)
+
for method, view, args in service.definitions:
- args = dict(args) # make a copy of the dict to not modify it
+ args = copy.deepcopy(args) # make a copy of the dict to not modify it
args['request_method'] = method
+ if service.cors_enabled:
+ args['validators'].insert(0, cors_validator)
+ args['filters'].append(cors_filter)
+
decorated_view = decorate_view(view, dict(args), method)
+
for item in ('filters', 'validators', 'schema', 'klass',
- 'error_handler'):
+ 'error_handler') + CORS_PARAMETERS:
if item in args:
del args[item]
Oops, something went wrong.

0 comments on commit 3ffb747

Please sign in to comment.