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