cubicweb/web/cors.py
changeset 11057 0b59724cb3f2
parent 10907 9ae707db5265
child 11348 70337ad23145
equal deleted inserted replaced
11052:058bb3dc685f 11057:0b59724cb3f2
       
     1 # -*- coding: utf-8 -*-
       
     2 # copyright 2014 Logilab, PARIS
       
     3 
       
     4 """A set of utility functions to handle CORS requests
       
     5 
       
     6 Unless specified, all references in this file are related to:
       
     7   http://www.w3.org/TR/cors
       
     8 
       
     9 The provided implementation roughly follows:
       
    10   http://www.html5rocks.com/static/images/cors_server_flowchart.png
       
    11 
       
    12 See also:
       
    13   https://developer.mozilla.org/en-US/docs/HTTP/Access_control_CORS
       
    14 
       
    15 """
       
    16 
       
    17 from six.moves.urllib.parse import urlsplit
       
    18 
       
    19 from cubicweb.web import LOGGER
       
    20 info = LOGGER.info
       
    21 
       
    22 class CORSFailed(Exception):
       
    23     """Raised when cross origin resource sharing checks failed"""
       
    24 
       
    25 
       
    26 class CORSPreflight(Exception):
       
    27     """Raised when cross origin resource sharing checks detects the
       
    28     request as a valid preflight request"""
       
    29 
       
    30 
       
    31 def process_request(req, config):
       
    32     """
       
    33     Process a request to apply CORS specification algorithms
       
    34 
       
    35     Check whether the CORS specification is respected and set corresponding
       
    36     headers to ensure response complies with the specification.
       
    37 
       
    38     In case of non-compliance, no CORS-related header is set.
       
    39     """
       
    40     base_url = urlsplit(req.base_url())
       
    41     expected_host = '://'.join((base_url.scheme, base_url.netloc))
       
    42     if not req.get_header('Origin') or req.get_header('Origin') == expected_host:
       
    43         # not a CORS request, nothing to do
       
    44         return
       
    45     try:
       
    46         # handle cross origin resource sharing (CORS)
       
    47         if req.http_method() == 'OPTIONS':
       
    48             if req.get_header('Access-Control-Request-Method'):
       
    49                 # preflight CORS request
       
    50                 process_preflight(req, config)
       
    51         else: # Simple CORS or actual request
       
    52             process_simple(req, config)
       
    53     except CORSFailed as exc:
       
    54         info('Cross origin resource sharing failed: %s' % exc)
       
    55     except CORSPreflight:
       
    56         info('Cross origin resource sharing: valid Preflight request %s')
       
    57         raise
       
    58 
       
    59 def process_preflight(req, config):
       
    60     """cross origin resource sharing (preflight)
       
    61     Cf http://www.w3.org/TR/cors/#resource-preflight-requests
       
    62     """
       
    63     origin = check_origin(req, config)
       
    64     allowed_methods = set(config['access-control-allow-methods'])
       
    65     allowed_headers = set(config['access-control-allow-headers'])
       
    66     try:
       
    67         method = req.get_header('Access-Control-Request-Method')
       
    68     except ValueError:
       
    69         raise CORSFailed('Access-Control-Request-Method is incorrect')
       
    70     if method not in allowed_methods:
       
    71         raise CORSFailed('Method is not allowed')
       
    72     try:
       
    73         req.get_header('Access-Control-Request-Headers', ())
       
    74     except ValueError:
       
    75         raise CORSFailed('Access-Control-Request-Headers is incorrect')
       
    76     req.set_header('Access-Control-Allow-Methods', allowed_methods, raw=False)
       
    77     req.set_header('Access-Control-Allow-Headers', allowed_headers, raw=False)
       
    78 
       
    79     process_common(req, config, origin)
       
    80     raise CORSPreflight()
       
    81 
       
    82 def process_simple(req, config):
       
    83     """Handle the Simple Cross-Origin Request case
       
    84     """
       
    85     origin = check_origin(req, config)
       
    86     exposed_headers = config['access-control-expose-headers']
       
    87     if exposed_headers:
       
    88         req.set_header('Access-Control-Expose-Headers', exposed_headers, raw=False)
       
    89     process_common(req, config, origin)
       
    90 
       
    91 def process_common(req, config, origin):
       
    92     req.set_header('Access-Control-Allow-Origin', origin)
       
    93     # in CW, we always support credential/authentication
       
    94     req.set_header('Access-Control-Allow-Credentials', 'true')
       
    95 
       
    96 def check_origin(req, config):
       
    97     origin = req.get_header('Origin').lower()
       
    98     allowed_origins = config.get('access-control-allow-origin')
       
    99     if not allowed_origins:
       
   100         raise CORSFailed('access-control-allow-origin is not configured')
       
   101     if '*' not in allowed_origins and origin not in allowed_origins:
       
   102         raise CORSFailed('Origin is not allowed')
       
   103     # bit of sanity check; see "6.3 Security"
       
   104     myhost = urlsplit(req.base_url()).netloc
       
   105     host = req.get_header('Host')
       
   106     if host != myhost:
       
   107         info('cross origin resource sharing detected possible '
       
   108              'DNS rebinding attack Host header != host of base_url: '
       
   109              '%s != %s' % (host, myhost))
       
   110         raise CORSFailed('Host header and hostname do not match')
       
   111     # include "Vary: Origin" header (see 6.4)
       
   112     req.headers_out.addHeader('Vary', 'Origin')
       
   113     return origin