cubicweb/web/cors.py
changeset 11057 0b59724cb3f2
parent 10907 9ae707db5265
child 11348 70337ad23145
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/web/cors.py	Sat Jan 16 13:48:51 2016 +0100
@@ -0,0 +1,113 @@
+# -*- coding: utf-8 -*-
+# copyright 2014 Logilab, PARIS
+
+"""A set of utility functions to handle CORS requests
+
+Unless specified, all references in this file are related to:
+  http://www.w3.org/TR/cors
+
+The provided implementation roughly follows:
+  http://www.html5rocks.com/static/images/cors_server_flowchart.png
+
+See also:
+  https://developer.mozilla.org/en-US/docs/HTTP/Access_control_CORS
+
+"""
+
+from six.moves.urllib.parse import urlsplit
+
+from cubicweb.web import LOGGER
+info = LOGGER.info
+
+class CORSFailed(Exception):
+    """Raised when cross origin resource sharing checks failed"""
+
+
+class CORSPreflight(Exception):
+    """Raised when cross origin resource sharing checks detects the
+    request as a valid preflight request"""
+
+
+def process_request(req, config):
+    """
+    Process a request to apply CORS specification algorithms
+
+    Check whether the CORS specification is respected and set corresponding
+    headers to ensure response complies with the specification.
+
+    In case of non-compliance, no CORS-related header is set.
+    """
+    base_url = urlsplit(req.base_url())
+    expected_host = '://'.join((base_url.scheme, base_url.netloc))
+    if not req.get_header('Origin') or req.get_header('Origin') == expected_host:
+        # not a CORS request, nothing to do
+        return
+    try:
+        # handle cross origin resource sharing (CORS)
+        if req.http_method() == 'OPTIONS':
+            if req.get_header('Access-Control-Request-Method'):
+                # preflight CORS request
+                process_preflight(req, config)
+        else: # Simple CORS or actual request
+            process_simple(req, config)
+    except CORSFailed as exc:
+        info('Cross origin resource sharing failed: %s' % exc)
+    except CORSPreflight:
+        info('Cross origin resource sharing: valid Preflight request %s')
+        raise
+
+def process_preflight(req, config):
+    """cross origin resource sharing (preflight)
+    Cf http://www.w3.org/TR/cors/#resource-preflight-requests
+    """
+    origin = check_origin(req, config)
+    allowed_methods = set(config['access-control-allow-methods'])
+    allowed_headers = set(config['access-control-allow-headers'])
+    try:
+        method = req.get_header('Access-Control-Request-Method')
+    except ValueError:
+        raise CORSFailed('Access-Control-Request-Method is incorrect')
+    if method not in allowed_methods:
+        raise CORSFailed('Method is not allowed')
+    try:
+        req.get_header('Access-Control-Request-Headers', ())
+    except ValueError:
+        raise CORSFailed('Access-Control-Request-Headers is incorrect')
+    req.set_header('Access-Control-Allow-Methods', allowed_methods, raw=False)
+    req.set_header('Access-Control-Allow-Headers', allowed_headers, raw=False)
+
+    process_common(req, config, origin)
+    raise CORSPreflight()
+
+def process_simple(req, config):
+    """Handle the Simple Cross-Origin Request case
+    """
+    origin = check_origin(req, config)
+    exposed_headers = config['access-control-expose-headers']
+    if exposed_headers:
+        req.set_header('Access-Control-Expose-Headers', exposed_headers, raw=False)
+    process_common(req, config, origin)
+
+def process_common(req, config, origin):
+    req.set_header('Access-Control-Allow-Origin', origin)
+    # in CW, we always support credential/authentication
+    req.set_header('Access-Control-Allow-Credentials', 'true')
+
+def check_origin(req, config):
+    origin = req.get_header('Origin').lower()
+    allowed_origins = config.get('access-control-allow-origin')
+    if not allowed_origins:
+        raise CORSFailed('access-control-allow-origin is not configured')
+    if '*' not in allowed_origins and origin not in allowed_origins:
+        raise CORSFailed('Origin is not allowed')
+    # bit of sanity check; see "6.3 Security"
+    myhost = urlsplit(req.base_url()).netloc
+    host = req.get_header('Host')
+    if host != myhost:
+        info('cross origin resource sharing detected possible '
+             'DNS rebinding attack Host header != host of base_url: '
+             '%s != %s' % (host, myhost))
+        raise CORSFailed('Host header and hostname do not match')
+    # include "Vary: Origin" header (see 6.4)
+    req.headers_out.addHeader('Vary', 'Origin')
+    return origin