web/cors.py
author Julien Cristau <julien.cristau@logilab.fr>
Fri, 17 Oct 2014 12:14:58 +0200
changeset 10045 0a49d61c8cca
parent 10002 586d0e527052
child 10588 fdaa0e4b7eaf
permissions -rw-r--r--
Add custom checker for Password values We override the default converter to pass Binary values through, but don't do anything about the checker. This worked previously because yams allowed StringIO instances, although its converter didn't do the right thing for them. Fixing this in yams requires that we properly register a checker.

# -*- 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

"""

import urlparse

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 = urlparse.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, 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 = urlparse.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