Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Cross-Origin Resource Sharing (CORS) support. #98

Merged
merged 2 commits into from
Jan 26, 2013
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGES.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
0.13 - XXXX-XX-XX
=================

- ???
- Added Cross-Origin Resource Sharing (CORS) support.

0.12 - 2012-11-21
=================
Expand Down
128 changes: 128 additions & 0 deletions cornice/cors.py
Original file line number Diff line number Diff line change
@@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a larger architectural thought, I wonder if we should pass the service object into each validator by default. IOW, make the signature of a validator function "validate(service, request)" or similar. How common is it to make a validator that closes over the service object like this?



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
27 changes: 23 additions & 4 deletions cornice/pyramidhook.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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]

Expand Down
Loading