-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathcherrypy_cors.py
256 lines (199 loc) · 8.82 KB
/
cherrypy_cors.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
import re
import six
import cherrypy
from cherrypy.lib import set_vary_header
import httpagentparser
CORS_ALLOW_METHODS = 'Access-Control-Allow-Methods'
CORS_ALLOW_ORIGIN = 'Access-Control-Allow-Origin'
CORS_ALLOW_CREDENTIALS = 'Access-Control-Allow-Credentials'
CORS_EXPOSE_HEADERS = 'Access-Control-Expose-Headers'
CORS_REQUEST_METHOD = 'Access-Control-Request-Method'
CORS_REQUEST_HEADERS = 'Access-Control-Request-Headers'
CORS_MAX_AGE = 'Access-Control-Max-Age'
CORS_ALLOW_HEADERS = 'Access-Control-Allow-Headers'
PUBLIC_ORIGIN = '*'
def expose(allow_credentials=False, expose_headers=None, origins=None):
"""Adds CORS support to the resource.
If the resource is allowed to be exposed, the value of the
`Access-Control-Allow-Origin`_ header in the response will echo
the `Origin`_ request header, and `Origin` will be
appended to the `Vary`_ response header.
:param allow_credentials: Use credentials to make cookies work
(see `Access-Control-Allow-Credentials`_).
:type allow_credentials: bool
:param expose_headers: List of headers clients will be able to access
(see `Access-Control-Expose-Headers`_).
:type expose_headers: list or NoneType
:param origins: List of allowed origins clients must reference.
:type origins: list or NoneType
:returns: Whether the resource is being exposed.
:rtype: bool
- Configuration example:
.. code-block:: python
config = {
'/static': {
'tools.staticdir.on': True,
'cors.expose.on': True,
}
}
- Decorator example:
.. code-block:: python
@cherrypy_cors.tools.expose()
def DELETE(self):
self._delete()
"""
if _get_cors().expose(allow_credentials, expose_headers, origins):
_safe_caching_headers()
return True
return False
def expose_public(expose_headers=None):
"""Adds CORS support to the resource from any origin.
If the resource is allowed to be exposed, the value of the
`Access-Control-Allow-Origin`_ header in the response will be `*`.
:param expose_headers: List of headers clients will be able to access
(see `Access-Control-Expose-Headers`_).
:type expose_headers: list or NoneType
:rtype: NoneType
"""
_get_cors().expose_public(expose_headers)
def preflight(allowed_methods, allowed_headers=None, allow_credentials=False,
max_age=None, origins=None):
"""Adds CORS `preflight`_ support to a `HTTP OPTIONS` request.
:param allowed_methods: List of supported `HTTP` methods
(see `Access-Control-Allow-Methods`_).
:type allowed_methods: list or NoneType
:param allowed_headers: List of supported `HTTP` headers
(see `Access-Control-Allow-Headers`_).
:type allowed_headers: list or NoneType
:param allow_credentials: Use credentials to make cookies work
(see `Access-Control-Allow-Credentials`_).
:type allow_credentials: bool
:param max_age: Seconds to cache the preflight request
(see `Access-Control-Max-Age`_).
:type max_age: int
:param origins: List of allowed origins clients must reference.
:type origins: list or NoneType
:returns: Whether the preflight is allowed.
:rtype: bool
- Used as a decorator with the `Method Dispatcher`_
.. code-block:: python
@cherrypy_cors.tools.preflight(
allowed_methods=["GET", "DELETE", "PUT"])
def OPTIONS(self):
pass
- Function call with the `Object Dispatcher`_
.. code-block:: python
@cherrypy.expose
@cherrypy.tools.allow(
methods=["GET", "DELETE", "PUT", "OPTIONS"])
def thing(self):
if cherrypy.request.method == "OPTIONS":
cherrypy_cors.preflight(
allowed_methods=["GET", "DELETE", "PUT"])
else:
self._do_other_things()
"""
if _get_cors().preflight(allowed_methods, allowed_headers,
allow_credentials, max_age, origins):
_safe_caching_headers()
return True
return False
def install():
"""Install the toolbox such that it's available in all applications."""
cherrypy._cptree.Application.toolboxes.update(cors=tools)
class CORS(object):
"""A generic CORS handler."""
def __init__(self, req_headers, resp_headers):
self.req_headers = req_headers
self.resp_headers = resp_headers
def expose(self, allow_credentials, expose_headers, origins):
if self._is_valid_origin(origins):
self._add_origin_and_credentials_headers(allow_credentials)
self._add_expose_headers(expose_headers)
return True
return False
def expose_public(self, expose_headers):
self._add_public_origin()
self._add_expose_headers(expose_headers)
def preflight(self, allowed_methods, allowed_headers, allow_credentials,
max_age, origins):
if self._is_valid_preflight_request(allowed_headers, allowed_methods, origins):
self._add_origin_and_credentials_headers(allow_credentials)
self._add_prefligt_headers(allowed_methods, max_age)
return True
return False
@property
def origin(self):
return self.req_headers.get('Origin')
def _is_valid_origin(self, origins):
if origins is None:
origins = [self.origin]
origins = map(self._make_regex, origins)
return (
self.origin is not None
and any(origin.match(self.origin) for origin in origins)
)
@staticmethod
def _make_regex(pattern):
if isinstance(pattern, six.string_types):
pattern = re.compile(re.escape(pattern) + '$')
return pattern
def _add_origin_and_credentials_headers(self, allow_credentials):
self.resp_headers[CORS_ALLOW_ORIGIN] = self.origin
if allow_credentials:
self.resp_headers[CORS_ALLOW_CREDENTIALS] = 'true'
def _add_public_origin(self):
self.resp_headers[CORS_ALLOW_ORIGIN] = PUBLIC_ORIGIN
def _add_expose_headers(self, expose_headers):
if expose_headers:
self.resp_headers[CORS_EXPOSE_HEADERS] = expose_headers
@property
def requested_method(self):
return self.req_headers.get(CORS_REQUEST_METHOD)
@property
def requested_headers(self):
return self.req_headers.get(CORS_REQUEST_HEADERS)
def _has_valid_method(self, allowed_methods):
return (
self.requested_method and
self.requested_method in allowed_methods
)
def _valid_headers(self, allowed_headers):
if self.requested_headers and allowed_headers:
for header in self.requested_headers.split(','):
if header.strip() not in allowed_headers:
return False
return True
def _is_valid_preflight_request(self, allowed_headers, allowed_methods, origins):
return (
self._is_valid_origin(origins) and
self._has_valid_method(allowed_methods) and
self._valid_headers(allowed_headers)
)
def _add_prefligt_headers(self, allowed_methods, max_age):
rh = self.resp_headers
rh[CORS_ALLOW_METHODS] = ', '.join(allowed_methods)
if max_age:
rh[CORS_MAX_AGE] = max_age
if self.requested_headers:
rh[CORS_ALLOW_HEADERS] = self.requested_headers
def _get_cors():
return CORS(
cherrypy.serving.request.headers,
cherrypy.serving.response.headers
)
def _safe_caching_headers():
"""Adds `Origin`_ to the `Vary`_ header to ensure caching works properly.
Except in IE because it will disable caching completely. The caching
strategy in that case is out of the scope of this library.
https://blogs.msdn.microsoft.com/ieinternals/2009/06/17/vary-with-care/
"""
uah = cherrypy.serving.request.headers.get('User-Agent', '')
ua = httpagentparser.detect(uah)
IE = 'Microsoft Internet Explorer'
if ua.get('browser', {}).get('name') != IE:
set_vary_header(cherrypy.serving.response, "Origin")
tools = cherrypy._cptools.Toolbox("cors")
tools.expose = cherrypy.Tool('before_handler', expose)
tools.expose_public = cherrypy.Tool('before_handler', expose_public)
tools.preflight = cherrypy.Tool('before_handler', preflight)