Skip to content

Commit 9e0c757

Browse files
pedrokieferasvetlov
authored andcommitted
Implement CorsViewMixin (#145)
* Implement CorsViewMixin * fix real browser test * Add more tests to CorsViewMixin * Make python 3.4 compatible * Extract preflight handler to a class
1 parent 566c48e commit 9e0c757

13 files changed

Lines changed: 746 additions & 168 deletions

README.rst

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,46 @@ in the router:
346346
for route in list(app.router.routes()):
347347
cors.add(route)
348348
349+
You can also use ``CorsViewMixin`` on ``web.View``:
350+
351+
.. code-block:: python
352+
353+
class CorsView(web.View, CorsViewMixin):
354+
355+
cors_config = {
356+
"*": ResourceOption(
357+
allow_credentials=True,
358+
allow_headers="X-Request-ID",
359+
)
360+
}
361+
362+
@asyncio.coroutine
363+
def get(self):
364+
return web.Response(text="Done")
365+
366+
@custom_cors({
367+
"*": ResourceOption(
368+
allow_credentials=True,
369+
allow_headers="*",
370+
)
371+
})
372+
@asyncio.coroutine
373+
def post(self):
374+
return web.Response(text="Done")
375+
376+
cors = aiohttp_cors.setup(app, defaults={
377+
"*": aiohttp_cors.ResourceOptions(
378+
allow_credentials=True,
379+
expose_headers="*",
380+
allow_headers="*",
381+
)
382+
})
383+
384+
cors.add(
385+
app.router.add_route("*", "/resource", CorsView),
386+
webview=True)
387+
388+
349389
Security
350390
========
351391

@@ -460,7 +500,7 @@ Post release steps:
460500
Bugs
461501
====
462502

463-
Please report bugs, issues, feature requests, etc. on
503+
Please report bugs, issues, feature requests, etc. on
464504
`GitHub <https://github.com/aio-libs/aiohttp_cors/issues>`__.
465505

466506

aiohttp_cors/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,12 @@
2525
)
2626
from .resource_options import ResourceOptions
2727
from .cors_config import CorsConfig
28+
from .mixin import CorsViewMixin, custom_cors
2829

2930
__all__ = (
3031
"__title__", "__version__", "__author__", "__email__", "__summary__",
3132
"__uri__", "__license__", "__copyright__",
32-
"setup", "CorsConfig", "ResourceOptions",
33+
"setup", "CorsConfig", "ResourceOptions", "CorsViewMixin", "custom_cors"
3334
)
3435

3536

aiohttp_cors/abc.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,10 @@ class AbstractRouterAdapter(metaclass=ABCMeta):
5151
"""
5252

5353
@abstractmethod
54-
def add_preflight_handler(self, routing_entity, handler):
54+
def add_preflight_handler(self,
55+
routing_entity,
56+
handler,
57+
webview: bool=False):
5558
"""Add OPTIONS handler for all routes defined by `routing_entity`.
5659
5760
Does nothing if CORS handler already handles routing entity.

aiohttp_cors/cors_config.py

Lines changed: 17 additions & 130 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,12 @@
2525
from .urldispatcher_router_adapter import ResourcesUrlDispatcherRouterAdapter
2626
from .abc import AbstractRouterAdapter
2727
from .resource_options import ResourceOptions
28+
from .preflight_handler import _PreflightHandler
2829

2930
__all__ = (
3031
"CorsConfig",
3132
)
3233

