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
Changes from all commits
Commits
File filter...
Filter file types
Jump to…
Jump to file or symbol
Failed to load files and symbols.
+647 −39
Diff settings

Always

Just for now

Copy path View file
@@ -1,7 +1,7 @@
0.13 - XXXX-XX-XX
=================

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

0.12 - 2012-11-21
=================
Copy path View file
@@ -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

This comment has been minimized.

@rfk

rfk Jan 25, 2013

Contributor

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
Copy path View file
@@ -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.
ProTip! Use n and p to navigate between commits in a pull request.
You can’t perform that action at this time.