33-
3434
# Positive response to Access-Control-Allow-Credentials
3535
_TRUE = "true"
3636
# CORS simple response headers:
@@ -103,7 +103,7 @@ def _parse_config_options(
103103
_ConfigType = Mapping[str, Union[ResourceOptions, Mapping[str, Any]]]
104104

105105

106-
class _CorsConfigImpl:
106+
class _CorsConfigImpl(_PreflightHandler):
107107

108108
def __init__(self,
109109
app: web.Application,
@@ -118,7 +118,8 @@ def __init__(self,
118118

119119
def add(self,
120120
routing_entity,
121-
config: _ConfigType=None):
121+
config: _ConfigType=None,
122+
webview: bool=False):
122123
"""Enable CORS for specific route or resource.
123124
124125
If route is passed CORS is enabled for route's resource.
@@ -133,9 +134,9 @@ def add(self,
133134
parsed_config = _parse_config_options(config)
134135

135136
self._router_adapter.add_preflight_handler(
136-
routing_entity, self._preflight_handler)
137+
routing_entity, self._preflight_handler, webview=webview)
137138
self._router_adapter.set_config_for_routing_entity(
138-
routing_entity, parsed_config)
139+
routing_entity, parsed_config, webview=webview)
139140

140141
return routing_entity
141142

@@ -196,127 +197,12 @@ def _on_response_prepare(self,
196197
# Set allowed credentials.
197198
response.headers[hdrs.ACCESS_CONTROL_ALLOW_CREDENTIALS] = _TRUE
198199

199-
@staticmethod
200-
def _parse_request_method(request: web.Request):
201-
"""Parse Access-Control-Request-Method header of the preflight request
202-
"""
203-
method = request.headers.get(hdrs.ACCESS_CONTROL_REQUEST_METHOD)
204-
if method is None:
205-
raise web.HTTPForbidden(
206-
text="CORS preflight request failed: "
207-
"'Access-Control-Request-Method' header is not specified")
208-
209-
# FIXME: validate method string (ABNF: method = token), if parsing
210-
# fails, raise HTTPForbidden.
211-
212-
return method
213-
214-
@staticmethod
215-
def _parse_request_headers(request: web.Request):
216-
"""Parse Access-Control-Request-Headers header or the preflight request
217-
218-
Returns set of headers in upper case.
219-
"""
220-
headers = request.headers.get(hdrs.ACCESS_CONTROL_REQUEST_HEADERS)
221-
if headers is None:
222-
return frozenset()
223-
224-
# FIXME: validate each header string, if parsing fails, raise
225-
# HTTPForbidden.
226-
# FIXME: check, that headers split and stripped correctly (according
227-
# to ABNF).
228-
headers = (h.strip(" \t").upper() for h in headers.split(","))
229-
# pylint: disable=bad-builtin
230-
return frozenset(filter(None, headers))
231-
232200
@asyncio.coroutine
233-
def _preflight_handler(self, request: web.Request):
234-
"""CORS preflight request handler"""
235-
236-
# Handle according to part 6.2 of the CORS specification.
237-
238-
origin = request.headers.get(hdrs.ORIGIN)
239-
if origin is None:
240-
# Terminate CORS according to CORS 6.2.1.
241-
raise web.HTTPForbidden(
242-
text="CORS preflight request failed: "
243-
"origin header is not specified in the request")
244-
245-
# CORS 6.2.3. Doing it out of order is not an error.
246-
request_method = self._parse_request_method(request)
247-
248-
# CORS 6.2.5. Doing it out of order is not an error.
249-
250-
try:
251-
config = \
252-
yield from self._router_adapter.get_preflight_request_config(
253-
request, origin, request_method)
254-
except KeyError:
255-
raise web.HTTPForbidden(
256-
text="CORS preflight request failed: "
257-
"request method {!r} is not allowed "
258-
"for {!r} origin".format(request_method, origin))
259-
260-
if not config:
261-
# No allowed origins for the route.
262-
# Terminate CORS according to CORS 6.2.1.
263-
raise web.HTTPForbidden(
264-
text="CORS preflight request failed: "
265-
"no origins are allowed")
266-
267-
options = config.get(origin, config.get("*"))
268-
if options is None:
269-
# No configuration for the origin - deny.
270-
# Terminate CORS according to CORS 6.2.2.
271-
raise web.HTTPForbidden(
272-
text="CORS preflight request failed: "
273-
"origin '{}' is not allowed".format(origin))
274-
275-
# CORS 6.2.4
276-
request_headers = self._parse_request_headers(request)
277-
278-
# CORS 6.2.6
279-
if options.allow_headers == "*":
280-
pass
281-
else:
282-
disallowed_headers = request_headers - options.allow_headers
283-
if disallowed_headers:
284-
raise web.HTTPForbidden(
285-
text="CORS preflight request failed: "
286-
"headers are not allowed: {}".format(
287-
", ".join(disallowed_headers)))
288-
289-
# Ok, CORS actual request with specified in the preflight request
290-
# parameters is allowed.
291-
# Set appropriate headers and return 200 response.
292-
293-
response = web.Response()
294-
295-
# CORS 6.2.7
296-
response.headers[hdrs.ACCESS_CONTROL_ALLOW_ORIGIN] = origin
297-
if options.allow_credentials:
298-
# Set allowed credentials.
299-
response.headers[hdrs.ACCESS_CONTROL_ALLOW_CREDENTIALS] = _TRUE
300-
301-
# CORS 6.2.8
302-
if options.max_age is not None:
303-
response.headers[hdrs.ACCESS_CONTROL_MAX_AGE] = \
304-
str(options.max_age)
305-
306-
# CORS 6.2.9
307-
# TODO: more optimal for client preflight request cache would be to
308-
# respond with ALL allowed methods.
309-
response.headers[hdrs.ACCESS_CONTROL_ALLOW_METHODS] = request_method
310-
311-
# CORS 6.2.10
312-
if request_headers:
313-
# Note: case of the headers in the request is changed, but this
314-
# shouldn't be a problem, since the headers should be compared in
315-
# the case-insensitive way.
316-
response.headers[hdrs.ACCESS_CONTROL_ALLOW_HEADERS] = \
317-
",".join(request_headers)
318-
319-
return response
201+
def _get_config(self, request, origin, request_method):
202+
config = \
203+
yield from self._router_adapter.get_preflight_request_config(
204+
request, origin, request_method)
205+
return config
320206

321207

322208
class CorsConfig:
@@ -341,7 +227,7 @@ def __init__(self, app: web.Application, *,
341227
Router adapter. Required if application uses non-default router.
342228
"""
343229

344-
defaults = _parse_config_options(defaults)
230+
self.defaults = _parse_config_options(defaults)
345231

346232
self._cors_impl = None
347233

@@ -355,13 +241,13 @@ def __init__(self, app: web.Application, *,
355241

356242
elif isinstance(app.router, web.UrlDispatcher):
357243
self._resources_router_adapter = \
358-
ResourcesUrlDispatcherRouterAdapter(app.router, defaults)
244+
ResourcesUrlDispatcherRouterAdapter(app.router, self.defaults)
359245
self._resources_cors_impl = _CorsConfigImpl(
360246
app,
361247
self._resources_router_adapter)
362248
self._old_routes_cors_impl = _CorsConfigImpl(
363249
app,
364-
OldRoutesUrlDispatcherRouterAdapter(app.router, defaults))
250+
OldRoutesUrlDispatcherRouterAdapter(app.router, self.defaults))
365251
else:
366252
raise RuntimeError(
367253
"Router adapter is not specified. "
@@ -370,7 +256,8 @@ def __init__(self, app: web.Application, *,
370256

371257
def add(self,
372258
routing_entity,
373-
config: _ConfigType = None):
259+
config: _ConfigType = None,
260+
webview: bool=False):
374261
"""Enable CORS for specific route or resource.
375262
376263
If route is passed CORS is enabled for route's resource.
@@ -404,7 +291,7 @@ def add(self,
404291
# Route which resource has no CORS configuration, i.e.
405292
# old-style route.
406293
return self._old_routes_cors_impl.add(
407-
routing_entity, config)
294+
routing_entity, config, webview=webview)
408295

409296
else:
410297
raise ValueError(

aiohttp_cors/mixin.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import asyncio
2+
import collections
3+
4+
from aiohttp import hdrs, web
5+
from .preflight_handler import _PreflightHandler
6+
7+
def custom_cors(config):
8+
def wrapper(function):
9+
name = "{}_cors_config".format(function.__name__)
10+
setattr(function, name, config)
11+
return function
12+
return wrapper
13+
14+
15+
class CorsViewMixin(_PreflightHandler):
16+
cors_config = None
17+
18+
@classmethod
19+
def get_request_config(cls, request, request_method):
20+
try:
21+
from . import APP_CONFIG_KEY
22+
cors = request.app[APP_CONFIG_KEY]
23+
except KeyError:
24+
raise ValueError("aiohttp-cors is not configured.")
25+
26+
method = getattr(cls, request_method.lower(), None)
27+
28+
if not method:
29+
raise KeyError()
30+
31+
config_property_key = "{}_cors_config".format(request_method.lower())
32+
33+
custom_config = getattr(method, config_property_key, None)
34+
if not custom_config:
35+
custom_config = {}
36+
37+
class_config = cls.cors_config
38+
if not class_config:
39+
class_config = {}
40+
41+
return collections.ChainMap(custom_config, class_config, cors.defaults)
42+
43+
@asyncio.coroutine
44+
def _get_config(self, request, origin, request_method):
45+
return self.get_request_config(request, request_method)
46+
47+
@asyncio.coroutine
48+
def options(self):
49+
response = yield from self._preflight_handler(self.request)
50+
return response

0 commit comments

Comments
 (0)