From d7298dc1b49ed3c31c6f46b105d5ee8ed5bdfb71 Mon Sep 17 00:00:00 2001 From: froosty Date: Mon, 7 May 2018 17:58:20 +0300 Subject: [PATCH 01/23] Init library --- .gitignore | 28 +++ .isort.cfg | 2 + .travis.yml | 20 +++ LICENCE | 21 +++ MANIFEST.in | 4 + README.rst | 262 +++++++++++++++++++++++++++- aiohttp_csrf/__init__.py | 175 +++++++++++++++++++ aiohttp_csrf/policy.py | 55 ++++++ aiohttp_csrf/storage.py | 116 ++++++++++++ aiohttp_csrf/token_generator.py | 30 ++++ demo/manual_protection.py | 68 ++++++++ demo/middleware.py | 106 +++++++++++ demo/session_storage.py | 113 ++++++++++++ pytest.ini | 2 + requirements_dev.txt | 9 + setup.py | 70 ++++++++ test.py | 35 ++++ tests/__init__.py | 0 tests/conftest.py | 69 ++++++++ tests/test_custom_error_renderer.py | 99 +++++++++++ tests/test_errors.py | 77 ++++++++ tests/test_exempt_decorator.py | 55 ++++++ tests/test_form_policy.py | 158 +++++++++++++++++ tests/test_header_policy.py | 111 ++++++++++++ tests/test_protect_decorator.py | 136 +++++++++++++++ tests/test_storage_api.py | 65 +++++++ tests/test_token_generator.py | 37 ++++ tox.ini | 23 +++ 28 files changed, 1945 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 .isort.cfg create mode 100644 .travis.yml create mode 100644 LICENCE create mode 100644 MANIFEST.in create mode 100644 aiohttp_csrf/__init__.py create mode 100644 aiohttp_csrf/policy.py create mode 100644 aiohttp_csrf/storage.py create mode 100644 aiohttp_csrf/token_generator.py create mode 100644 demo/manual_protection.py create mode 100644 demo/middleware.py create mode 100644 demo/session_storage.py create mode 100644 pytest.ini create mode 100644 requirements_dev.txt create mode 100644 setup.py create mode 100644 test.py create mode 100644 tests/__init__.py create mode 100644 tests/conftest.py create mode 100644 tests/test_custom_error_renderer.py create mode 100644 tests/test_errors.py create mode 100644 tests/test_exempt_decorator.py create mode 100644 tests/test_form_policy.py create mode 100644 tests/test_header_policy.py create mode 100644 tests/test_protect_decorator.py create mode 100644 tests/test_storage_api.py create mode 100644 tests/test_token_generator.py create mode 100644 tox.ini diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3ae2f48 --- /dev/null +++ b/.gitignore @@ -0,0 +1,28 @@ +# python specific +env* +.cache/ +.pytest_cache/ +.idea/ +*.pyc +*.so +*.pyd +aiohttp_csrf.egg-info +build/* +dist/* +MANIFEST +__pycache__/ +*.egg-info/ +.coverage +.python-version +htmlcov + +# generic files to ignore +*~ +*.lock +*.DS_Store +*.swp +*.out + +.tox/ +deps/ +docs/_build/ diff --git a/.isort.cfg b/.isort.cfg new file mode 100644 index 0000000..47f77e9 --- /dev/null +++ b/.isort.cfg @@ -0,0 +1,2 @@ +[settings] +known_third_party=aiohttp_csrf diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..53c1bfd --- /dev/null +++ b/.travis.yml @@ -0,0 +1,20 @@ +dist: trusty +language: python +python: + - "3.5" + - "3.6" +install: + - pip install -U setuptools + - pip install -U pip + - pip install -U wheel + - pip install -U tox +script: + - export TOXENV=py`python -c 'import sys; print("".join(map(str, sys.version_info[:2])))'` + - echo "$TOXENV" + + - tox +cache: + directories: + - $HOME/.cache/pip +notifications: + email: false diff --git a/LICENCE b/LICENCE new file mode 100644 index 0000000..22f6914 --- /dev/null +++ b/LICENCE @@ -0,0 +1,21 @@ +The MIT License + +Copyright (c) Ocean S.A. https://ocean.io/ + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..e24206f --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,4 @@ +include README.rst +include LICENSE +recursive-exclude * __pycache__ +recursive-exclude * *.py[co] diff --git a/README.rst b/README.rst index b779bd2..4ebe07c 100644 --- a/README.rst +++ b/README.rst @@ -1 +1,261 @@ -# aiohttp-csrf \ No newline at end of file +aiohttp_csrf +============ + +The library provides csrf (xsrf) protection for `aiohttp.web`__. + +.. _aiohttp_web: https://docs.aiohttp.org/en/latest/web.html + +__ aiohttp_web_ + +.. image:: https://img.shields.io/travis/wikibusiness/aiohttp-csrf.svg + :target: https://travis-ci.org/wikibusiness/aiohttp-csrf + +Basic usage +----------- + +The library allows you to implement csrf (xsrf) protection for requests + + +Basic usage example: + +.. code-block:: python + + import aiohttp_csrf + from aiohttp import web + + FORM_FIELD_NAME = '_csrf_token' + COOKIE_NAME = 'csrf_token' + + + def make_app(): + csrf_policy = aiohttp_csrf.policy.FormPolicy(FORM_FIELD_NAME) + + csrf_storage = aiohttp_csrf.storage.CookieStorage(COOKIE_NAME) + + app = web.Application() + + aiohttp_csrf.setup(app, policy=csrf_policy, storage=csrf_storage) + + app.middlewares.append(aiohttp_csrf.csrf_middleware) + + async def handler_get_form_with_token(request): + token = await aiohttp_csrf.generate_token(request) + + + body = ''' + + Form with csrf protection + +
+ + + +
+ + + ''' # noqa + + body = body.format(field_name=FORM_FIELD_NAME, token=token) + + return web.Response( + body=body.encode('utf-8'), + content_type='text/html', + ) + + async def handler_post_check(request): + post = await request.post() + + body = 'Hello, {name}'.format(name=post['name']) + + return web.Response( + body=body.encode('utf-8'), + content_type='text/html', + ) + + app.router.add_route( + 'GET', + '/', + handler_get_form_with_token, + ) + + app.router.add_route( + 'POST', + '/', + handler_post_check, + ) + + return app + + + web.run_app(make_app()) + + +Initialize +~~~~~~~~~~ + + +First of all, you need to initialize ``aiohttp_csrf`` in your application: + +.. code-block:: python + + app = web.Application() + + csrf_policy = aiohttp_csrf.policy.FormPolicy(FORM_FIELD_NAME) + + csrf_storage = aiohttp_csrf.storage.CookieStorage(COOKIE_NAME) + + aiohttp_csrf.setup(app, policy=csrf_policy, storage=csrf_storage) + + +Middleware and decorators +~~~~~~~~~~~~~~~~~~~~~~~~~ + + +After initialize you can use ``@aiohttp_csrf.csrf_protect`` for handlers, that you want to protect. +Or you can initialize ``aiohttp_csrf.csrf_middleware`` and do not disturb about using decorator (`full middleware example here`_): + +.. _full middleware example here: demo/middleware.py + +.. code-block:: python + + ... + app.middlewares.append(aiohttp_csrf.csrf_middleware) + ... + + +In this case all your handlers will be protected. + + +**Note:** we strongly recommend to use ``aiohttp_csrf.csrf_middleware`` and ``@aiohttp_csrf.csrf_exempt`` instead of manually managing with ``@aiohttp_csrf.csrf_protect``. +But if you prefer to use ``@aiohttp_csrf.csrf_protect``, don't forget to use ``@aiohttp_csrf.csrf_protect`` for both methods: GET and POST +(`manual protection example`_) + +.. _manual protection example: demo/manual_protection.py + + +If you want to use middleware, but need handlers without protection, you can use ``@aiohttp_csrf.csrf_exempt``. +Mark you handler with this decorator and this handler will not check the token: + +.. code-block:: python + + @aiohttp_csrf.csrf_exempt + async def handler_post_not_check(request): + ... + + + +Generate token +~~~~~~~~~~~~~~ + +For generate token you need to call ``aiohttp_csrf.generate_token`` in your handler: + +.. code-block:: python + + @aiohttp_csrf.csrf_protect + async def handler_get(request): + token = await aiohttp_csrf.generate_token(request) + ... + + +Advanced usage +-------------- + + +Policies +~~~~~~~~ + +You can use different policies for check tokens. Library provides 3 types of policy: + +- **FormPolicy**. This policy will search token in the body of your POST request (Usually use for forms). You need to specify name of field that will be checked. +- **HeaderPolicy**. This policy will search token in headers of your POST request (Usually use for AJAX requests). You need to specify name of header that will be checked. +- **FormAndHeaderPolicy**. This policy combines behavior of **FormPolicy** and **HeaderPolicy**. + +You can implement your custom policies if needed. But make sure that your custom policy implements ``aiohttp_csrf.policy.AbstractPolicy`` interface. + +Storages +~~~~~~~~ + +You can use different types of storages for storing token. Library provides 2 types of storage: + +- **CookieStorage**. Your token will be stored in cookie variable. You need to specify cookie name. +- **SessionStorage**. Your token will be stored in session. You need to specify session variable name. + +**Important:** If you want to use session storage, you need setup aiohttp_session in your application +(`session storage example`_) + +.. _session storage example: demo/session_storage.py#L22 + +You can implement your custom storages if needed. But make sure that your custom storage implements ``aiohttp_csrf.storage.AbstractStorage`` interface. + + +Token generators +~~~~~~~~~~~~~~~~ + +You can use different token generator in your application. +By default storages using ``aiohttp_csrf.token_generator.SimpleTokenGenerator`` + +But if you need more secure token generator - you can use ``aiohttp_csrf.token_generator.HashedTokenGenerator`` + +And you can implement your custom token generators if needed. But make sure that your custom token generator implements ``aiohttp_csrf.token_generator.AbstractTokenGenerator`` interface. + + +Invalid token behavior +~~~~~~~~~~~~~~~~~~~~~~ + +By default, if token is invalid, ``aiohttp_csrf`` will raise ``aiohttp.web.HTTPForbidden`` exception. + +You have abbility to specify your custom error handler. It can be: + +- **callable instance**. Input parameter - aiohttp request. +.. code-block:: python + + def custom_error_handler(request): + # do something + return aiohttp.web.Response(status=403) + + # or + + async def custom_async_error_handler(request): + # await do something + return aiohttp.web.Response(status=403) + +It will be called instead of protected handler. + +- **sub class of Exception**. In this case this Exception will be raised. + +.. code-block:: python + + class CustomException(Exception): + pass + + +You can specify custom error handler globally, when initialize ``aiohttp_csrf`` in your application: + +.. code-block:: python + + ... + class CustomException(Exception): + pass + + ... + aiohttp_csrf.setup(app, policy=csrf_policy, storage=csrf_storage, error_renderer=CustomException) + ... + +In this case custom error handler will be applied to all protected handlers. + +Or you can specify custom error handler locally, for specific handler: + +.. code-block:: python + + ... + class CustomException(Exception): + pass + + ... + @aiohttp_csrf.csrf_protect(error_renderer=CustomException) + def handler_with_custom_csrf_error(request): + ... + + +In this case custom error handler will be applied to this handler only. +For all other handlers will be applied global error handler. diff --git a/aiohttp_csrf/__init__.py b/aiohttp_csrf/__init__.py new file mode 100644 index 0000000..7a4c361 --- /dev/null +++ b/aiohttp_csrf/__init__.py @@ -0,0 +1,175 @@ +import asyncio +import inspect + +from functools import wraps + +from aiohttp import web + +from .policy import AbstractPolicy +from .storage import AbstractStorage + + +__version__ = '0.0.1' + +APP_POLICY_KEY = 'aiohttp_csrf_policy' +APP_STORAGE_KEY = 'aiohttp_csrf_storage' +APP_ERROR_RENDERER_KEY = 'aiohttp_csrf_error_renderer' + +MIDDLEWARE_SKIP_PROPERTY = 'csrf_middleware_skip' + +UNPROTECTED_HTTP_METHODS = ('GET', 'HEAD', 'OPTIONS', 'TRACE') + + +def setup(app, *, policy, storage, error_renderer=web.HTTPForbidden): + if not isinstance(policy, AbstractPolicy): + raise TypeError('Policy must be instance of AbstractPolicy') + + if not isinstance(storage, AbstractStorage): + raise TypeError('Storage must be instance of AbstractStorage') + + if not isinstance(error_renderer, Exception) and not callable(error_renderer): # noqa + raise TypeError( + 'Default error renderer must be instance of Exception or callable.' + ) + + app[APP_POLICY_KEY] = policy + app[APP_STORAGE_KEY] = storage + app[APP_ERROR_RENDERER_KEY] = error_renderer + + +def _get_policy(request): + try: + return request.app[APP_POLICY_KEY] + except KeyError: + raise RuntimeError( + 'Policy not found. Install aiohttp_csrf in your ' + 'aiohttp.web.Application using aiohttp_csrf.setup()' + ) + + +def _get_storage(request): + try: + return request.app[APP_STORAGE_KEY] + except KeyError: + raise RuntimeError( + 'Storage not found. Install aiohttp_csrf in your ' + 'aiohttp.web.Application using aiohttp_csrf.setup()' + ) + + +async def _render_error(request, error_renderer=None): + if error_renderer is None: + try: + error_renderer = request.app[APP_ERROR_RENDERER_KEY] + except KeyError: + raise RuntimeError( + 'Default error renderer not found. Install aiohttp_csrf in ' + 'your aiohttp.web.Application using aiohttp_csrf.setup()' + ) + + if inspect.isclass(error_renderer) and issubclass(error_renderer, Exception): # noqa + raise error_renderer + elif callable(error_renderer): + if asyncio.iscoroutinefunction(error_renderer): + return await error_renderer(request) + else: + return error_renderer(request) + else: + raise NotImplementedError + + +async def get_token(request): + storage = _get_storage(request) + + return await storage.get(request) + + +async def generate_token(request): + storage = _get_storage(request) + + return await storage.generate_new_token(request) + + +async def save_token(request, response): + storage = _get_storage(request) + + await storage.save_token(request, response) + + +def csrf_exempt(handler): + @wraps(handler) + def wrapped_handler(*args, **kwargs): + return handler(*args, **kwargs) + + setattr(wrapped_handler, MIDDLEWARE_SKIP_PROPERTY, True) + + return wrapped_handler + + +async def _check(request): + if not isinstance(request, web.Request): + raise RuntimeError('Can\'t get request from handler params') + + original_token = await get_token(request) + + policy = _get_policy(request) + + return await policy.check(request, original_token) + + +def csrf_protect(handler=None, error_renderer=None): + if ( + error_renderer is not None + and not isinstance(error_renderer, Exception) + and not callable(error_renderer) + ): + raise TypeError( + 'Renderer must be instance of Exception or callable.' + ) + + def wrapper(handler): + @wraps(handler) + async def wrapped(*args, **kwargs): + request = args[-1] + + if isinstance(request, web.View): + request = request.request + + if ( + request.method not in UNPROTECTED_HTTP_METHODS + and not await _check(request) + ): + return await _render_error(request, error_renderer) + + raise_response = False + + try: + response = await handler(*args, **kwargs) + except web.HTTPException as exc: + response = exc + raise_response = True + + if isinstance(response, web.Response): + await save_token(request, response) + + if raise_response: + raise response + + return response + + setattr(wrapped, MIDDLEWARE_SKIP_PROPERTY, True) + + return wrapped + + if handler is None: + return wrapper + + return wrapper(handler) + + +@web.middleware +async def csrf_middleware(request, handler): + if not getattr(handler, MIDDLEWARE_SKIP_PROPERTY, False): + handler = csrf_protect(handler=handler) + + return await handler(request) diff --git a/aiohttp_csrf/policy.py b/aiohttp_csrf/policy.py new file mode 100644 index 0000000..1700e1d --- /dev/null +++ b/aiohttp_csrf/policy.py @@ -0,0 +1,55 @@ +import abc + + +class AbstractPolicy(metaclass=abc.ABCMeta): + @abc.abstractmethod + async def check(self, request, original_value): + pass # pragma: no cover + + +class FormPolicy(AbstractPolicy): + + def __init__(self, field_name): + self.field_name = field_name + + async def check(self, request, original_value): + post = await request.post() + + token = post.get(self.field_name) + + return token == original_value + + +class HeaderPolicy(AbstractPolicy): + + def __init__(self, header_name): + self.header_name = header_name + + async def check(self, request, original_value): + token = request.headers.get(self.header_name) + + return token == original_value + + +class FormAndHeaderPolicy(HeaderPolicy, FormPolicy): + + def __init__(self, header_name, field_name): + self.header_name = header_name + self.field_name = field_name + + async def check(self, request, original_value): + header_check = await HeaderPolicy.check( + self, + request, + original_value, + ) + + if header_check: + return True + + form_check = await FormPolicy.check(self, request, original_value) + + if form_check: + return True + + return False diff --git a/aiohttp_csrf/storage.py b/aiohttp_csrf/storage.py new file mode 100644 index 0000000..c77a227 --- /dev/null +++ b/aiohttp_csrf/storage.py @@ -0,0 +1,116 @@ +import abc + +from .token_generator import AbstractTokenGenerator, SimpleTokenGenerator + +try: + from aiohttp_session import get_session +except ImportError: # pragma: no cover + pass + + +REQUEST_NEW_TOKEN_KEY = 'aiohttp_csrf_new_token' + + +class AbstractStorage(metaclass=abc.ABCMeta): + + @abc.abstractmethod + async def generate_new_token(self, request): + pass # pragma: no cover + + @abc.abstractmethod + async def get(self, request): + pass # pragma: no cover + + @abc.abstractmethod + async def save_token(self, request, response): + pass # pragma: no cover + + +class BaseStorage(AbstractStorage, metaclass=abc.ABCMeta): + + def __init__(self, token_generator=None): + if token_generator is None: + token_generator = SimpleTokenGenerator() + elif not isinstance(token_generator, AbstractTokenGenerator): + raise TypeError( + 'Token generator must be instance of AbstractTokenGenerator', + ) + + self.token_generator = token_generator + + def _generate_token(self): + return self.token_generator.generate() + + async def generate_new_token(self, request): + if REQUEST_NEW_TOKEN_KEY in request: + return request[REQUEST_NEW_TOKEN_KEY] + + token = self._generate_token() + + request[REQUEST_NEW_TOKEN_KEY] = token + + return token + + @abc.abstractmethod + async def _get(self, request): + pass # pragma: no cover + + async def get(self, request): + token = await self._get(request) + + await self.generate_new_token(request) + + return token + + @abc.abstractmethod + async def _save_token(self, request, response, token): + pass # pragma: no cover + + async def save_token(self, request, response): + old_token = await self._get(request) + + if REQUEST_NEW_TOKEN_KEY in request: + token = request[REQUEST_NEW_TOKEN_KEY] + elif old_token is None: + token = await self.generate_new_token(request) + else: + token = None + + if token is not None: + await self._save_token(request, response, token) + + +class CookieStorage(BaseStorage): + + def __init__(self, cookie_name, cookie_kwargs=None, *args, **kwargs): + self.cookie_name = cookie_name + self.cookie_kwargs = cookie_kwargs or {} + + super().__init__(*args, **kwargs) + + async def _get(self, request): + return request.cookies.get(self.cookie_name, None) + + async def _save_token(self, request, response, token): + response.set_cookie( + self.cookie_name, + token, + **self.cookie_kwargs, + ) + + +class SessionStorage(BaseStorage): + def __init__(self, session_name, *args, **kwargs): + self.session_name = session_name + + super().__init__(*args, **kwargs) + + async def _get(self, request): + session = await get_session(request) + + return session.get(self.session_name, None) + + async def _save_token(self, request, response, token): + session = await get_session(request) + + session[self.session_name] = token diff --git a/aiohttp_csrf/token_generator.py b/aiohttp_csrf/token_generator.py new file mode 100644 index 0000000..dcecbb7 --- /dev/null +++ b/aiohttp_csrf/token_generator.py @@ -0,0 +1,30 @@ +import abc +import hashlib +import uuid + + +class AbstractTokenGenerator(metaclass=abc.ABCMeta): + @abc.abstractmethod + def generate(self): + pass # pragma: no cover + + +class SimpleTokenGenerator(AbstractTokenGenerator): + def generate(self): + return uuid.uuid4().hex + + +class HashedTokenGenerator(AbstractTokenGenerator): + encoding = 'utf-8' + + def __init__(self, secret_phrase): + self.secret_phrase = secret_phrase + + def generate(self): + token = uuid.uuid4().hex + + token += self.secret_phrase + + hasher = hashlib.sha256(token.encode(self.encoding)) + + return hasher.hexdigest() diff --git a/demo/manual_protection.py b/demo/manual_protection.py new file mode 100644 index 0000000..07caaa1 --- /dev/null +++ b/demo/manual_protection.py @@ -0,0 +1,68 @@ +import aiohttp_csrf +from aiohttp import web + +FORM_FIELD_NAME = '_csrf_token' +COOKIE_NAME = 'csrf_token' + + +def make_app(): + csrf_policy = aiohttp_csrf.policy.FormPolicy(FORM_FIELD_NAME) + + csrf_storage = aiohttp_csrf.storage.CookieStorage(COOKIE_NAME) + + app = web.Application() + + aiohttp_csrf.setup(app, policy=csrf_policy, storage=csrf_storage) + + # IMPORTANT! You need use @csrf_protect for both methods: GET and POST + @aiohttp_csrf.csrf_protect + async def handler_get(request): + token = await aiohttp_csrf.generate_token(request) + + body = ''' + + Form with csrf protection + +
+ + + +
+ + + ''' # noqa + + body = body.format(field_name=FORM_FIELD_NAME, token=token) + + return web.Response( + body=body.encode('utf-8'), + content_type='text/html', + ) + + @aiohttp_csrf.csrf_protect + async def handler_post(request): + post = await request.post() + + body = 'Hello, {name}'.format(name=post['name']) + + return web.Response( + body=body.encode('utf-8'), + content_type='text/html', + ) + + app.router.add_route( + 'GET', + '/', + handler_get, + ) + + app.router.add_route( + 'POST', + '/', + handler_post, + ) + + return app + + +web.run_app(make_app()) diff --git a/demo/middleware.py b/demo/middleware.py new file mode 100644 index 0000000..42220b0 --- /dev/null +++ b/demo/middleware.py @@ -0,0 +1,106 @@ +import aiohttp_csrf +from aiohttp import web + +FORM_FIELD_NAME = '_csrf_token' +COOKIE_NAME = 'csrf_token' + + +def make_app(): + csrf_policy = aiohttp_csrf.policy.FormPolicy(FORM_FIELD_NAME) + + csrf_storage = aiohttp_csrf.storage.CookieStorage(COOKIE_NAME) + + app = web.Application() + + aiohttp_csrf.setup(app, policy=csrf_policy, storage=csrf_storage) + + app.middlewares.append(aiohttp_csrf.csrf_middleware) + + async def handler_get_form_with_token(request): + token = await aiohttp_csrf.generate_token(request) + + body = ''' + + Form with csrf protection + +
+ + + +
+ + + ''' # noqa + + body = body.format(field_name=FORM_FIELD_NAME, token=token) + + return web.Response( + body=body.encode('utf-8'), + content_type='text/html', + ) + + async def handler_post_check(request): + post = await request.post() + + body = 'Hello, {name}'.format(name=post['name']) + + return web.Response( + body=body.encode('utf-8'), + content_type='text/html', + ) + + async def handler_get_form_without_token(request): + body = ''' + + Form without csrf protection + +
+ + +
+ + + ''' + + return web.Response( + body=body.encode('utf-8'), + content_type='text/html', + ) + + @aiohttp_csrf.csrf_exempt + async def handler_post_not_check(request): + post = await request.post() + + body = 'Hello, {name}'.format(name=post['name']) + + return web.Response( + body=body.encode('utf-8'), + content_type='text/html', + ) + + app.router.add_route( + 'GET', + '/form_with_check', + handler_get_form_with_token, + ) + app.router.add_route( + 'POST', + '/post_with_check', + handler_post_check, + ) + + app.router.add_route( + 'GET', + '/form_without_check', + handler_get_form_without_token, + ) + app.router.add_route( + 'POST', + '/post_without_check', + handler_post_not_check, + ) + + return app + + +web.run_app(make_app()) diff --git a/demo/session_storage.py b/demo/session_storage.py new file mode 100644 index 0000000..e27a489 --- /dev/null +++ b/demo/session_storage.py @@ -0,0 +1,113 @@ +import aiohttp_csrf +from aiohttp import web +from aiohttp_session import setup as setup_session +from aiohttp_session import SimpleCookieStorage + +FORM_FIELD_NAME = '_csrf_token' +SESSION_NAME = 'csrf_token' + + +def make_app(): + csrf_policy = aiohttp_csrf.policy.FormPolicy(FORM_FIELD_NAME) + + csrf_storage = aiohttp_csrf.storage.SessionStorage(SESSION_NAME) + + app = web.Application() + + aiohttp_csrf.setup(app, policy=csrf_policy, storage=csrf_storage) + + session_storage = SimpleCookieStorage() + + # Important!!! + setup_session(app, session_storage) + + app.middlewares.append(aiohttp_csrf.csrf_middleware) + + async def handler_get_form_with_token(request): + token = await aiohttp_csrf.generate_token(request) + + body = ''' + + Form with csrf protection + +
+ + + +
+ + + ''' # noqa + + body = body.format(field_name=FORM_FIELD_NAME, token=token) + + return web.Response( + body=body.encode('utf-8'), + content_type='text/html', + ) + + async def handler_post_check(request): + post = await request.post() + + body = 'Hello, {name}'.format(name=post['name']) + + return web.Response( + body=body.encode('utf-8'), + content_type='text/html', + ) + + async def handler_get_form_without_token(request): + body = ''' + + Form without csrf protection + +
+ + +
+ + + ''' + + return web.Response( + body=body.encode('utf-8'), + content_type='text/html', + ) + + @aiohttp_csrf.csrf_exempt + async def handler_post_not_check(request): + post = await request.post() + + body = 'Hello, {name}'.format(name=post['name']) + + return web.Response( + body=body.encode('utf-8'), + content_type='text/html', + ) + + app.router.add_route( + 'GET', + '/form_with_check', + handler_get_form_with_token, + ) + app.router.add_route( + 'POST', + '/post_with_check', + handler_post_check, + ) + + app.router.add_route( + 'GET', + '/form_without_check', + handler_get_form_without_token, + ) + app.router.add_route( + 'POST', + '/post_without_check', + handler_post_not_check, + ) + + return app + + +web.run_app(make_app()) diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..870e595 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +addopts= --no-cov-on-fail --cov=aiohttp_csrf --cov-report=term --cov-report=html diff --git a/requirements_dev.txt b/requirements_dev.txt new file mode 100644 index 0000000..f5d515e --- /dev/null +++ b/requirements_dev.txt @@ -0,0 +1,9 @@ +aiohttp==2.3.10 +-e . +aiohttp-session==1.0.1 +flake8==3.4.1 +isort==4.2.15 +pytest==3.4.0 +pytest-aiohttp==0.1.3 +pytest-cov==2.5.1 +tox==2.9.1 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..4396b14 --- /dev/null +++ b/setup.py @@ -0,0 +1,70 @@ +import codecs +import io +import os +import re + +from setuptools import setup + + +def get_version(): + with codecs.open( + os.path.join( + os.path.abspath( + os.path.dirname(__file__), + ), + 'aiohttp_csrf', + '__init__.py', + ), + 'r', + 'utf-8', + ) as fp: + try: + return re.findall(r"^__version__ = '([^']+)'$", fp.read(), re.M)[0] + except IndexError: + raise RuntimeError('Unable to determine version.') + + +def read(*parts): + filename = os.path.join(os.path.abspath(os.path.dirname(__file__)), *parts) + + with io.open(filename, encoding='utf-8', mode='rt') as fp: + return fp.read() + + +install_requires = ['aiohttp>=3.2.0'] +extras_require = { + 'session': ['aiohttp-session>=2.4.0'], +} + + +setup( + name='aiohttp-csrf', + version=get_version(), + description=('CSRF protection for aiohttp.web',), + long_description=read('README.rst'), + author='Ocean S.A.', + author_email='osf@ocean.io', + url='https://github.com/wikibusiness/aiohttp-csrf', + packages=['aiohttp_csrf'], + include_package_data=True, + install_requires=install_requires, + extras_require=extras_require, + zip_safe=False, + classifiers=[ + 'Development Status :: 5 - Production/Stable', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: MIT License', + 'Operating System :: POSIX', + 'Operating System :: MacOS :: MacOS X', + 'Operating System :: Microsoft :: Windows', + 'Programming Language :: Python', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + ], + keywords=[ + 'csrf', + 'xsrf', + 'aiohttp', + ], +) diff --git a/test.py b/test.py new file mode 100644 index 0000000..beea3a1 --- /dev/null +++ b/test.py @@ -0,0 +1,35 @@ +import asyncio + +from aiohttp import web + +async def hello(request): + return web.Response(text="Hello, world") + + +def dec(handler): + def wrapped(*args, **kwargs): + request = args[-1] + import ipdb;ipdb.set_trace() + return handler(*args, **kwargs) + + return wrapped + + +class MyView(web.View): + @dec + async def get(self): + return web.Response(text="Get Hello, world") + + async def post(self): + return web.Response(text="Post Hello, world") + + +@web.middleware +async def middleware(request, handler): + return await handler(request) + + +app = web.Application(middlewares=[middleware]) +app.router.add_route('*', '/', MyView) + +web.run_app(app) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..1a2f168 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,69 @@ +import aiohttp_csrf +import pytest +from aiohttp import web + +SESSION_NAME = COOKIE_NAME = 'csrf_token' +FORM_FIELD_NAME = HEADER_NAME = 'X-CSRF-TOKEN' + + +@pytest.yield_fixture +def init_app(): + def go( + loop, + policy, + storage, + handlers, + error_renderer=None, + ): + app = web.Application() + + kwargs = { + 'policy': policy, + 'storage': storage, + } + + if error_renderer is not None: + kwargs['error_renderer'] = error_renderer + + aiohttp_csrf.setup(app, **kwargs) + + for method, url, handler in handlers: + app.router.add_route( + method, + url, + handler, + ) + + return app + + yield go + + +@pytest.fixture(params=[ + (aiohttp_csrf.policy.FormPolicy, (FORM_FIELD_NAME,)), + (aiohttp_csrf.policy.FormAndHeaderPolicy, (HEADER_NAME, FORM_FIELD_NAME)), +]) +def csrf_form_policy(request): + _class, args = request.param + + return _class(*args) + + +@pytest.fixture(params=[ + (aiohttp_csrf.policy.HeaderPolicy, (HEADER_NAME,)), + (aiohttp_csrf.policy.FormAndHeaderPolicy, (HEADER_NAME, FORM_FIELD_NAME)), +]) +def csrf_header_policy(request): + _class, args = request.param + + return _class(*args) + + +@pytest.fixture(params=[ + (aiohttp_csrf.storage.SessionStorage, (SESSION_NAME,)), + (aiohttp_csrf.storage.CookieStorage, (COOKIE_NAME,)), +]) +def csrf_storage(request): + _class, args = request.param + + return _class(*args) diff --git a/tests/test_custom_error_renderer.py b/tests/test_custom_error_renderer.py new file mode 100644 index 0000000..a4160a0 --- /dev/null +++ b/tests/test_custom_error_renderer.py @@ -0,0 +1,99 @@ +import asyncio + +import aiohttp_csrf +import pytest +from aiohttp import web + +COOKIE_NAME = 'csrf_token' +HEADER_NAME = 'X-CSRF-TOKEN' + + +@pytest.yield_fixture +def create_app(init_app): + def go(loop, error_renderer): + @aiohttp_csrf.csrf_protect + async def handler_get(request): + await aiohttp_csrf.generate_token(request) + + return web.Response(body=b'OK') + + @aiohttp_csrf.csrf_protect(error_renderer=error_renderer) + async def handler_post(request): + return web.Response(body=b'OK') + + handlers = [ + ('GET', '/', handler_get), + ('POST', '/', handler_post) + ] + + storage = aiohttp_csrf.storage.CookieStorage(COOKIE_NAME) + policy = aiohttp_csrf.policy.HeaderPolicy(HEADER_NAME) + + app = init_app( + policy=policy, + storage=storage, + handlers=handlers, + loop=loop, + ) + + return app + + yield go + + +async def test_custom_exception_error_renderer(test_client, create_app): + client = await test_client( + create_app, + error_renderer=web.HTTPBadRequest, + ) + + await client.get('/') + + resp = await client.post('/') + + assert resp.status == web.HTTPBadRequest.status_code + + +@pytest.fixture(params=[False, True]) +def make_error_renderer(request): + is_coroutine = request.param + + def make_renderer(error_body): + def error_renderer(request): + return web.Response(body=error_body) + + if not is_coroutine: + return error_renderer + + return asyncio.coroutine(error_renderer) + + return make_renderer + + +async def test_custom_coroutine_callable_error_renderer(test_client, create_app, make_error_renderer): # noqa + error_body = b'CSRF error' + + error_renderer = make_error_renderer(error_body) + + client = await test_client( + create_app, + error_renderer=error_renderer, + ) + + await client.get('/') + + resp = await client.post('/') + + assert resp.status == 200 + + assert await resp.read() == error_body + + +async def test_bad_error_renderer(test_client, create_app): + error_renderer = 'trololo' + + with pytest.raises(TypeError): + await test_client( + create_app, + error_renderer=error_renderer, + ) diff --git a/tests/test_errors.py b/tests/test_errors.py new file mode 100644 index 0000000..0d7cf36 --- /dev/null +++ b/tests/test_errors.py @@ -0,0 +1,77 @@ +import aiohttp_csrf +import pytest +from aiohttp import web + +COOKIE_NAME = 'csrf_token' +HEADER_NAME = 'X-CSRF-TOKEN' + + +class FakeClass: + pass + + +async def test_bad_policy(test_client, init_app): + policy = FakeClass() + storage = aiohttp_csrf.storage.CookieStorage(COOKIE_NAME) + + with pytest.raises(TypeError): + await test_client( + init_app, + policy=policy, + storage=storage, + handlers=[], + ) + + +async def test_bad_storage(test_client, init_app): + policy = aiohttp_csrf.policy.HeaderPolicy(HEADER_NAME) + storage = FakeClass() + + with pytest.raises(TypeError): + await test_client( + init_app, + policy=policy, + storage=storage, + handlers=[], + ) + + +async def test_bad_error_renderer(test_client, init_app): + policy = aiohttp_csrf.policy.HeaderPolicy(HEADER_NAME) + storage = aiohttp_csrf.storage.CookieStorage(COOKIE_NAME) + + with pytest.raises(TypeError): + await test_client( + init_app, + policy=policy, + storage=storage, + error_renderer=1, + handlers=[], + ) + + +async def test_app_without_setup(test_client): + def create_app(loop): + app = web.Application() + + @aiohttp_csrf.csrf_protect + async def handler(request): + await aiohttp_csrf.generate_token(request) + + return web.Response() + + app.router.add_route( + 'GET', + '/', + handler, + ) + + return app + + client = await test_client( + create_app, + ) + + resp = await client.get('/') + + assert resp.status == 500 diff --git a/tests/test_exempt_decorator.py b/tests/test_exempt_decorator.py new file mode 100644 index 0000000..6802827 --- /dev/null +++ b/tests/test_exempt_decorator.py @@ -0,0 +1,55 @@ +import aiohttp_csrf +import pytest +from aiohttp import web + +COOKIE_NAME = 'csrf_token' +HEADER_NAME = 'X-CSRF-TOKEN' + + +@pytest.yield_fixture +def create_app(init_app): + def go(loop): + async def handler_get(request): + await aiohttp_csrf.generate_token(request) + + return web.Response(body=b'OK') + + @aiohttp_csrf.csrf_exempt + async def handler_post(request): + return web.Response(body=b'OK') + + handlers = [ + ('GET', '/', handler_get), + ('POST', '/', handler_post), + ] + + policy = aiohttp_csrf.policy.HeaderPolicy(HEADER_NAME) + storage = aiohttp_csrf.storage.CookieStorage(COOKIE_NAME) + + app = init_app( + policy=policy, + storage=storage, + handlers=handlers, + loop=loop, + ) + + app.middlewares.append(aiohttp_csrf.csrf_middleware) + + return app + + yield go + + +async def test_decorator_method_view(test_client, create_app): + + client = await test_client( + create_app, + ) + + resp = await client.get('/') + + assert resp.status == 200 + + resp = await client.post('/') + + assert resp.status == 200 diff --git a/tests/test_form_policy.py b/tests/test_form_policy.py new file mode 100644 index 0000000..11a9689 --- /dev/null +++ b/tests/test_form_policy.py @@ -0,0 +1,158 @@ +import re +import uuid +from unittest import mock + +import aiohttp_csrf +import pytest +from aiohttp import web +from aiohttp_session import setup as setup_session +from aiohttp_session import SimpleCookieStorage + +from .conftest import FORM_FIELD_NAME + +FORM_FIELD_REGEX = re.compile( + r'', +) + + +@pytest.yield_fixture +def create_app(init_app): + def go(loop, policy, storage): + async def handler_get(request): + token = await aiohttp_csrf.generate_token(request) + + body = ''' + + + +
+ +
+ + + ''' # noqa + + body = body.format(field_name=FORM_FIELD_NAME, token=token) + + return web.Response(body=body.encode('utf-8')) + + async def handler_post(request): + return web.Response(body=b'OK') + + handlers = [ + ('GET', '/', handler_get), + ('POST', '/', handler_post) + ] + + app = init_app( + policy=policy, + storage=storage, + handlers=handlers, + loop=loop, + ) + + if isinstance(storage, aiohttp_csrf.storage.SessionStorage): + session_storage = SimpleCookieStorage() + setup_session(app, session_storage) + + app.middlewares.append(aiohttp_csrf.csrf_middleware) + + return app + + yield go + + +async def test_form_policy_success( + test_client, + create_app, + csrf_form_policy, + csrf_storage, +): + client = await test_client( + create_app, + policy=csrf_form_policy, + storage=csrf_storage, + ) + + resp = await client.get('/') + + assert resp.status == 200 + + body = await resp.text() + + search_result = FORM_FIELD_REGEX.search(body) + + token = search_result.group('token') + + data = {FORM_FIELD_NAME: token} + + resp = await client.post('/', data=data) + + assert resp.status == 200 + + +async def test_form_policy_bad_token( + test_client, + create_app, + csrf_form_policy, + csrf_storage, +): + real_token = uuid.uuid4().hex + + bad_token = real_token + + while bad_token == real_token: + bad_token = uuid.uuid4().hex + + with mock.patch( + 'aiohttp_csrf.token_generator.SimpleTokenGenerator.generate', + return_value=real_token, + ): + client = await test_client( + create_app, + policy=csrf_form_policy, + storage=csrf_storage, + ) + + resp = await client.get('/') + + assert resp.status == 200 + + data = {FORM_FIELD_NAME: bad_token} + + resp = await client.post('/', data=data) + + assert resp.status == 403 + + +async def test_form_policy_reuse_token( + test_client, + create_app, + csrf_form_policy, + csrf_storage, +): + client = await test_client( + create_app, + policy=csrf_form_policy, + storage=csrf_storage, + ) + + resp = await client.get('/') + + assert resp.status == 200 + + body = await resp.text() + + search_result = FORM_FIELD_REGEX.search(body) + + token = search_result.group('token') + + data = {FORM_FIELD_NAME: token} + + resp = await client.post('/', data=data) + + assert resp.status == 200 + + resp = await client.post('/', data=data) + + assert resp.status == 403 diff --git a/tests/test_header_policy.py b/tests/test_header_policy.py new file mode 100644 index 0000000..66b9bbf --- /dev/null +++ b/tests/test_header_policy.py @@ -0,0 +1,111 @@ +import uuid +from unittest import mock + +import aiohttp_csrf +import pytest +from aiohttp import web + +from .conftest import COOKIE_NAME, HEADER_NAME + + +@pytest.yield_fixture +def create_app(init_app): + def go(loop, policy): + async def handler_get(request): + await aiohttp_csrf.generate_token(request) + + return web.Response(body=b'OK') + + async def handler_post(request): + return web.Response(body=b'OK') + + handlers = [ + ('GET', '/', handler_get), + ('POST', '/', handler_post) + ] + + storage = aiohttp_csrf.storage.CookieStorage(COOKIE_NAME) + + app = init_app( + policy=policy, + storage=storage, + handlers=handlers, + loop=loop, + ) + + app.middlewares.append(aiohttp_csrf.csrf_middleware) + + return app + + yield go + + +async def test_header_policy_success(test_client, create_app, csrf_header_policy): # noqa + client = await test_client( + create_app, + policy=csrf_header_policy, + ) + + resp = await client.get('/') + + assert resp.status == 200 + + token = resp.cookies[COOKIE_NAME].value + + headers = {HEADER_NAME: token} + + resp = await client.post('/', headers=headers) + + assert resp.status == 200 + + +async def test_header_policy_bad_token(test_client, create_app, csrf_header_policy): # noqa + real_token = uuid.uuid4().hex + + bad_token = real_token + + while bad_token == real_token: + bad_token = uuid.uuid4().hex + + with mock.patch( + 'aiohttp_csrf.token_generator.SimpleTokenGenerator.generate', + return_value=real_token, + ): + + client = await test_client( + create_app, + policy=csrf_header_policy, + ) + + resp = await client.get('/') + + assert resp.status == 200 + + headers = {HEADER_NAME: bad_token} + + resp = await client.post('/', headers=headers) + + assert resp.status == 403 + + +async def test_header_policy_reuse_token(test_client, create_app, csrf_header_policy): # noqa + client = await test_client( + create_app, + policy=csrf_header_policy, + ) + + resp = await client.get('/') + + assert resp.status == 200 + + token = resp.cookies[COOKIE_NAME].value + + headers = {HEADER_NAME: token} + + resp = await client.post('/', headers=headers) + + assert resp.status == 200 + + resp = await client.post('/', headers=headers) + + assert resp.status == 403 diff --git a/tests/test_protect_decorator.py b/tests/test_protect_decorator.py new file mode 100644 index 0000000..94976f6 --- /dev/null +++ b/tests/test_protect_decorator.py @@ -0,0 +1,136 @@ +import aiohttp_csrf +from aiohttp import web + +COOKIE_NAME = 'csrf_token' +HEADER_NAME = 'X-CSRF-TOKEN' + + +async def test_decorator_method_view(test_client, init_app): + @aiohttp_csrf.csrf_protect + async def handler_get(request): + await aiohttp_csrf.generate_token(request) + + return web.Response(body=b'OK') + + @aiohttp_csrf.csrf_protect + async def handler_post(request): + return web.Response(body=b'OK') + + handlers = [ + ('GET', '/', handler_get), + ('POST', '/', handler_post) + ] + + policy = aiohttp_csrf.policy.HeaderPolicy(HEADER_NAME) + storage = aiohttp_csrf.storage.CookieStorage(COOKIE_NAME) + + client = await test_client( + init_app, + policy=policy, + storage=storage, + handlers=handlers, + ) + + resp = await client.get('/') + + assert resp.status == 200 + + token = resp.cookies[COOKIE_NAME].value + + headers = {HEADER_NAME: token} + + resp = await client.post('/', headers=headers) + + assert resp.status == 200 + + resp = await client.post('/', headers=headers) + + assert resp.status == 403 + + +async def test_decorator_class_view(test_client): + class TestView(web.View): + @aiohttp_csrf.csrf_protect + async def get(self): + await aiohttp_csrf.generate_token(self.request) + + return web.Response(body=b'OK') + + @aiohttp_csrf.csrf_protect + async def post(self): + return web.Response(body=b'OK') + + def create_app(loop): + policy = aiohttp_csrf.policy.HeaderPolicy(HEADER_NAME) + storage = aiohttp_csrf.storage.CookieStorage(COOKIE_NAME) + + app = web.Application() + + aiohttp_csrf.setup(app, policy=policy, storage=storage) + + if hasattr(app.router, 'add_view'): + # For aiohttp >= 3.0.0 + app.router.add_view('/', TestView) + else: + app.router.add_route('*', '/', TestView) + + return app + + client = await test_client( + create_app, + ) + + resp = await client.get('/') + + assert resp.status == 200 + + token = resp.cookies[COOKIE_NAME].value + + headers = {HEADER_NAME: token} + + resp = await client.post('/', headers=headers) + + assert resp.status == 200 + + resp = await client.post('/', headers=headers) + + assert resp.status == 403 + + +async def test_handle_http_exceptions(test_client, init_app): + @aiohttp_csrf.csrf_protect + async def handler_get(request): + await aiohttp_csrf.generate_token(request) + + return web.Response(body=b'OK') + + @aiohttp_csrf.csrf_protect + async def handler_post(request): + raise web.HTTPBadRequest + + handlers = [ + ('GET', '/', handler_get), + ('POST', '/', handler_post) + ] + + policy = aiohttp_csrf.policy.HeaderPolicy(HEADER_NAME) + storage = aiohttp_csrf.storage.CookieStorage(COOKIE_NAME) + + client = await test_client( + init_app, + policy=policy, + storage=storage, + handlers=handlers, + ) + + resp = await client.get('/') + + assert resp.status == 200 + + token = resp.cookies[COOKIE_NAME].value + + headers = {HEADER_NAME: token} + + resp = await client.post('/', headers=headers) + + assert resp.status == 400 diff --git a/tests/test_storage_api.py b/tests/test_storage_api.py new file mode 100644 index 0000000..519dab8 --- /dev/null +++ b/tests/test_storage_api.py @@ -0,0 +1,65 @@ +from unittest.mock import MagicMock + +import aiohttp_csrf +import pytest +from aiohttp.test_utils import make_mocked_request + + +class FakeStorage(aiohttp_csrf.storage.BaseStorage): + + async def _get(self, request): + return request.get('my_field') + + async def _save_token(self, request, response, token): + request['my_field'] = token + + +async def test_1(): + storage = FakeStorage() + + storage._generate_token = MagicMock(return_value='1') + storage._get = MagicMock(return_value='1') + storage._save = MagicMock() + + assert storage._generate_token.call_count == 0 + + request = make_mocked_request('/', 'GET') + + await storage.generate_new_token(request) + + assert storage._generate_token.call_count == 1 + + await storage.generate_new_token(request) + await storage.generate_new_token(request) + + assert storage._generate_token.call_count == 1 + + +async def test_2(): + storage = FakeStorage() + + storage._generate_token = MagicMock(return_value='1') + + request = make_mocked_request('/', 'GET') + + assert storage._generate_token.call_count == 0 + + await storage.save_token(request, None) + + assert storage._generate_token.call_count == 1 + + request2 = make_mocked_request('/', 'GET') + + request2['my_field'] = 1 + + await storage.save_token(request2, None) + + +async def test_3(): + class Some: + pass + + token_generator = Some() + + with pytest.raises(TypeError): + FakeStorage(token_generator=token_generator) diff --git a/tests/test_token_generator.py b/tests/test_token_generator.py new file mode 100644 index 0000000..e4ac6b0 --- /dev/null +++ b/tests/test_token_generator.py @@ -0,0 +1,37 @@ +import hashlib +import uuid +from unittest import mock + +import aiohttp_csrf + +COOKIE_NAME = 'csrf_token' +HEADER_NAME = 'X-CSRF-TOKEN' + + +def test_simple_token_generator(): + token_generator = aiohttp_csrf.token_generator.SimpleTokenGenerator() + + u = uuid.uuid4() + + with mock.patch('uuid.uuid4', return_value=u): + token = token_generator.generate() + + assert u.hex == token + + +def test_hashed_token_generator(): + encoding = aiohttp_csrf.token_generator.HashedTokenGenerator.encoding + + token_generator = aiohttp_csrf.token_generator.HashedTokenGenerator( + 'secret', + ) + + u = uuid.uuid4() + token_string = u.hex + 'secret' + + hasher = hashlib.sha256(token_string.encode(encoding=encoding)) + + with mock.patch('hashlib.sha256', return_value=hasher): + token = token_generator.generate() + + assert token == hasher.hexdigest() diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..6e8ebe8 --- /dev/null +++ b/tox.ini @@ -0,0 +1,23 @@ +[tox] +envlist = + py3{5,6} +skip_missing_interpreters = True +skipsdist = True + +[testenv] +deps = + -r{toxinidir}/requirements_dev.txt +commands = + flake8 --show-source aiohttp_csrf + isort --check-only -rc aiohttp_csrf --diff + + flake8 --show-source demo + isort --check-only -rc demo --diff + + flake8 --show-source tests + isort --check-only -rc tests --diff + + flake8 --show-source setup.py + isort --check-only setup.py --diff + + pytest tests From aa44c5327e5b91f9bda559706d15d2ca377793b3 Mon Sep 17 00:00:00 2001 From: TensorTom Date: Wed, 4 Mar 2020 19:19:02 -0500 Subject: [PATCH 02/23] Bring project deps current, setup for dev. Pipenv added. --- Pipfile | 20 +++ Pipfile.lock | 409 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 429 insertions(+) create mode 100644 Pipfile create mode 100644 Pipfile.lock diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..620e1db --- /dev/null +++ b/Pipfile @@ -0,0 +1,20 @@ +[[source]] +name = "pypi" +url = "https://pypi.org/simple" +verify_ssl = true + +[dev-packages] +aiohttp-csrf = {editable = true,path = "."} + +[packages] +aiohttp = "==3.6.2" +aiohttp-session = "==2.9.0" +flake8 = "==3.7.9" +isort = "==4.3.21" +pytest = "==5.3.5" +pytest-aiohttp = "==0.3.0" +pytest-cov = "==2.8.1" +tox = "==3.14.5" + +[requires] +python_version = "3.8" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..3398070 --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,409 @@ +{ + "_meta": { + "hash": { + "sha256": "1b80dce2e4c89c4cd680ea5be8b6dcb4eb6239bb32d395cc25655ad50cc1539f" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.8" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "aiohttp": { + "hashes": [ + "sha256:1e984191d1ec186881ffaed4581092ba04f7c61582a177b187d3a2f07ed9719e", + "sha256:259ab809ff0727d0e834ac5e8a283dc5e3e0ecc30c4d80b3cd17a4139ce1f326", + "sha256:2f4d1a4fdce595c947162333353d4a44952a724fba9ca3205a3df99a33d1307a", + "sha256:32e5f3b7e511aa850829fbe5aa32eb455e5534eaa4b1ce93231d00e2f76e5654", + "sha256:344c780466b73095a72c616fac5ea9c4665add7fc129f285fbdbca3cccf4612a", + "sha256:460bd4237d2dbecc3b5ed57e122992f60188afe46e7319116da5eb8a9dfedba4", + "sha256:4c6efd824d44ae697814a2a85604d8e992b875462c6655da161ff18fd4f29f17", + "sha256:50aaad128e6ac62e7bf7bd1f0c0a24bc968a0c0590a726d5a955af193544bcec", + "sha256:6206a135d072f88da3e71cc501c59d5abffa9d0bb43269a6dcd28d66bfafdbdd", + "sha256:65f31b622af739a802ca6fd1a3076fd0ae523f8485c52924a89561ba10c49b48", + "sha256:ae55bac364c405caa23a4f2d6cfecc6a0daada500274ffca4a9230e7129eac59", + "sha256:b778ce0c909a2653741cb4b1ac7015b5c130ab9c897611df43ae6a58523cb965" + ], + "index": "pypi", + "version": "==3.6.2" + }, + "aiohttp-session": { + "hashes": [ + "sha256:74853d1177541cccfefb436409f9ea5d67a62f84e13946a3e115a765d9a0349c", + "sha256:959413468b84e30e7ca09719617cfb0000066a2e0f6c20062d043433e82aeb74" + ], + "index": "pypi", + "version": "==2.9.0" + }, + "appdirs": { + "hashes": [ + "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92", + "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e" + ], + "version": "==1.4.3" + }, + "async-timeout": { + "hashes": [ + "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", + "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3" + ], + "version": "==3.0.1" + }, + "attrs": { + "hashes": [ + "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", + "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" + ], + "version": "==19.3.0" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "coverage": { + "hashes": [ + "sha256:15cf13a6896048d6d947bf7d222f36e4809ab926894beb748fc9caa14605d9c3", + "sha256:1daa3eceed220f9fdb80d5ff950dd95112cd27f70d004c7918ca6dfc6c47054c", + "sha256:1e44a022500d944d42f94df76727ba3fc0a5c0b672c358b61067abb88caee7a0", + "sha256:25dbf1110d70bab68a74b4b9d74f30e99b177cde3388e07cc7272f2168bd1477", + "sha256:3230d1003eec018ad4a472d254991e34241e0bbd513e97a29727c7c2f637bd2a", + "sha256:3dbb72eaeea5763676a1a1efd9b427a048c97c39ed92e13336e726117d0b72bf", + "sha256:5012d3b8d5a500834783689a5d2292fe06ec75dc86ee1ccdad04b6f5bf231691", + "sha256:51bc7710b13a2ae0c726f69756cf7ffd4362f4ac36546e243136187cfcc8aa73", + "sha256:527b4f316e6bf7755082a783726da20671a0cc388b786a64417780b90565b987", + "sha256:722e4557c8039aad9592c6a4213db75da08c2cd9945320220634f637251c3894", + "sha256:76e2057e8ffba5472fd28a3a010431fd9e928885ff480cb278877c6e9943cc2e", + "sha256:77afca04240c40450c331fa796b3eab6f1e15c5ecf8bf2b8bee9706cd5452fef", + "sha256:7afad9835e7a651d3551eab18cbc0fdb888f0a6136169fbef0662d9cdc9987cf", + "sha256:9bea19ac2f08672636350f203db89382121c9c2ade85d945953ef3c8cf9d2a68", + "sha256:a8b8ac7876bc3598e43e2603f772d2353d9931709345ad6c1149009fd1bc81b8", + "sha256:b0840b45187699affd4c6588286d429cd79a99d509fe3de0f209594669bb0954", + "sha256:b26aaf69713e5674efbde4d728fb7124e429c9466aeaf5f4a7e9e699b12c9fe2", + "sha256:b63dd43f455ba878e5e9f80ba4f748c0a2156dde6e0e6e690310e24d6e8caf40", + "sha256:be18f4ae5a9e46edae3f329de2191747966a34a3d93046dbdf897319923923bc", + "sha256:c312e57847db2526bc92b9bfa78266bfbaabac3fdcd751df4d062cd4c23e46dc", + "sha256:c60097190fe9dc2b329a0eb03393e2e0829156a589bd732e70794c0dd804258e", + "sha256:c62a2143e1313944bf4a5ab34fd3b4be15367a02e9478b0ce800cb510e3bbb9d", + "sha256:cc1109f54a14d940b8512ee9f1c3975c181bbb200306c6d8b87d93376538782f", + "sha256:cd60f507c125ac0ad83f05803063bed27e50fa903b9c2cfee3f8a6867ca600fc", + "sha256:d513cc3db248e566e07a0da99c230aca3556d9b09ed02f420664e2da97eac301", + "sha256:d649dc0bcace6fcdb446ae02b98798a856593b19b637c1b9af8edadf2b150bea", + "sha256:d7008a6796095a79544f4da1ee49418901961c97ca9e9d44904205ff7d6aa8cb", + "sha256:da93027835164b8223e8e5af2cf902a4c80ed93cb0909417234f4a9df3bcd9af", + "sha256:e69215621707119c6baf99bda014a45b999d37602cb7043d943c76a59b05bf52", + "sha256:ea9525e0fef2de9208250d6c5aeeee0138921057cd67fcef90fbed49c4d62d37", + "sha256:fca1669d464f0c9831fd10be2eef6b86f5ebd76c724d1e0706ebdff86bb4adf0" + ], + "version": "==5.0.3" + }, + "distlib": { + "hashes": [ + "sha256:2e166e231a26b36d6dfe35a48c4464346620f8645ed0ace01ee31822b288de21" + ], + "version": "==0.3.0" + }, + "entrypoints": { + "hashes": [ + "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19", + "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451" + ], + "version": "==0.3" + }, + "filelock": { + "hashes": [ + "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59", + "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836" + ], + "version": "==3.0.12" + }, + "flake8": { + "hashes": [ + "sha256:45681a117ecc81e870cbf1262835ae4af5e7a8b08e40b944a8a6e6b895914cfb", + "sha256:49356e766643ad15072a789a20915d3c91dc89fd313ccd71802303fd67e4deca" + ], + "index": "pypi", + "version": "==3.7.9" + }, + "idna": { + "hashes": [ + "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb", + "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa" + ], + "version": "==2.9" + }, + "isort": { + "hashes": [ + "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1", + "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd" + ], + "index": "pypi", + "version": "==4.3.21" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "more-itertools": { + "hashes": [ + "sha256:5dd8bcf33e5f9513ffa06d5ad33d78f31e1931ac9a18f33d37e77a180d393a7c", + "sha256:b1ddb932186d8a6ac451e1d95844b382f55e12686d51ca0c68b6f61f2ab7a507" + ], + "version": "==8.2.0" + }, + "multidict": { + "hashes": [ + "sha256:317f96bc0950d249e96d8d29ab556d01dd38888fbe68324f46fd834b430169f1", + "sha256:42f56542166040b4474c0c608ed051732033cd821126493cf25b6c276df7dd35", + "sha256:4b7df040fb5fe826d689204f9b544af469593fb3ff3a069a6ad3409f742f5928", + "sha256:544fae9261232a97102e27a926019100a9db75bec7b37feedd74b3aa82f29969", + "sha256:620b37c3fea181dab09267cd5a84b0f23fa043beb8bc50d8474dd9694de1fa6e", + "sha256:6e6fef114741c4d7ca46da8449038ec8b1e880bbe68674c01ceeb1ac8a648e78", + "sha256:7774e9f6c9af3f12f296131453f7b81dabb7ebdb948483362f5afcaac8a826f1", + "sha256:85cb26c38c96f76b7ff38b86c9d560dea10cf3459bb5f4caf72fc1bb932c7136", + "sha256:a326f4240123a2ac66bb163eeba99578e9d63a8654a59f4688a79198f9aa10f8", + "sha256:ae402f43604e3b2bc41e8ea8b8526c7fa7139ed76b0d64fc48e28125925275b2", + "sha256:aee283c49601fa4c13adc64c09c978838a7e812f85377ae130a24d7198c0331e", + "sha256:b51249fdd2923739cd3efc95a3d6c363b67bbf779208e9f37fd5e68540d1a4d4", + "sha256:bb519becc46275c594410c6c28a8a0adc66fe24fef154a9addea54c1adb006f5", + "sha256:c2c37185fb0af79d5c117b8d2764f4321eeb12ba8c141a95d0aa8c2c1d0a11dd", + "sha256:dc561313279f9d05a3d0ffa89cd15ae477528ea37aa9795c4654588a3287a9ab", + "sha256:e439c9a10a95cb32abd708bb8be83b2134fa93790a4fb0535ca36db3dda94d20", + "sha256:fc3b4adc2ee8474cb3cd2a155305d5f8eda0a9c91320f83e55748e1fcb68f8e3" + ], + "version": "==4.7.5" + }, + "packaging": { + "hashes": [ + "sha256:170748228214b70b672c581a3dd610ee51f733018650740e98c7df862a583f73", + "sha256:e665345f9eef0c621aa0bf2f8d78cf6d21904eef16a93f020240b704a57f1334" + ], + "version": "==20.1" + }, + "pluggy": { + "hashes": [ + "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", + "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" + ], + "version": "==0.13.1" + }, + "py": { + "hashes": [ + "sha256:5e27081401262157467ad6e7f851b7aa402c5852dbcb3dae06768434de5752aa", + "sha256:c20fdd83a5dbc0af9efd622bee9a5564e278f6380fffcacc43ba6f43db2813b0" + ], + "version": "==1.8.1" + }, + "pycodestyle": { + "hashes": [ + "sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56", + "sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c" + ], + "version": "==2.5.0" + }, + "pyflakes": { + "hashes": [ + "sha256:17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0", + "sha256:d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2" + ], + "version": "==2.1.1" + }, + "pyparsing": { + "hashes": [ + "sha256:4c830582a84fb022400b85429791bc551f1f4871c33f23e44f353119e92f969f", + "sha256:c342dccb5250c08d45fd6f8b4a559613ca603b57498511740e65cd11a2e7dcec" + ], + "version": "==2.4.6" + }, + "pytest": { + "hashes": [ + "sha256:0d5fe9189a148acc3c3eb2ac8e1ac0742cb7618c084f3d228baaec0c254b318d", + "sha256:ff615c761e25eb25df19edddc0b970302d2a9091fbce0e7213298d85fb61fef6" + ], + "index": "pypi", + "version": "==5.3.5" + }, + "pytest-aiohttp": { + "hashes": [ + "sha256:0b9b660b146a65e1313e2083d0d2e1f63047797354af9a28d6b7c9f0726fa33d", + "sha256:c929854339637977375838703b62fef63528598bc0a9d451639eba95f4aaa44f" + ], + "index": "pypi", + "version": "==0.3.0" + }, + "pytest-cov": { + "hashes": [ + "sha256:cc6742d8bac45070217169f5f72ceee1e0e55b0221f54bcf24845972d3a47f2b", + "sha256:cdbdef4f870408ebdbfeb44e63e07eb18bb4619fae852f6e760645fa36172626" + ], + "index": "pypi", + "version": "==2.8.1" + }, + "six": { + "hashes": [ + "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a", + "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c" + ], + "version": "==1.14.0" + }, + "toml": { + "hashes": [ + "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", + "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e" + ], + "version": "==0.10.0" + }, + "tox": { + "hashes": [ + "sha256:0cbe98369081fa16bd6f1163d3d0b2a62afa29d402ccfad2bd09fb2668be0956", + "sha256:676f1e3e7de245ad870f956436b84ea226210587d1f72c8dfb8cd5ac7b6f0e70" + ], + "index": "pypi", + "version": "==3.14.5" + }, + "virtualenv": { + "hashes": [ + "sha256:0c04c7e8e0314470b4c2b43740ff68be1c62bb3fdef8309341ff1daea60d49d1", + "sha256:1f0369d068d9761b5c1ed7b44dad1ec124727eb10bc7f4aaefbba0cdca3bd924" + ], + "version": "==20.0.8" + }, + "wcwidth": { + "hashes": [ + "sha256:8fd29383f539be45b20bd4df0dc29c20ba48654a41e661925e612311e9f3c603", + "sha256:f28b3e8a6483e5d49e7f8949ac1a78314e740333ae305b4ba5defd3e74fb37a8" + ], + "version": "==0.1.8" + }, + "yarl": { + "hashes": [ + "sha256:0c2ab325d33f1b824734b3ef51d4d54a54e0e7a23d13b86974507602334c2cce", + "sha256:0ca2f395591bbd85ddd50a82eb1fde9c1066fafe888c5c7cc1d810cf03fd3cc6", + "sha256:2098a4b4b9d75ee352807a95cdf5f10180db903bc5b7270715c6bbe2551f64ce", + "sha256:25e66e5e2007c7a39541ca13b559cd8ebc2ad8fe00ea94a2aad28a9b1e44e5ae", + "sha256:26d7c90cb04dee1665282a5d1a998defc1a9e012fdca0f33396f81508f49696d", + "sha256:308b98b0c8cd1dfef1a0311dc5e38ae8f9b58349226aa0533f15a16717ad702f", + "sha256:3ce3d4f7c6b69c4e4f0704b32eca8123b9c58ae91af740481aa57d7857b5e41b", + "sha256:58cd9c469eced558cd81aa3f484b2924e8897049e06889e8ff2510435b7ef74b", + "sha256:5b10eb0e7f044cf0b035112446b26a3a2946bca9d7d7edb5e54a2ad2f6652abb", + "sha256:6faa19d3824c21bcbfdfce5171e193c8b4ddafdf0ac3f129ccf0cdfcb083e462", + "sha256:944494be42fa630134bf907714d40207e646fd5a94423c90d5b514f7b0713fea", + "sha256:a161de7e50224e8e3de6e184707476b5a989037dcb24292b391a3d66ff158e70", + "sha256:a4844ebb2be14768f7994f2017f70aca39d658a96c786211be5ddbe1c68794c1", + "sha256:c2b509ac3d4b988ae8769901c66345425e361d518aecbe4acbfc2567e416626a", + "sha256:c9959d49a77b0e07559e579f38b2f3711c2b8716b8410b320bf9713013215a1b", + "sha256:d8cdee92bc930d8b09d8bd2043cedd544d9c8bd7436a77678dd602467a993080", + "sha256:e15199cdb423316e15f108f51249e44eb156ae5dba232cb73be555324a1d49c2" + ], + "version": "==1.4.2" + } + }, + "develop": { + "aiohttp": { + "hashes": [ + "sha256:1e984191d1ec186881ffaed4581092ba04f7c61582a177b187d3a2f07ed9719e", + "sha256:259ab809ff0727d0e834ac5e8a283dc5e3e0ecc30c4d80b3cd17a4139ce1f326", + "sha256:2f4d1a4fdce595c947162333353d4a44952a724fba9ca3205a3df99a33d1307a", + "sha256:32e5f3b7e511aa850829fbe5aa32eb455e5534eaa4b1ce93231d00e2f76e5654", + "sha256:344c780466b73095a72c616fac5ea9c4665add7fc129f285fbdbca3cccf4612a", + "sha256:460bd4237d2dbecc3b5ed57e122992f60188afe46e7319116da5eb8a9dfedba4", + "sha256:4c6efd824d44ae697814a2a85604d8e992b875462c6655da161ff18fd4f29f17", + "sha256:50aaad128e6ac62e7bf7bd1f0c0a24bc968a0c0590a726d5a955af193544bcec", + "sha256:6206a135d072f88da3e71cc501c59d5abffa9d0bb43269a6dcd28d66bfafdbdd", + "sha256:65f31b622af739a802ca6fd1a3076fd0ae523f8485c52924a89561ba10c49b48", + "sha256:ae55bac364c405caa23a4f2d6cfecc6a0daada500274ffca4a9230e7129eac59", + "sha256:b778ce0c909a2653741cb4b1ac7015b5c130ab9c897611df43ae6a58523cb965" + ], + "index": "pypi", + "version": "==3.6.2" + }, + "aiohttp-csrf": { + "editable": true, + "path": "." + }, + "async-timeout": { + "hashes": [ + "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", + "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3" + ], + "version": "==3.0.1" + }, + "attrs": { + "hashes": [ + "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", + "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" + ], + "version": "==19.3.0" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "idna": { + "hashes": [ + "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb", + "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa" + ], + "version": "==2.9" + }, + "multidict": { + "hashes": [ + "sha256:317f96bc0950d249e96d8d29ab556d01dd38888fbe68324f46fd834b430169f1", + "sha256:42f56542166040b4474c0c608ed051732033cd821126493cf25b6c276df7dd35", + "sha256:4b7df040fb5fe826d689204f9b544af469593fb3ff3a069a6ad3409f742f5928", + "sha256:544fae9261232a97102e27a926019100a9db75bec7b37feedd74b3aa82f29969", + "sha256:620b37c3fea181dab09267cd5a84b0f23fa043beb8bc50d8474dd9694de1fa6e", + "sha256:6e6fef114741c4d7ca46da8449038ec8b1e880bbe68674c01ceeb1ac8a648e78", + "sha256:7774e9f6c9af3f12f296131453f7b81dabb7ebdb948483362f5afcaac8a826f1", + "sha256:85cb26c38c96f76b7ff38b86c9d560dea10cf3459bb5f4caf72fc1bb932c7136", + "sha256:a326f4240123a2ac66bb163eeba99578e9d63a8654a59f4688a79198f9aa10f8", + "sha256:ae402f43604e3b2bc41e8ea8b8526c7fa7139ed76b0d64fc48e28125925275b2", + "sha256:aee283c49601fa4c13adc64c09c978838a7e812f85377ae130a24d7198c0331e", + "sha256:b51249fdd2923739cd3efc95a3d6c363b67bbf779208e9f37fd5e68540d1a4d4", + "sha256:bb519becc46275c594410c6c28a8a0adc66fe24fef154a9addea54c1adb006f5", + "sha256:c2c37185fb0af79d5c117b8d2764f4321eeb12ba8c141a95d0aa8c2c1d0a11dd", + "sha256:dc561313279f9d05a3d0ffa89cd15ae477528ea37aa9795c4654588a3287a9ab", + "sha256:e439c9a10a95cb32abd708bb8be83b2134fa93790a4fb0535ca36db3dda94d20", + "sha256:fc3b4adc2ee8474cb3cd2a155305d5f8eda0a9c91320f83e55748e1fcb68f8e3" + ], + "version": "==4.7.5" + }, + "yarl": { + "hashes": [ + "sha256:0c2ab325d33f1b824734b3ef51d4d54a54e0e7a23d13b86974507602334c2cce", + "sha256:0ca2f395591bbd85ddd50a82eb1fde9c1066fafe888c5c7cc1d810cf03fd3cc6", + "sha256:2098a4b4b9d75ee352807a95cdf5f10180db903bc5b7270715c6bbe2551f64ce", + "sha256:25e66e5e2007c7a39541ca13b559cd8ebc2ad8fe00ea94a2aad28a9b1e44e5ae", + "sha256:26d7c90cb04dee1665282a5d1a998defc1a9e012fdca0f33396f81508f49696d", + "sha256:308b98b0c8cd1dfef1a0311dc5e38ae8f9b58349226aa0533f15a16717ad702f", + "sha256:3ce3d4f7c6b69c4e4f0704b32eca8123b9c58ae91af740481aa57d7857b5e41b", + "sha256:58cd9c469eced558cd81aa3f484b2924e8897049e06889e8ff2510435b7ef74b", + "sha256:5b10eb0e7f044cf0b035112446b26a3a2946bca9d7d7edb5e54a2ad2f6652abb", + "sha256:6faa19d3824c21bcbfdfce5171e193c8b4ddafdf0ac3f129ccf0cdfcb083e462", + "sha256:944494be42fa630134bf907714d40207e646fd5a94423c90d5b514f7b0713fea", + "sha256:a161de7e50224e8e3de6e184707476b5a989037dcb24292b391a3d66ff158e70", + "sha256:a4844ebb2be14768f7994f2017f70aca39d658a96c786211be5ddbe1c68794c1", + "sha256:c2b509ac3d4b988ae8769901c66345425e361d518aecbe4acbfc2567e416626a", + "sha256:c9959d49a77b0e07559e579f38b2f3711c2b8716b8410b320bf9713013215a1b", + "sha256:d8cdee92bc930d8b09d8bd2043cedd544d9c8bd7436a77678dd602467a993080", + "sha256:e15199cdb423316e15f108f51249e44eb156ae5dba232cb73be555324a1d49c2" + ], + "version": "==1.4.2" + } + } +} From 8859f0d40f700c93f666c0d4fd966ab83442c6ed Mon Sep 17 00:00:00 2001 From: TensorTom Date: Wed, 4 Mar 2020 19:32:06 -0500 Subject: [PATCH 03/23] Defined dev-packages. --- Pipfile | 12 ++-- Pipfile.lock | 194 +++++++++++++++++++++++++-------------------------- 2 files changed, 103 insertions(+), 103 deletions(-) diff --git a/Pipfile b/Pipfile index 620e1db..f901de5 100644 --- a/Pipfile +++ b/Pipfile @@ -5,16 +5,16 @@ verify_ssl = true [dev-packages] aiohttp-csrf = {editable = true,path = "."} - -[packages] -aiohttp = "==3.6.2" -aiohttp-session = "==2.9.0" -flake8 = "==3.7.9" -isort = "==4.3.21" pytest = "==5.3.5" pytest-aiohttp = "==0.3.0" pytest-cov = "==2.8.1" tox = "==3.14.5" +flake8 = "==3.7.9" +isort = "==4.3.21" + +[packages] +aiohttp = "==3.6.2" +aiohttp-session = "==2.9.0" [requires] python_version = "3.8" diff --git a/Pipfile.lock b/Pipfile.lock index 3398070..094f22e 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "1b80dce2e4c89c4cd680ea5be8b6dcb4eb6239bb32d395cc25655ad50cc1539f" + "sha256": "98bef3cc25d988088ce98b184ba121f19af4f5e089817cdd0b5e6aa3f2a06e8a" }, "pipfile-spec": 6, "requires": { @@ -42,6 +42,102 @@ "index": "pypi", "version": "==2.9.0" }, + "async-timeout": { + "hashes": [ + "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", + "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3" + ], + "version": "==3.0.1" + }, + "attrs": { + "hashes": [ + "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", + "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" + ], + "version": "==19.3.0" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "idna": { + "hashes": [ + "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb", + "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa" + ], + "version": "==2.9" + }, + "multidict": { + "hashes": [ + "sha256:317f96bc0950d249e96d8d29ab556d01dd38888fbe68324f46fd834b430169f1", + "sha256:42f56542166040b4474c0c608ed051732033cd821126493cf25b6c276df7dd35", + "sha256:4b7df040fb5fe826d689204f9b544af469593fb3ff3a069a6ad3409f742f5928", + "sha256:544fae9261232a97102e27a926019100a9db75bec7b37feedd74b3aa82f29969", + "sha256:620b37c3fea181dab09267cd5a84b0f23fa043beb8bc50d8474dd9694de1fa6e", + "sha256:6e6fef114741c4d7ca46da8449038ec8b1e880bbe68674c01ceeb1ac8a648e78", + "sha256:7774e9f6c9af3f12f296131453f7b81dabb7ebdb948483362f5afcaac8a826f1", + "sha256:85cb26c38c96f76b7ff38b86c9d560dea10cf3459bb5f4caf72fc1bb932c7136", + "sha256:a326f4240123a2ac66bb163eeba99578e9d63a8654a59f4688a79198f9aa10f8", + "sha256:ae402f43604e3b2bc41e8ea8b8526c7fa7139ed76b0d64fc48e28125925275b2", + "sha256:aee283c49601fa4c13adc64c09c978838a7e812f85377ae130a24d7198c0331e", + "sha256:b51249fdd2923739cd3efc95a3d6c363b67bbf779208e9f37fd5e68540d1a4d4", + "sha256:bb519becc46275c594410c6c28a8a0adc66fe24fef154a9addea54c1adb006f5", + "sha256:c2c37185fb0af79d5c117b8d2764f4321eeb12ba8c141a95d0aa8c2c1d0a11dd", + "sha256:dc561313279f9d05a3d0ffa89cd15ae477528ea37aa9795c4654588a3287a9ab", + "sha256:e439c9a10a95cb32abd708bb8be83b2134fa93790a4fb0535ca36db3dda94d20", + "sha256:fc3b4adc2ee8474cb3cd2a155305d5f8eda0a9c91320f83e55748e1fcb68f8e3" + ], + "version": "==4.7.5" + }, + "yarl": { + "hashes": [ + "sha256:0c2ab325d33f1b824734b3ef51d4d54a54e0e7a23d13b86974507602334c2cce", + "sha256:0ca2f395591bbd85ddd50a82eb1fde9c1066fafe888c5c7cc1d810cf03fd3cc6", + "sha256:2098a4b4b9d75ee352807a95cdf5f10180db903bc5b7270715c6bbe2551f64ce", + "sha256:25e66e5e2007c7a39541ca13b559cd8ebc2ad8fe00ea94a2aad28a9b1e44e5ae", + "sha256:26d7c90cb04dee1665282a5d1a998defc1a9e012fdca0f33396f81508f49696d", + "sha256:308b98b0c8cd1dfef1a0311dc5e38ae8f9b58349226aa0533f15a16717ad702f", + "sha256:3ce3d4f7c6b69c4e4f0704b32eca8123b9c58ae91af740481aa57d7857b5e41b", + "sha256:58cd9c469eced558cd81aa3f484b2924e8897049e06889e8ff2510435b7ef74b", + "sha256:5b10eb0e7f044cf0b035112446b26a3a2946bca9d7d7edb5e54a2ad2f6652abb", + "sha256:6faa19d3824c21bcbfdfce5171e193c8b4ddafdf0ac3f129ccf0cdfcb083e462", + "sha256:944494be42fa630134bf907714d40207e646fd5a94423c90d5b514f7b0713fea", + "sha256:a161de7e50224e8e3de6e184707476b5a989037dcb24292b391a3d66ff158e70", + "sha256:a4844ebb2be14768f7994f2017f70aca39d658a96c786211be5ddbe1c68794c1", + "sha256:c2b509ac3d4b988ae8769901c66345425e361d518aecbe4acbfc2567e416626a", + "sha256:c9959d49a77b0e07559e579f38b2f3711c2b8716b8410b320bf9713013215a1b", + "sha256:d8cdee92bc930d8b09d8bd2043cedd544d9c8bd7436a77678dd602467a993080", + "sha256:e15199cdb423316e15f108f51249e44eb156ae5dba232cb73be555324a1d49c2" + ], + "version": "==1.4.2" + } + }, + "develop": { + "aiohttp": { + "hashes": [ + "sha256:1e984191d1ec186881ffaed4581092ba04f7c61582a177b187d3a2f07ed9719e", + "sha256:259ab809ff0727d0e834ac5e8a283dc5e3e0ecc30c4d80b3cd17a4139ce1f326", + "sha256:2f4d1a4fdce595c947162333353d4a44952a724fba9ca3205a3df99a33d1307a", + "sha256:32e5f3b7e511aa850829fbe5aa32eb455e5534eaa4b1ce93231d00e2f76e5654", + "sha256:344c780466b73095a72c616fac5ea9c4665add7fc129f285fbdbca3cccf4612a", + "sha256:460bd4237d2dbecc3b5ed57e122992f60188afe46e7319116da5eb8a9dfedba4", + "sha256:4c6efd824d44ae697814a2a85604d8e992b875462c6655da161ff18fd4f29f17", + "sha256:50aaad128e6ac62e7bf7bd1f0c0a24bc968a0c0590a726d5a955af193544bcec", + "sha256:6206a135d072f88da3e71cc501c59d5abffa9d0bb43269a6dcd28d66bfafdbdd", + "sha256:65f31b622af739a802ca6fd1a3076fd0ae523f8485c52924a89561ba10c49b48", + "sha256:ae55bac364c405caa23a4f2d6cfecc6a0daada500274ffca4a9230e7129eac59", + "sha256:b778ce0c909a2653741cb4b1ac7015b5c130ab9c897611df43ae6a58523cb965" + ], + "index": "pypi", + "version": "==3.6.2" + }, + "aiohttp-csrf": { + "editable": true, + "path": "." + }, "appdirs": { "hashes": [ "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92", @@ -309,101 +405,5 @@ ], "version": "==1.4.2" } - }, - "develop": { - "aiohttp": { - "hashes": [ - "sha256:1e984191d1ec186881ffaed4581092ba04f7c61582a177b187d3a2f07ed9719e", - "sha256:259ab809ff0727d0e834ac5e8a283dc5e3e0ecc30c4d80b3cd17a4139ce1f326", - "sha256:2f4d1a4fdce595c947162333353d4a44952a724fba9ca3205a3df99a33d1307a", - "sha256:32e5f3b7e511aa850829fbe5aa32eb455e5534eaa4b1ce93231d00e2f76e5654", - "sha256:344c780466b73095a72c616fac5ea9c4665add7fc129f285fbdbca3cccf4612a", - "sha256:460bd4237d2dbecc3b5ed57e122992f60188afe46e7319116da5eb8a9dfedba4", - "sha256:4c6efd824d44ae697814a2a85604d8e992b875462c6655da161ff18fd4f29f17", - "sha256:50aaad128e6ac62e7bf7bd1f0c0a24bc968a0c0590a726d5a955af193544bcec", - "sha256:6206a135d072f88da3e71cc501c59d5abffa9d0bb43269a6dcd28d66bfafdbdd", - "sha256:65f31b622af739a802ca6fd1a3076fd0ae523f8485c52924a89561ba10c49b48", - "sha256:ae55bac364c405caa23a4f2d6cfecc6a0daada500274ffca4a9230e7129eac59", - "sha256:b778ce0c909a2653741cb4b1ac7015b5c130ab9c897611df43ae6a58523cb965" - ], - "index": "pypi", - "version": "==3.6.2" - }, - "aiohttp-csrf": { - "editable": true, - "path": "." - }, - "async-timeout": { - "hashes": [ - "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", - "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3" - ], - "version": "==3.0.1" - }, - "attrs": { - "hashes": [ - "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", - "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" - ], - "version": "==19.3.0" - }, - "chardet": { - "hashes": [ - "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", - "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" - ], - "version": "==3.0.4" - }, - "idna": { - "hashes": [ - "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb", - "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa" - ], - "version": "==2.9" - }, - "multidict": { - "hashes": [ - "sha256:317f96bc0950d249e96d8d29ab556d01dd38888fbe68324f46fd834b430169f1", - "sha256:42f56542166040b4474c0c608ed051732033cd821126493cf25b6c276df7dd35", - "sha256:4b7df040fb5fe826d689204f9b544af469593fb3ff3a069a6ad3409f742f5928", - "sha256:544fae9261232a97102e27a926019100a9db75bec7b37feedd74b3aa82f29969", - "sha256:620b37c3fea181dab09267cd5a84b0f23fa043beb8bc50d8474dd9694de1fa6e", - "sha256:6e6fef114741c4d7ca46da8449038ec8b1e880bbe68674c01ceeb1ac8a648e78", - "sha256:7774e9f6c9af3f12f296131453f7b81dabb7ebdb948483362f5afcaac8a826f1", - "sha256:85cb26c38c96f76b7ff38b86c9d560dea10cf3459bb5f4caf72fc1bb932c7136", - "sha256:a326f4240123a2ac66bb163eeba99578e9d63a8654a59f4688a79198f9aa10f8", - "sha256:ae402f43604e3b2bc41e8ea8b8526c7fa7139ed76b0d64fc48e28125925275b2", - "sha256:aee283c49601fa4c13adc64c09c978838a7e812f85377ae130a24d7198c0331e", - "sha256:b51249fdd2923739cd3efc95a3d6c363b67bbf779208e9f37fd5e68540d1a4d4", - "sha256:bb519becc46275c594410c6c28a8a0adc66fe24fef154a9addea54c1adb006f5", - "sha256:c2c37185fb0af79d5c117b8d2764f4321eeb12ba8c141a95d0aa8c2c1d0a11dd", - "sha256:dc561313279f9d05a3d0ffa89cd15ae477528ea37aa9795c4654588a3287a9ab", - "sha256:e439c9a10a95cb32abd708bb8be83b2134fa93790a4fb0535ca36db3dda94d20", - "sha256:fc3b4adc2ee8474cb3cd2a155305d5f8eda0a9c91320f83e55748e1fcb68f8e3" - ], - "version": "==4.7.5" - }, - "yarl": { - "hashes": [ - "sha256:0c2ab325d33f1b824734b3ef51d4d54a54e0e7a23d13b86974507602334c2cce", - "sha256:0ca2f395591bbd85ddd50a82eb1fde9c1066fafe888c5c7cc1d810cf03fd3cc6", - "sha256:2098a4b4b9d75ee352807a95cdf5f10180db903bc5b7270715c6bbe2551f64ce", - "sha256:25e66e5e2007c7a39541ca13b559cd8ebc2ad8fe00ea94a2aad28a9b1e44e5ae", - "sha256:26d7c90cb04dee1665282a5d1a998defc1a9e012fdca0f33396f81508f49696d", - "sha256:308b98b0c8cd1dfef1a0311dc5e38ae8f9b58349226aa0533f15a16717ad702f", - "sha256:3ce3d4f7c6b69c4e4f0704b32eca8123b9c58ae91af740481aa57d7857b5e41b", - "sha256:58cd9c469eced558cd81aa3f484b2924e8897049e06889e8ff2510435b7ef74b", - "sha256:5b10eb0e7f044cf0b035112446b26a3a2946bca9d7d7edb5e54a2ad2f6652abb", - "sha256:6faa19d3824c21bcbfdfce5171e193c8b4ddafdf0ac3f129ccf0cdfcb083e462", - "sha256:944494be42fa630134bf907714d40207e646fd5a94423c90d5b514f7b0713fea", - "sha256:a161de7e50224e8e3de6e184707476b5a989037dcb24292b391a3d66ff158e70", - "sha256:a4844ebb2be14768f7994f2017f70aca39d658a96c786211be5ddbe1c68794c1", - "sha256:c2b509ac3d4b988ae8769901c66345425e361d518aecbe4acbfc2567e416626a", - "sha256:c9959d49a77b0e07559e579f38b2f3711c2b8716b8410b320bf9713013215a1b", - "sha256:d8cdee92bc930d8b09d8bd2043cedd544d9c8bd7436a77678dd602467a993080", - "sha256:e15199cdb423316e15f108f51249e44eb156ae5dba232cb73be555324a1d49c2" - ], - "version": "==1.4.2" - } } } From ee0389c9231291fdc395dda2a504106210746fd2 Mon Sep 17 00:00:00 2001 From: TensorTom Date: Wed, 4 Mar 2020 20:55:13 -0500 Subject: [PATCH 04/23] Add missing ipdb dev dep. --- Pipfile | 1 + Pipfile.lock | 94 +++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 94 insertions(+), 1 deletion(-) diff --git a/Pipfile b/Pipfile index f901de5..eb9f1fd 100644 --- a/Pipfile +++ b/Pipfile @@ -11,6 +11,7 @@ pytest-cov = "==2.8.1" tox = "==3.14.5" flake8 = "==3.7.9" isort = "==4.3.21" +ipdb = "*" [packages] aiohttp = "==3.6.2" diff --git a/Pipfile.lock b/Pipfile.lock index 094f22e..39f6cbf 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "98bef3cc25d988088ce98b184ba121f19af4f5e089817cdd0b5e6aa3f2a06e8a" + "sha256": "e092f3bbe7824881c5a4642836e64d8dec79ed9e7619304a65aa340ff18e1be0" }, "pipfile-spec": 6, "requires": { @@ -159,6 +159,13 @@ ], "version": "==19.3.0" }, + "backcall": { + "hashes": [ + "sha256:38ecd85be2c1e78f77fd91700c76e14667dc21e2713b63876c0eb901196e01e4", + "sha256:bbbf4b1e5cd2bdb08f915895b51081c041bac22394fdfcfdfbe9f14b77c08bf2" + ], + "version": "==0.1.0" + }, "chardet": { "hashes": [ "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", @@ -202,6 +209,13 @@ ], "version": "==5.0.3" }, + "decorator": { + "hashes": [ + "sha256:41fa54c2a0cc4ba648be4fd43cff00aedf5b9465c9bf18d64325bc225f08f760", + "sha256:e3a62f0520172440ca0dcc823749319382e377f37f140a0b99ef45fecb84bfe7" + ], + "version": "==4.4.2" + }, "distlib": { "hashes": [ "sha256:2e166e231a26b36d6dfe35a48c4464346620f8645ed0ace01ee31822b288de21" @@ -237,6 +251,27 @@ ], "version": "==2.9" }, + "ipdb": { + "hashes": [ + "sha256:77fb1c2a6fccdfee0136078c9ed6fe547ab00db00bebff181f1e8c9e13418d49" + ], + "index": "pypi", + "version": "==0.13.2" + }, + "ipython": { + "hashes": [ + "sha256:ca478e52ae1f88da0102360e57e528b92f3ae4316aabac80a2cd7f7ab2efb48a", + "sha256:eb8d075de37f678424527b5ef6ea23f7b80240ca031c2dd6de5879d687a65333" + ], + "version": "==7.13.0" + }, + "ipython-genutils": { + "hashes": [ + "sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8", + "sha256:eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8" + ], + "version": "==0.2.0" + }, "isort": { "hashes": [ "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1", @@ -245,6 +280,13 @@ "index": "pypi", "version": "==4.3.21" }, + "jedi": { + "hashes": [ + "sha256:b4f4052551025c6b0b0b193b29a6ff7bdb74c52450631206c262aef9f7159ad2", + "sha256:d5c871cb9360b414f981e7072c52c33258d598305280fef91c6cae34739d65d5" + ], + "version": "==0.16.0" + }, "mccabe": { "hashes": [ "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", @@ -288,6 +330,28 @@ ], "version": "==20.1" }, + "parso": { + "hashes": [ + "sha256:0c5659e0c6eba20636f99a04f469798dca8da279645ce5c387315b2c23912157", + "sha256:8515fc12cfca6ee3aa59138741fc5624d62340c97e401c74875769948d4f2995" + ], + "version": "==0.6.2" + }, + "pexpect": { + "hashes": [ + "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937", + "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c" + ], + "markers": "sys_platform != 'win32'", + "version": "==4.8.0" + }, + "pickleshare": { + "hashes": [ + "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca", + "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56" + ], + "version": "==0.7.5" + }, "pluggy": { "hashes": [ "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", @@ -295,6 +359,20 @@ ], "version": "==0.13.1" }, + "prompt-toolkit": { + "hashes": [ + "sha256:a402e9bf468b63314e37460b68ba68243d55b2f8c4d0192f85a019af3945050e", + "sha256:c93e53af97f630f12f5f62a3274e79527936ed466f038953dfa379d4941f651a" + ], + "version": "==3.0.3" + }, + "ptyprocess": { + "hashes": [ + "sha256:923f299cc5ad920c68f2bc0bc98b75b9f838b93b599941a6b63ddbc2476394c0", + "sha256:d7cc528d76e76342423ca640335bd3633420dc1366f258cb31d05e865ef5ca1f" + ], + "version": "==0.6.0" + }, "py": { "hashes": [ "sha256:5e27081401262157467ad6e7f851b7aa402c5852dbcb3dae06768434de5752aa", @@ -316,6 +394,13 @@ ], "version": "==2.1.1" }, + "pygments": { + "hashes": [ + "sha256:2a3fe295e54a20164a9df49c75fa58526d3be48e14aceba6d6b1e8ac0bfd6f1b", + "sha256:98c8aa5a9f778fcd1026a17361ddaf7330d1b7c62ae97c3bb0ae73e0b9b6b0fe" + ], + "version": "==2.5.2" + }, "pyparsing": { "hashes": [ "sha256:4c830582a84fb022400b85429791bc551f1f4871c33f23e44f353119e92f969f", @@ -369,6 +454,13 @@ "index": "pypi", "version": "==3.14.5" }, + "traitlets": { + "hashes": [ + "sha256:70b4c6a1d9019d7b4f6846832288f86998aa3b9207c6821f3578a6a6a467fe44", + "sha256:d023ee369ddd2763310e4c3eae1ff649689440d4ae59d7485eb4cfbbe3e359f7" + ], + "version": "==4.3.3" + }, "virtualenv": { "hashes": [ "sha256:0c04c7e8e0314470b4c2b43740ff68be1c62bb3fdef8309341ff1daea60d49d1", From 00b29dab8cc5446299b5dd4f577960fe11d06fac Mon Sep 17 00:00:00 2001 From: TensorTom Date: Wed, 4 Mar 2020 20:59:00 -0500 Subject: [PATCH 05/23] Disabled ipdb since I've never used it and I want to get going here. --- test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test.py b/test.py index beea3a1..ad31687 100644 --- a/test.py +++ b/test.py @@ -9,7 +9,7 @@ async def hello(request): def dec(handler): def wrapped(*args, **kwargs): request = args[-1] - import ipdb;ipdb.set_trace() + #import ipdb;ipdb.set_trace() return handler(*args, **kwargs) return wrapped From 19a662afe2eb0e482360635d120f8ee7724b5f25 Mon Sep 17 00:00:00 2001 From: TensorTom Date: Wed, 4 Mar 2020 21:06:21 -0500 Subject: [PATCH 06/23] Minor correction to import. --- test.py | 1 + tests/test_form_policy.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/test.py b/test.py index ad31687..843d442 100644 --- a/test.py +++ b/test.py @@ -2,6 +2,7 @@ from aiohttp import web + async def hello(request): return web.Response(text="Hello, world") diff --git a/tests/test_form_policy.py b/tests/test_form_policy.py index 11a9689..2cfc99b 100644 --- a/tests/test_form_policy.py +++ b/tests/test_form_policy.py @@ -8,7 +8,7 @@ from aiohttp_session import setup as setup_session from aiohttp_session import SimpleCookieStorage -from .conftest import FORM_FIELD_NAME +from conftest import FORM_FIELD_NAME FORM_FIELD_REGEX = re.compile( r'', From 3cc85fd7079e5491d2a691d5483acca4b32a3c68 Mon Sep 17 00:00:00 2001 From: TensorTom Date: Wed, 4 Mar 2020 21:24:54 -0500 Subject: [PATCH 07/23] Matching tokens using == operator is vulnerable to timing attacks. Using compare_digest() instead. --- aiohttp_csrf/policy.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/aiohttp_csrf/policy.py b/aiohttp_csrf/policy.py index 1700e1d..2196dca 100644 --- a/aiohttp_csrf/policy.py +++ b/aiohttp_csrf/policy.py @@ -1,4 +1,5 @@ import abc +from secrets import compare_digest class AbstractPolicy(metaclass=abc.ABCMeta): @@ -17,7 +18,7 @@ async def check(self, request, original_value): token = post.get(self.field_name) - return token == original_value + return compare_digest(token, original_value) class HeaderPolicy(AbstractPolicy): @@ -28,7 +29,7 @@ def __init__(self, header_name): async def check(self, request, original_value): token = request.headers.get(self.header_name) - return token == original_value + return compare_digest(token, original_value) class FormAndHeaderPolicy(HeaderPolicy, FormPolicy): From a323137eb037c0b32b6e651714363e79fe6a973f Mon Sep 17 00:00:00 2001 From: TensorTom Date: Wed, 4 Mar 2020 22:04:27 -0500 Subject: [PATCH 08/23] Added support for CSRF in GET, compatible with multipart. Updated README to reflect. --- README.md | 252 +++++++++++++++++++++++++++++++++++++++ README.rst | 261 ----------------------------------------- aiohttp_csrf/policy.py | 4 +- demo/middleware.py | 36 +++++- 4 files changed, 285 insertions(+), 268 deletions(-) create mode 100644 README.md delete mode 100644 README.rst diff --git a/README.md b/README.md new file mode 100644 index 0000000..745c082 --- /dev/null +++ b/README.md @@ -0,0 +1,252 @@ +aiohttp\_csrf +============= + +The library provides csrf (xsrf) protection for +[aiohttp.web](aiohttp_web_). + +Basic usage +----------- + +The library allows you to implement csrf (xsrf) protection for requests + +Basic usage example: + +``` {.sourceCode .python} +import aiohttp_csrf +from aiohttp import web + +FORM_FIELD_NAME = '_csrf_token' +COOKIE_NAME = 'csrf_token' + + +def make_app(): + csrf_policy = aiohttp_csrf.policy.FormPolicy(FORM_FIELD_NAME) + + csrf_storage = aiohttp_csrf.storage.CookieStorage(COOKIE_NAME) + + app = web.Application() + + aiohttp_csrf.setup(app, policy=csrf_policy, storage=csrf_storage) + + app.middlewares.append(aiohttp_csrf.csrf_middleware) + + async def handler_get_form_with_token(request): + token = await aiohttp_csrf.generate_token(request) + + + body = ''' + + Form with csrf protection + +
+ + + +
+ + + ''' # noqa + + body = body.format(field_name=FORM_FIELD_NAME, token=token) + + return web.Response( + body=body.encode('utf-8'), + content_type='text/html', + ) + + async def handler_post_check(request): + post = await request.post() + + body = 'Hello, {name}'.format(name=post['name']) + + return web.Response( + body=body.encode('utf-8'), + content_type='text/html', + ) + + app.router.add_route( + 'GET', + '/', + handler_get_form_with_token, + ) + + app.router.add_route( + 'POST', + '/', + handler_post_check, + ) + + return app + + +web.run_app(make_app()) +``` + +### Initialize + +First of all, you need to initialize `aiohttp_csrf` in your application: + +``` {.sourceCode .python} +app = web.Application() + +csrf_policy = aiohttp_csrf.policy.FormPolicy(FORM_FIELD_NAME) + +csrf_storage = aiohttp_csrf.storage.CookieStorage(COOKIE_NAME) + +aiohttp_csrf.setup(app, policy=csrf_policy, storage=csrf_storage) +``` + +### Middleware and decorators + +After initialize you can use `@aiohttp_csrf.csrf_protect` for handlers, +that you want to protect. Or you can initialize +`aiohttp_csrf.csrf_middleware` and do not disturb about using decorator +([full middleware example here](demo/middleware.py)): + +``` {.sourceCode .python} +... +app.middlewares.append(aiohttp_csrf.csrf_middleware) +... +``` + +In this case all your handlers will be protected. + +**Note:** we strongly recommend to use `aiohttp_csrf.csrf_middleware` +and `@aiohttp_csrf.csrf_exempt` instead of manually managing with +`@aiohttp_csrf.csrf_protect`. But if you prefer to use +`@aiohttp_csrf.csrf_protect`, don't forget to use +`@aiohttp_csrf.csrf_protect` for both methods: GET and POST ([manual +protection example](demo/manual_protection.py)) + +If you want to use middleware, but need handlers without protection, you +can use `@aiohttp_csrf.csrf_exempt`. Mark you handler with this +decorator and this handler will not check the token: + +``` {.sourceCode .python} +@aiohttp_csrf.csrf_exempt +async def handler_post_not_check(request): + ... +``` + +### Generate token + +For generate token you need to call `aiohttp_csrf.generate_token` in +your handler: + +``` {.sourceCode .python} +@aiohttp_csrf.csrf_protect +async def handler_get(request): + token = await aiohttp_csrf.generate_token(request) + ... +``` + +Advanced usage +-------------- + +### Policies + +You can use different policies for check tokens. Library provides 3 +types of policy: + +- **FormPolicy**. This policy will search token in the body of your + POST request (Usually use for forms) or as a GET variable of the + same name. You need to specify name of field that will be checked. +- **HeaderPolicy**. This policy will search token in headers of your + POST request (Usually use for AJAX requests). You need to specify + name of header that will be checked. +- **FormAndHeaderPolicy**. This policy combines behavior of + **FormPolicy** and **HeaderPolicy**. + +You can implement your custom policies if needed. But make sure that +your custom policy implements `aiohttp_csrf.policy.AbstractPolicy` +interface. + +### Storages + +You can use different types of storages for storing token. Library +provides 2 types of storage: + +- **CookieStorage**. Your token will be stored in cookie variable. You + need to specify cookie name. +- **SessionStorage**. Your token will be stored in session. You need + to specify session variable name. + +**Important:** If you want to use session storage, you need setup +aiohttp\_session in your application ([session storage +example](demo/session_storage.py#L22)) + +You can implement your custom storages if needed. But make sure that +your custom storage implements `aiohttp_csrf.storage.AbstractStorage` +interface. + +### Token generators + +You can use different token generator in your application. By default +storages using `aiohttp_csrf.token_generator.SimpleTokenGenerator` + +But if you need more secure token generator - you can use +`aiohttp_csrf.token_generator.HashedTokenGenerator` + +And you can implement your custom token generators if needed. But make +sure that your custom token generator implements +`aiohttp_csrf.token_generator.AbstractTokenGenerator` interface. + +### Invalid token behavior + +By default, if token is invalid, `aiohttp_csrf` will raise +`aiohttp.web.HTTPForbidden` exception. + +You have abbility to specify your custom error handler. It can be: + +- **callable instance**. Input parameter - aiohttp request. .. +code-block:: python + +> def custom\_error\_handler(request): +> : \# do something return aiohttp.web.Response(status=403) +> +> \# or +> +> async def custom\_async\_error\_handler(request): +> : \# await do something return aiohttp.web.Response(status=403) +> +It will be called instead of protected handler. + +- **sub class of Exception**. In this case this Exception will be + raised. + +``` {.sourceCode .python} +class CustomException(Exception): + pass +``` + +You can specify custom error handler globally, when initialize +`aiohttp_csrf` in your application: + +``` {.sourceCode .python} +... +class CustomException(Exception): + pass + +... +aiohttp_csrf.setup(app, policy=csrf_policy, storage=csrf_storage, error_renderer=CustomException) +... +``` + +In this case custom error handler will be applied to all protected +handlers. + +Or you can specify custom error handler locally, for specific handler: + +``` {.sourceCode .python} +... +class CustomException(Exception): + pass + +... +@aiohttp_csrf.csrf_protect(error_renderer=CustomException) +def handler_with_custom_csrf_error(request): + ... +``` + +In this case custom error handler will be applied to this handler only. +For all other handlers will be applied global error handler. \ No newline at end of file diff --git a/README.rst b/README.rst deleted file mode 100644 index 4ebe07c..0000000 --- a/README.rst +++ /dev/null @@ -1,261 +0,0 @@ -aiohttp_csrf -============ - -The library provides csrf (xsrf) protection for `aiohttp.web`__. - -.. _aiohttp_web: https://docs.aiohttp.org/en/latest/web.html - -__ aiohttp_web_ - -.. image:: https://img.shields.io/travis/wikibusiness/aiohttp-csrf.svg - :target: https://travis-ci.org/wikibusiness/aiohttp-csrf - -Basic usage ------------ - -The library allows you to implement csrf (xsrf) protection for requests - - -Basic usage example: - -.. code-block:: python - - import aiohttp_csrf - from aiohttp import web - - FORM_FIELD_NAME = '_csrf_token' - COOKIE_NAME = 'csrf_token' - - - def make_app(): - csrf_policy = aiohttp_csrf.policy.FormPolicy(FORM_FIELD_NAME) - - csrf_storage = aiohttp_csrf.storage.CookieStorage(COOKIE_NAME) - - app = web.Application() - - aiohttp_csrf.setup(app, policy=csrf_policy, storage=csrf_storage) - - app.middlewares.append(aiohttp_csrf.csrf_middleware) - - async def handler_get_form_with_token(request): - token = await aiohttp_csrf.generate_token(request) - - - body = ''' - - Form with csrf protection - -
- - - -
- - - ''' # noqa - - body = body.format(field_name=FORM_FIELD_NAME, token=token) - - return web.Response( - body=body.encode('utf-8'), - content_type='text/html', - ) - - async def handler_post_check(request): - post = await request.post() - - body = 'Hello, {name}'.format(name=post['name']) - - return web.Response( - body=body.encode('utf-8'), - content_type='text/html', - ) - - app.router.add_route( - 'GET', - '/', - handler_get_form_with_token, - ) - - app.router.add_route( - 'POST', - '/', - handler_post_check, - ) - - return app - - - web.run_app(make_app()) - - -Initialize -~~~~~~~~~~ - - -First of all, you need to initialize ``aiohttp_csrf`` in your application: - -.. code-block:: python - - app = web.Application() - - csrf_policy = aiohttp_csrf.policy.FormPolicy(FORM_FIELD_NAME) - - csrf_storage = aiohttp_csrf.storage.CookieStorage(COOKIE_NAME) - - aiohttp_csrf.setup(app, policy=csrf_policy, storage=csrf_storage) - - -Middleware and decorators -~~~~~~~~~~~~~~~~~~~~~~~~~ - - -After initialize you can use ``@aiohttp_csrf.csrf_protect`` for handlers, that you want to protect. -Or you can initialize ``aiohttp_csrf.csrf_middleware`` and do not disturb about using decorator (`full middleware example here`_): - -.. _full middleware example here: demo/middleware.py - -.. code-block:: python - - ... - app.middlewares.append(aiohttp_csrf.csrf_middleware) - ... - - -In this case all your handlers will be protected. - - -**Note:** we strongly recommend to use ``aiohttp_csrf.csrf_middleware`` and ``@aiohttp_csrf.csrf_exempt`` instead of manually managing with ``@aiohttp_csrf.csrf_protect``. -But if you prefer to use ``@aiohttp_csrf.csrf_protect``, don't forget to use ``@aiohttp_csrf.csrf_protect`` for both methods: GET and POST -(`manual protection example`_) - -.. _manual protection example: demo/manual_protection.py - - -If you want to use middleware, but need handlers without protection, you can use ``@aiohttp_csrf.csrf_exempt``. -Mark you handler with this decorator and this handler will not check the token: - -.. code-block:: python - - @aiohttp_csrf.csrf_exempt - async def handler_post_not_check(request): - ... - - - -Generate token -~~~~~~~~~~~~~~ - -For generate token you need to call ``aiohttp_csrf.generate_token`` in your handler: - -.. code-block:: python - - @aiohttp_csrf.csrf_protect - async def handler_get(request): - token = await aiohttp_csrf.generate_token(request) - ... - - -Advanced usage --------------- - - -Policies -~~~~~~~~ - -You can use different policies for check tokens. Library provides 3 types of policy: - -- **FormPolicy**. This policy will search token in the body of your POST request (Usually use for forms). You need to specify name of field that will be checked. -- **HeaderPolicy**. This policy will search token in headers of your POST request (Usually use for AJAX requests). You need to specify name of header that will be checked. -- **FormAndHeaderPolicy**. This policy combines behavior of **FormPolicy** and **HeaderPolicy**. - -You can implement your custom policies if needed. But make sure that your custom policy implements ``aiohttp_csrf.policy.AbstractPolicy`` interface. - -Storages -~~~~~~~~ - -You can use different types of storages for storing token. Library provides 2 types of storage: - -- **CookieStorage**. Your token will be stored in cookie variable. You need to specify cookie name. -- **SessionStorage**. Your token will be stored in session. You need to specify session variable name. - -**Important:** If you want to use session storage, you need setup aiohttp_session in your application -(`session storage example`_) - -.. _session storage example: demo/session_storage.py#L22 - -You can implement your custom storages if needed. But make sure that your custom storage implements ``aiohttp_csrf.storage.AbstractStorage`` interface. - - -Token generators -~~~~~~~~~~~~~~~~ - -You can use different token generator in your application. -By default storages using ``aiohttp_csrf.token_generator.SimpleTokenGenerator`` - -But if you need more secure token generator - you can use ``aiohttp_csrf.token_generator.HashedTokenGenerator`` - -And you can implement your custom token generators if needed. But make sure that your custom token generator implements ``aiohttp_csrf.token_generator.AbstractTokenGenerator`` interface. - - -Invalid token behavior -~~~~~~~~~~~~~~~~~~~~~~ - -By default, if token is invalid, ``aiohttp_csrf`` will raise ``aiohttp.web.HTTPForbidden`` exception. - -You have abbility to specify your custom error handler. It can be: - -- **callable instance**. Input parameter - aiohttp request. -.. code-block:: python - - def custom_error_handler(request): - # do something - return aiohttp.web.Response(status=403) - - # or - - async def custom_async_error_handler(request): - # await do something - return aiohttp.web.Response(status=403) - -It will be called instead of protected handler. - -- **sub class of Exception**. In this case this Exception will be raised. - -.. code-block:: python - - class CustomException(Exception): - pass - - -You can specify custom error handler globally, when initialize ``aiohttp_csrf`` in your application: - -.. code-block:: python - - ... - class CustomException(Exception): - pass - - ... - aiohttp_csrf.setup(app, policy=csrf_policy, storage=csrf_storage, error_renderer=CustomException) - ... - -In this case custom error handler will be applied to all protected handlers. - -Or you can specify custom error handler locally, for specific handler: - -.. code-block:: python - - ... - class CustomException(Exception): - pass - - ... - @aiohttp_csrf.csrf_protect(error_renderer=CustomException) - def handler_with_custom_csrf_error(request): - ... - - -In this case custom error handler will be applied to this handler only. -For all other handlers will be applied global error handler. diff --git a/aiohttp_csrf/policy.py b/aiohttp_csrf/policy.py index 2196dca..ecf3352 100644 --- a/aiohttp_csrf/policy.py +++ b/aiohttp_csrf/policy.py @@ -15,8 +15,8 @@ def __init__(self, field_name): async def check(self, request, original_value): post = await request.post() - - token = post.get(self.field_name) + get = request.match_info.get(self.field_name, None) + token = get if get is not None else post.get(self.field_name) return compare_digest(token, original_value) diff --git a/demo/middleware.py b/demo/middleware.py index 42220b0..b554138 100644 --- a/demo/middleware.py +++ b/demo/middleware.py @@ -16,7 +16,7 @@ def make_app(): app.middlewares.append(aiohttp_csrf.csrf_middleware) - async def handler_get_form_with_token(request): + async def handler_get_form_with_post_token(request): token = await aiohttp_csrf.generate_token(request) body = ''' @@ -39,6 +39,28 @@ async def handler_get_form_with_token(request): content_type='text/html', ) + async def handler_get_form_with_get_token(request): + token = await aiohttp_csrf.generate_token(request) + + body = ''' + + Form with csrf protection + +
+ + +
+ + + ''' # noqa + + body = body.format(field_name=FORM_FIELD_NAME, token=token) + + return web.Response( + body=body.encode('utf-8'), + content_type='text/html', + ) + async def handler_post_check(request): post = await request.post() @@ -80,15 +102,19 @@ async def handler_post_not_check(request): app.router.add_route( 'GET', - '/form_with_check', - handler_get_form_with_token, + '/form_with_post_check', + handler_get_form_with_post_token, + ) + app.router.add_route( + 'GET', + '/form_with_get_check', + handler_get_form_with_get_token, ) app.router.add_route( 'POST', - '/post_with_check', + '/post_with_check/{'+FORM_FIELD_NAME+'}', handler_post_check, ) - app.router.add_route( 'GET', '/form_without_check', From fdd2b5af73a25a697a67a0c1064afeb05fc55abc Mon Sep 17 00:00:00 2001 From: TensorTom Date: Wed, 4 Mar 2020 22:48:03 -0500 Subject: [PATCH 09/23] Minor changes to pass build checks. --- README.md | 252 ------------------------------------ README.rst | 261 ++++++++++++++++++++++++++++++++++++++ test.py | 2 +- tests/test_form_policy.py | 2 +- 4 files changed, 263 insertions(+), 254 deletions(-) delete mode 100644 README.md create mode 100644 README.rst diff --git a/README.md b/README.md deleted file mode 100644 index 745c082..0000000 --- a/README.md +++ /dev/null @@ -1,252 +0,0 @@ -aiohttp\_csrf -============= - -The library provides csrf (xsrf) protection for -[aiohttp.web](aiohttp_web_). - -Basic usage ------------ - -The library allows you to implement csrf (xsrf) protection for requests - -Basic usage example: - -``` {.sourceCode .python} -import aiohttp_csrf -from aiohttp import web - -FORM_FIELD_NAME = '_csrf_token' -COOKIE_NAME = 'csrf_token' - - -def make_app(): - csrf_policy = aiohttp_csrf.policy.FormPolicy(FORM_FIELD_NAME) - - csrf_storage = aiohttp_csrf.storage.CookieStorage(COOKIE_NAME) - - app = web.Application() - - aiohttp_csrf.setup(app, policy=csrf_policy, storage=csrf_storage) - - app.middlewares.append(aiohttp_csrf.csrf_middleware) - - async def handler_get_form_with_token(request): - token = await aiohttp_csrf.generate_token(request) - - - body = ''' - - Form with csrf protection - -
- - - -
- - - ''' # noqa - - body = body.format(field_name=FORM_FIELD_NAME, token=token) - - return web.Response( - body=body.encode('utf-8'), - content_type='text/html', - ) - - async def handler_post_check(request): - post = await request.post() - - body = 'Hello, {name}'.format(name=post['name']) - - return web.Response( - body=body.encode('utf-8'), - content_type='text/html', - ) - - app.router.add_route( - 'GET', - '/', - handler_get_form_with_token, - ) - - app.router.add_route( - 'POST', - '/', - handler_post_check, - ) - - return app - - -web.run_app(make_app()) -``` - -### Initialize - -First of all, you need to initialize `aiohttp_csrf` in your application: - -``` {.sourceCode .python} -app = web.Application() - -csrf_policy = aiohttp_csrf.policy.FormPolicy(FORM_FIELD_NAME) - -csrf_storage = aiohttp_csrf.storage.CookieStorage(COOKIE_NAME) - -aiohttp_csrf.setup(app, policy=csrf_policy, storage=csrf_storage) -``` - -### Middleware and decorators - -After initialize you can use `@aiohttp_csrf.csrf_protect` for handlers, -that you want to protect. Or you can initialize -`aiohttp_csrf.csrf_middleware` and do not disturb about using decorator -([full middleware example here](demo/middleware.py)): - -``` {.sourceCode .python} -... -app.middlewares.append(aiohttp_csrf.csrf_middleware) -... -``` - -In this case all your handlers will be protected. - -**Note:** we strongly recommend to use `aiohttp_csrf.csrf_middleware` -and `@aiohttp_csrf.csrf_exempt` instead of manually managing with -`@aiohttp_csrf.csrf_protect`. But if you prefer to use -`@aiohttp_csrf.csrf_protect`, don't forget to use -`@aiohttp_csrf.csrf_protect` for both methods: GET and POST ([manual -protection example](demo/manual_protection.py)) - -If you want to use middleware, but need handlers without protection, you -can use `@aiohttp_csrf.csrf_exempt`. Mark you handler with this -decorator and this handler will not check the token: - -``` {.sourceCode .python} -@aiohttp_csrf.csrf_exempt -async def handler_post_not_check(request): - ... -``` - -### Generate token - -For generate token you need to call `aiohttp_csrf.generate_token` in -your handler: - -``` {.sourceCode .python} -@aiohttp_csrf.csrf_protect -async def handler_get(request): - token = await aiohttp_csrf.generate_token(request) - ... -``` - -Advanced usage --------------- - -### Policies - -You can use different policies for check tokens. Library provides 3 -types of policy: - -- **FormPolicy**. This policy will search token in the body of your - POST request (Usually use for forms) or as a GET variable of the - same name. You need to specify name of field that will be checked. -- **HeaderPolicy**. This policy will search token in headers of your - POST request (Usually use for AJAX requests). You need to specify - name of header that will be checked. -- **FormAndHeaderPolicy**. This policy combines behavior of - **FormPolicy** and **HeaderPolicy**. - -You can implement your custom policies if needed. But make sure that -your custom policy implements `aiohttp_csrf.policy.AbstractPolicy` -interface. - -### Storages - -You can use different types of storages for storing token. Library -provides 2 types of storage: - -- **CookieStorage**. Your token will be stored in cookie variable. You - need to specify cookie name. -- **SessionStorage**. Your token will be stored in session. You need - to specify session variable name. - -**Important:** If you want to use session storage, you need setup -aiohttp\_session in your application ([session storage -example](demo/session_storage.py#L22)) - -You can implement your custom storages if needed. But make sure that -your custom storage implements `aiohttp_csrf.storage.AbstractStorage` -interface. - -### Token generators - -You can use different token generator in your application. By default -storages using `aiohttp_csrf.token_generator.SimpleTokenGenerator` - -But if you need more secure token generator - you can use -`aiohttp_csrf.token_generator.HashedTokenGenerator` - -And you can implement your custom token generators if needed. But make -sure that your custom token generator implements -`aiohttp_csrf.token_generator.AbstractTokenGenerator` interface. - -### Invalid token behavior - -By default, if token is invalid, `aiohttp_csrf` will raise -`aiohttp.web.HTTPForbidden` exception. - -You have abbility to specify your custom error handler. It can be: - -- **callable instance**. Input parameter - aiohttp request. .. -code-block:: python - -> def custom\_error\_handler(request): -> : \# do something return aiohttp.web.Response(status=403) -> -> \# or -> -> async def custom\_async\_error\_handler(request): -> : \# await do something return aiohttp.web.Response(status=403) -> -It will be called instead of protected handler. - -- **sub class of Exception**. In this case this Exception will be - raised. - -``` {.sourceCode .python} -class CustomException(Exception): - pass -``` - -You can specify custom error handler globally, when initialize -`aiohttp_csrf` in your application: - -``` {.sourceCode .python} -... -class CustomException(Exception): - pass - -... -aiohttp_csrf.setup(app, policy=csrf_policy, storage=csrf_storage, error_renderer=CustomException) -... -``` - -In this case custom error handler will be applied to all protected -handlers. - -Or you can specify custom error handler locally, for specific handler: - -``` {.sourceCode .python} -... -class CustomException(Exception): - pass - -... -@aiohttp_csrf.csrf_protect(error_renderer=CustomException) -def handler_with_custom_csrf_error(request): - ... -``` - -In this case custom error handler will be applied to this handler only. -For all other handlers will be applied global error handler. \ No newline at end of file diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..fcc1fcc --- /dev/null +++ b/README.rst @@ -0,0 +1,261 @@ +aiohttp_csrf +============ + +The library provides csrf (xsrf) protection for `aiohttp.web`__. + +.. _aiohttp_web: https://docs.aiohttp.org/en/latest/web.html + +__ aiohttp_web_ + +.. image:: https://img.shields.io/travis/wikibusiness/aiohttp-csrf.svg + :target: https://travis-ci.org/wikibusiness/aiohttp-csrf + +Basic usage +----------- + +The library allows you to implement csrf (xsrf) protection for requests + + +Basic usage example: + +.. code-block:: python + + import aiohttp_csrf + from aiohttp import web + + FORM_FIELD_NAME = '_csrf_token' + COOKIE_NAME = 'csrf_token' + + + def make_app(): + csrf_policy = aiohttp_csrf.policy.FormPolicy(FORM_FIELD_NAME) + + csrf_storage = aiohttp_csrf.storage.CookieStorage(COOKIE_NAME) + + app = web.Application() + + aiohttp_csrf.setup(app, policy=csrf_policy, storage=csrf_storage) + + app.middlewares.append(aiohttp_csrf.csrf_middleware) + + async def handler_get_form_with_token(request): + token = await aiohttp_csrf.generate_token(request) + + + body = ''' + + Form with csrf protection + +
+ + + +
+ + + ''' # noqa + + body = body.format(field_name=FORM_FIELD_NAME, token=token) + + return web.Response( + body=body.encode('utf-8'), + content_type='text/html', + ) + + async def handler_post_check(request): + post = await request.post() + + body = 'Hello, {name}'.format(name=post['name']) + + return web.Response( + body=body.encode('utf-8'), + content_type='text/html', + ) + + app.router.add_route( + 'GET', + '/', + handler_get_form_with_token, + ) + + app.router.add_route( + 'POST', + '/', + handler_post_check, + ) + + return app + + + web.run_app(make_app()) + + +Initialize +~~~~~~~~~~ + + +First of all, you need to initialize ``aiohttp_csrf`` in your application: + +.. code-block:: python + + app = web.Application() + + csrf_policy = aiohttp_csrf.policy.FormPolicy(FORM_FIELD_NAME) + + csrf_storage = aiohttp_csrf.storage.CookieStorage(COOKIE_NAME) + + aiohttp_csrf.setup(app, policy=csrf_policy, storage=csrf_storage) + + +Middleware and decorators +~~~~~~~~~~~~~~~~~~~~~~~~~ + + +After initialize you can use ``@aiohttp_csrf.csrf_protect`` for handlers, that you want to protect. +Or you can initialize ``aiohttp_csrf.csrf_middleware`` and do not disturb about using decorator (`full middleware example here`_): + +.. _full middleware example here: demo/middleware.py + +.. code-block:: python + + ... + app.middlewares.append(aiohttp_csrf.csrf_middleware) + ... + + +In this case all your handlers will be protected. + + +**Note:** we strongly recommend to use ``aiohttp_csrf.csrf_middleware`` and ``@aiohttp_csrf.csrf_exempt`` instead of manually managing with ``@aiohttp_csrf.csrf_protect``. +But if you prefer to use ``@aiohttp_csrf.csrf_protect``, don't forget to use ``@aiohttp_csrf.csrf_protect`` for both methods: GET and POST +(`manual protection example`_) + +.. _manual protection example: demo/manual_protection.py + + +If you want to use middleware, but need handlers without protection, you can use ``@aiohttp_csrf.csrf_exempt``. +Mark you handler with this decorator and this handler will not check the token: + +.. code-block:: python + + @aiohttp_csrf.csrf_exempt + async def handler_post_not_check(request): + ... + + + +Generate token +~~~~~~~~~~~~~~ + +For generate token you need to call ``aiohttp_csrf.generate_token`` in your handler: + +.. code-block:: python + + @aiohttp_csrf.csrf_protect + async def handler_get(request): + token = await aiohttp_csrf.generate_token(request) + ... + + +Advanced usage +-------------- + + +Policies +~~~~~~~~ + +You can use different policies for check tokens. Library provides 3 types of policy: + +- **FormPolicy**. This policy will search token in the body of your POST request (Usually use for forms) or as a GET variable of the same name. You need to specify name of field that will be checked. +- **HeaderPolicy**. This policy will search token in headers of your POST request (Usually use for AJAX requests). You need to specify name of header that will be checked. +- **FormAndHeaderPolicy**. This policy combines behavior of **FormPolicy** and **HeaderPolicy**. + +You can implement your custom policies if needed. But make sure that your custom policy implements ``aiohttp_csrf.policy.AbstractPolicy`` interface. + +Storages +~~~~~~~~ + +You can use different types of storages for storing token. Library provides 2 types of storage: + +- **CookieStorage**. Your token will be stored in cookie variable. You need to specify cookie name. +- **SessionStorage**. Your token will be stored in session. You need to specify session variable name. + +**Important:** If you want to use session storage, you need setup aiohttp_session in your application +(`session storage example`_) + +.. _session storage example: demo/session_storage.py#L22 + +You can implement your custom storages if needed. But make sure that your custom storage implements ``aiohttp_csrf.storage.AbstractStorage`` interface. + + +Token generators +~~~~~~~~~~~~~~~~ + +You can use different token generator in your application. +By default storages using ``aiohttp_csrf.token_generator.SimpleTokenGenerator`` + +But if you need more secure token generator - you can use ``aiohttp_csrf.token_generator.HashedTokenGenerator`` + +And you can implement your custom token generators if needed. But make sure that your custom token generator implements ``aiohttp_csrf.token_generator.AbstractTokenGenerator`` interface. + + +Invalid token behavior +~~~~~~~~~~~~~~~~~~~~~~ + +By default, if token is invalid, ``aiohttp_csrf`` will raise ``aiohttp.web.HTTPForbidden`` exception. + +You have abbility to specify your custom error handler. It can be: + +- **callable instance**. Input parameter - aiohttp request. +.. code-block:: python + + def custom_error_handler(request): + # do something + return aiohttp.web.Response(status=403) + + # or + + async def custom_async_error_handler(request): + # await do something + return aiohttp.web.Response(status=403) + +It will be called instead of protected handler. + +- **sub class of Exception**. In this case this Exception will be raised. + +.. code-block:: python + + class CustomException(Exception): + pass + + +You can specify custom error handler globally, when initialize ``aiohttp_csrf`` in your application: + +.. code-block:: python + + ... + class CustomException(Exception): + pass + + ... + aiohttp_csrf.setup(app, policy=csrf_policy, storage=csrf_storage, error_renderer=CustomException) + ... + +In this case custom error handler will be applied to all protected handlers. + +Or you can specify custom error handler locally, for specific handler: + +.. code-block:: python + + ... + class CustomException(Exception): + pass + + ... + @aiohttp_csrf.csrf_protect(error_renderer=CustomException) + def handler_with_custom_csrf_error(request): + ... + + +In this case custom error handler will be applied to this handler only. +For all other handlers will be applied global error handler. diff --git a/test.py b/test.py index 843d442..4bee5ef 100644 --- a/test.py +++ b/test.py @@ -10,7 +10,7 @@ async def hello(request): def dec(handler): def wrapped(*args, **kwargs): request = args[-1] - #import ipdb;ipdb.set_trace() + import ipdb;ipdb.set_trace() return handler(*args, **kwargs) return wrapped diff --git a/tests/test_form_policy.py b/tests/test_form_policy.py index 2cfc99b..11a9689 100644 --- a/tests/test_form_policy.py +++ b/tests/test_form_policy.py @@ -8,7 +8,7 @@ from aiohttp_session import setup as setup_session from aiohttp_session import SimpleCookieStorage -from conftest import FORM_FIELD_NAME +from .conftest import FORM_FIELD_NAME FORM_FIELD_REGEX = re.compile( r'', From b9af6d9ae150aafe5514deae451f6cb706d59c73 Mon Sep 17 00:00:00 2001 From: TensorTom Date: Wed, 4 Mar 2020 23:24:36 -0500 Subject: [PATCH 10/23] Fix for multipart bug. --- aiohttp_csrf/policy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiohttp_csrf/policy.py b/aiohttp_csrf/policy.py index ecf3352..f372ae5 100644 --- a/aiohttp_csrf/policy.py +++ b/aiohttp_csrf/policy.py @@ -14,8 +14,8 @@ def __init__(self, field_name): self.field_name = field_name async def check(self, request, original_value): - post = await request.post() get = request.match_info.get(self.field_name, None) + post = await request.post() if get is None else None token = get if get is not None else post.get(self.field_name) return compare_digest(token, original_value) From e86686029542269d217646a987f7114e5cbb706d Mon Sep 17 00:00:00 2001 From: TensorTom Date: Wed, 4 Mar 2020 23:47:25 -0500 Subject: [PATCH 11/23] Avoid incorrect server error if CSRF is missing. --- aiohttp_csrf/policy.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/aiohttp_csrf/policy.py b/aiohttp_csrf/policy.py index f372ae5..c84083a 100644 --- a/aiohttp_csrf/policy.py +++ b/aiohttp_csrf/policy.py @@ -15,8 +15,10 @@ def __init__(self, field_name): async def check(self, request, original_value): get = request.match_info.get(self.field_name, None) - post = await request.post() if get is None else None - token = get if get is not None else post.get(self.field_name) + post_req = await request.post() if get is None else None + post = post_req.get(self.field_name) if post_req is not None else None + post = post if post is not None else '' + token = get if get is not None else post return compare_digest(token, original_value) From 5812c6e5ef2419e62c1b93758e906c7414387a6b Mon Sep 17 00:00:00 2001 From: TensorTom Date: Thu, 5 Mar 2020 00:03:27 -0500 Subject: [PATCH 12/23] Increase version to 0.0.2 --- aiohttp_csrf/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiohttp_csrf/__init__.py b/aiohttp_csrf/__init__.py index 7a4c361..2b625ed 100644 --- a/aiohttp_csrf/__init__.py +++ b/aiohttp_csrf/__init__.py @@ -9,7 +9,7 @@ from .storage import AbstractStorage -__version__ = '0.0.1' +__version__ = '0.0.2' APP_POLICY_KEY = 'aiohttp_csrf_policy' APP_STORAGE_KEY = 'aiohttp_csrf_storage' From 58a5b01a718076273ca79345959f4d03edb95710 Mon Sep 17 00:00:00 2001 From: TensorTom Date: Thu, 5 Mar 2020 00:24:58 -0500 Subject: [PATCH 13/23] Made ready for pypi upload. --- README.rst | 5 +++-- setup.py | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index fcc1fcc..dbe7c77 100644 --- a/README.rst +++ b/README.rst @@ -204,9 +204,10 @@ Invalid token behavior By default, if token is invalid, ``aiohttp_csrf`` will raise ``aiohttp.web.HTTPForbidden`` exception. -You have abbility to specify your custom error handler. It can be: +You have ability to specify your custom error handler. It can be: + +- **callable instance. Input parameter - aiohttp request.** -- **callable instance**. Input parameter - aiohttp request. .. code-block:: python def custom_error_handler(request): diff --git a/setup.py b/setup.py index 4396b14..0fdc06e 100644 --- a/setup.py +++ b/setup.py @@ -42,9 +42,8 @@ def read(*parts): version=get_version(), description=('CSRF protection for aiohttp.web',), long_description=read('README.rst'), - author='Ocean S.A.', - author_email='osf@ocean.io', - url='https://github.com/wikibusiness/aiohttp-csrf', + author='TensorTom', + url='https://github.com/TensorTom/aiohttp-csrf', packages=['aiohttp_csrf'], include_package_data=True, install_requires=install_requires, @@ -61,6 +60,7 @@ def read(*parts): 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.8', ], keywords=[ 'csrf', From b53555e37f46abab4895bca81c4385cd0aa762b8 Mon Sep 17 00:00:00 2001 From: TensorTom <14287229+TensorTom@users.noreply.github.com> Date: Sun, 20 Dec 2020 03:59:14 -0500 Subject: [PATCH 14/23] Switch to blake3 hashing & bumps. --- .gitignore | 3 +- Pipfile | 16 +- README.md | 244 ++++++++ README.rst | 11 + aiohttp_csrf/token_generator.py | 4 +- gensetup.sh | 3 + poetry.lock | 993 ++++++++++++++++++++++++++++++++ pyproject.toml | 23 + setup.py | 93 +-- 9 files changed, 1314 insertions(+), 76 deletions(-) create mode 100644 README.md create mode 100644 gensetup.sh create mode 100644 poetry.lock create mode 100644 pyproject.toml diff --git a/.gitignore b/.gitignore index 3ae2f48..00c922a 100644 --- a/.gitignore +++ b/.gitignore @@ -18,7 +18,6 @@ htmlcov # generic files to ignore *~ -*.lock *.DS_Store *.swp *.out @@ -26,3 +25,5 @@ htmlcov .tox/ deps/ docs/_build/ + +idea/ \ No newline at end of file diff --git a/Pipfile b/Pipfile index eb9f1fd..8f39852 100644 --- a/Pipfile +++ b/Pipfile @@ -4,18 +4,18 @@ url = "https://pypi.org/simple" verify_ssl = true [dev-packages] -aiohttp-csrf = {editable = true,path = "."} -pytest = "==5.3.5" -pytest-aiohttp = "==0.3.0" -pytest-cov = "==2.8.1" -tox = "==3.14.5" -flake8 = "==3.7.9" +pytest = ">=5.3.5" +pytest-aiohttp = "==0.*,>=0.3.0" +pytest-cov = ">=2.8.1" +tox = ">=3.14.5" +flake8 = ">=3.7.9" isort = "==4.3.21" ipdb = "*" [packages] -aiohttp = "==3.6.2" -aiohttp-session = "==2.9.0" +aiohttp = "<3.8,>=3.6.2" +aiohttp-session = {extras = ["aioredis"], git = "https://github.com/TheDoctorAI/aiohttp-session", ref = "master"} +blake3 = "==0.*,>=0.1.8" [requires] python_version = "3.8" diff --git a/README.md b/README.md new file mode 100644 index 0000000..ee2175b --- /dev/null +++ b/README.md @@ -0,0 +1,244 @@ +aiohttp_csrf +============= + +The library provides csrf (xsrf) protection for [aiohttp.web](https://docs.aiohttp.org/en/latest/web.html). + +**note:** The package [aiohttp-csrf-fixed](https://pypi.org/project/aiohttp-csrf-fixed) is aiohttp_csrf 0.0.2 + +[this commit](https://github.com/oplik0/aiohttp-csrf/commit/b1bd9207f43a2abf30e32e72ecdb10983a251823). The maintainer +didn't submit a PR so I just saw it by chance. I haven't had time to closely examine it but I think it's just removing +the HTTP security error that happens if no CSRF is provided. Why do that? An HTTP error is good because it tells the +client what happened and lets you handle it by middleware. + +Since they bumped to 0.0.3, I'm skipping that version and +going to 0.0.4 then merging their repo to a new branch. + +![image](https://img.shields.io/travis/wikibusiness/aiohttp-csrf.svg%0A%20:target:%20https://travis-ci.org/wikibusiness/aiohttp-csrf) + +Basic usage +----------- + +The library allows you to implement csrf (xsrf) protection for requests + +Basic usage example: + +``` {.sourceCode .python} +import aiohttp_csrf +from aiohttp import web + +FORM_FIELD_NAME = '_csrf_token' +COOKIE_NAME = 'csrf_token' + + +def make_app(): + csrf_policy = aiohttp_csrf.policy.FormPolicy(FORM_FIELD_NAME) + + csrf_storage = aiohttp_csrf.storage.CookieStorage(COOKIE_NAME) + + app = web.Application() + + aiohttp_csrf.setup(app, policy=csrf_policy, storage=csrf_storage) + + app.middlewares.append(aiohttp_csrf.csrf_middleware) + + async def handler_get_form_with_token(request): + token = await aiohttp_csrf.generate_token(request) + + + body = ''' + + Form with csrf protection + +
+ + + +
+ + + ''' # noqa + + body = body.format(field_name=FORM_FIELD_NAME, token=token) + + return web.Response( + body=body.encode('utf-8'), + content_type='text/html', + ) + + async def handler_post_check(request): + post = await request.post() + + body = 'Hello, {name}'.format(name=post['name']) + + return web.Response( + body=body.encode('utf-8'), + content_type='text/html', + ) + + app.router.add_route( + 'GET', + '/', + handler_get_form_with_token, + ) + + app.router.add_route( + 'POST', + '/', + handler_post_check, + ) + + return app + + +web.run_app(make_app()) +``` + +### Initialize + +First of all, you need to initialize `aiohttp_csrf` in your application: + +``` {.sourceCode .python} +app = web.Application() + +csrf_policy = aiohttp_csrf.policy.FormPolicy(FORM_FIELD_NAME) + +csrf_storage = aiohttp_csrf.storage.CookieStorage(COOKIE_NAME) + +aiohttp_csrf.setup(app, policy=csrf_policy, storage=csrf_storage) +``` + +### Middleware and decorators + +After initialize you can use `@aiohttp_csrf.csrf_protect` for handlers, that you want to protect. Or you can +initialize `aiohttp_csrf.csrf_middleware` and do not disturb about using +decorator ([full middleware example here](demo/middleware.py)): + +``` {.sourceCode .python} +... +app.middlewares.append(aiohttp_csrf.csrf_middleware) +... +``` + +In this case all your handlers will be protected. + +**Note:** we strongly recommend to use `aiohttp_csrf.csrf_middleware` and `@aiohttp_csrf.csrf_exempt` instead of +manually managing with `@aiohttp_csrf.csrf_protect`. But if you prefer to use `@aiohttp_csrf.csrf_protect`, don't forget +to use `@aiohttp_csrf.csrf_protect` for both methods: GET and +POST ([manual protection example](demo/manual_protection.py)) + +If you want to use middleware, but need handlers without protection, you can use `@aiohttp_csrf.csrf_exempt`. Mark you +handler with this decorator and this handler will not check the token: + +``` {.sourceCode .python} +@aiohttp_csrf.csrf_exempt +async def handler_post_not_check(request): + ... +``` + +### Generate token + +For generate token you need to call `aiohttp_csrf.generate_token` in your handler: + +``` {.sourceCode .python} +@aiohttp_csrf.csrf_protect +async def handler_get(request): + token = await aiohttp_csrf.generate_token(request) + ... +``` + +Advanced usage +-------------- + +### Policies + +You can use different policies for check tokens. Library provides 3 types of policy: + +- **FormPolicy**. This policy will search token in the body of your POST request (Usually use for forms) or as a GET + variable of the same name. You need to specify name of field that will be checked. +- **HeaderPolicy**. This policy will search token in headers of your POST request (Usually use for AJAX requests). You + need to specify name of header that will be checked. +- **FormAndHeaderPolicy**. This policy combines behavior of **FormPolicy** and **HeaderPolicy**. + +You can implement your custom policies if needed. But make sure that your custom policy +implements `aiohttp_csrf.policy.AbstractPolicy` interface. + +### Storages + +You can use different types of storages for storing token. Library provides 2 types of storage: + +- **CookieStorage**. Your token will be stored in cookie variable. You need to specify cookie name. +- **SessionStorage**. Your token will be stored in session. You need to specify session variable name. + +**Important:** If you want to use session storage, you need setup aiohttp\_session in your +application ([session storage example](demo/session_storage.py#L22)) + +You can implement your custom storages if needed. But make sure that your custom storage +implements `aiohttp_csrf.storage.AbstractStorage` interface. + +### Token generators + +You can use different token generator in your application. By default storages +using `aiohttp_csrf.token_generator.SimpleTokenGenerator` + +But if you need more secure token generator - you can use `aiohttp_csrf.token_generator.HashedTokenGenerator` + +And you can implement your custom token generators if needed. But make sure that your custom token generator +implements `aiohttp_csrf.token_generator.AbstractTokenGenerator` interface. + +### Invalid token behavior + +By default, if token is invalid, `aiohttp_csrf` will raise `aiohttp.web.HTTPForbidden` exception. + +You have ability to specify your custom error handler. It can be: + +- **callable instance. Input parameter - aiohttp request.** + +``` {.sourceCode .python} +def custom_error_handler(request): + # do something + return aiohttp.web.Response(status=403) + +# or + +async def custom_async_error_handler(request): + # await do something + return aiohttp.web.Response(status=403) +``` + +It will be called instead of protected handler. + +- **sub class of Exception**. In this case this Exception will be raised. + +``` {.sourceCode .python} +class CustomException(Exception): + pass +``` + +You can specify custom error handler globally, when initialize `aiohttp_csrf` in your application: + +``` {.sourceCode .python} +... +class CustomException(Exception): + pass + +... +aiohttp_csrf.setup(app, policy=csrf_policy, storage=csrf_storage, error_renderer=CustomException) +... +``` + +In this case custom error handler will be applied to all protected handlers. + +Or you can specify custom error handler locally, for specific handler: + +``` {.sourceCode .python} +... +class CustomException(Exception): + pass + +... +@aiohttp_csrf.csrf_protect(error_renderer=CustomException) +def handler_with_custom_csrf_error(request): + ... +``` + +In this case custom error handler will be applied to this handler only. For all other handlers will be applied global +error handler. diff --git a/README.rst b/README.rst index dbe7c77..efa3780 100644 --- a/README.rst +++ b/README.rst @@ -3,6 +3,17 @@ aiohttp_csrf The library provides csrf (xsrf) protection for `aiohttp.web`__. +The library provides csrf (xsrf) protection for [aiohttp.web](https://docs.aiohttp.org/en/latest/web.html). + +**note:** The package [aiohttp-csrf-fixed](https://pypi.org/project/aiohttp-csrf-fixed) is aiohttp_csrf 0.0.2 + +[this commit](https://github.com/oplik0/aiohttp-csrf/commit/b1bd9207f43a2abf30e32e72ecdb10983a251823). The maintainer +didn't submit a PR so I just saw it by chance. I haven't had time to closely examine it but I think it's just removing +the HTTP security error that happens if no CSRF is provided. Why do that? An HTTP error is good because it tells the +client what happened and lets you handle it by middleware. + +Since they bumped to 0.0.3, I'm skipping that version and +going to 0.0.4 then merging their repo to a new branch. + .. _aiohttp_web: https://docs.aiohttp.org/en/latest/web.html __ aiohttp_web_ diff --git a/aiohttp_csrf/token_generator.py b/aiohttp_csrf/token_generator.py index dcecbb7..364f66c 100644 --- a/aiohttp_csrf/token_generator.py +++ b/aiohttp_csrf/token_generator.py @@ -1,5 +1,5 @@ import abc -import hashlib +from blake3 import blake3 import uuid @@ -25,6 +25,6 @@ def generate(self): token += self.secret_phrase - hasher = hashlib.sha256(token.encode(self.encoding)) + hasher = blake3(token.encode(self.encoding)) return hasher.hexdigest() diff --git a/gensetup.sh b/gensetup.sh new file mode 100644 index 0000000..0db24d0 --- /dev/null +++ b/gensetup.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +tar -xvf $1 --wildcards --no-anchored '*/setup.py' --strip=1 diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..8ba51a3 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,993 @@ +[[package]] +name = "aiohttp" +version = "3.7.3" +description = "Async http client/server framework (asyncio)" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.extras] +speedups = ["aiodns", "brotlipy", "cchardet"] + +[package.dependencies] +async-timeout = ">=3.0,<4.0" +attrs = ">=17.3.0" +chardet = ">=2.0,<4.0" +multidict = ">=4.5,<7.0" +typing-extensions = ">=3.6.5" +yarl = ">=1.0,<2.0" + +[[package]] +name = "aiohttp-session" +version = "2.9.0" +description = "sessions for aiohttp.web" +category = "main" +optional = false +python-versions = ">=3.5" + +[package.extras] +aiomcache = ["aiomcache (>=0.5.2)"] +aioredis = ["aioredis (>=1.0.0)"] +pycrypto = ["cryptography"] +pynacl = ["pynacl"] +secure = ["cryptography"] + +[package.dependencies] +aiohttp = ">=3.0.1" + +[package.source] +url = "https://github.com/TheDoctorAI/aiohttp-session" +reference = "f8062f5d89ed0989e7df9f22aca0c83168245416" +type = "git" + +[[package]] +name = "aioredis" +version = "1.3.1" +description = "asyncio (PEP 3156) Redis support" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +async-timeout = "*" +hiredis = "*" + +[[package]] +name = "appdirs" +version = "1.4.4" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "appnope" +version = "0.1.2" +description = "Disable App Nap on macOS >= 10.9" +category = "dev" +optional = false +python-versions = "*" +marker = "python_version >= \"3.4\" and sys_platform == \"darwin\"" + +[[package]] +name = "async-timeout" +version = "3.0.1" +description = "Timeout context manager for asyncio programs" +category = "main" +optional = false +python-versions = ">=3.5.3" + +[[package]] +name = "atomicwrites" +version = "1.4.0" +description = "Atomic file writes." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +marker = "sys_platform == \"win32\"" + +[[package]] +name = "attrs" +version = "20.3.0" +description = "Classes Without Boilerplate" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.extras] +dev = ["coverage (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "furo", "sphinx", "pre-commit"] +docs = ["furo", "sphinx", "zope.interface"] +tests = ["coverage (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] +tests_no_zope = ["coverage (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six"] + +[[package]] +name = "backcall" +version = "0.2.0" +description = "Specifications for callback functions passed in to an API" +category = "dev" +optional = false +python-versions = "*" +marker = "python_version >= \"3.4\"" + +[[package]] +name = "blake3" +version = "0.1.8" +description = "Python bindings for the Rust blake3 crate" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "chardet" +version = "3.0.4" +description = "Universal encoding detector for Python 2 and 3" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "colorama" +version = "0.4.4" +description = "Cross-platform colored terminal text." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +marker = "python_version >= \"3.4\" and sys_platform == \"win32\" or sys_platform == \"win32\" or platform_system == \"Windows\"" + +[[package]] +name = "coverage" +version = "5.3.1" +description = "Code coverage measurement for Python" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" + +[package.extras] +toml = ["toml"] + +[[package]] +name = "decorator" +version = "4.4.2" +description = "Decorators for Humans" +category = "dev" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*" +marker = "python_version >= \"3.4\"" + +[[package]] +name = "distlib" +version = "0.3.1" +description = "Distribution utilities" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "filelock" +version = "3.0.12" +description = "A platform independent file lock." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "flake8" +version = "3.8.4" +description = "the modular source code checker: pep8 pyflakes and co" +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" + +[package.dependencies] +mccabe = ">=0.6.0,<0.7.0" +pycodestyle = ">=2.6.0a1,<2.7.0" +pyflakes = ">=2.2.0,<2.3.0" + +[[package]] +name = "hiredis" +version = "1.1.0" +description = "Python wrapper for hiredis" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "idna" +version = "2.10" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "iniconfig" +version = "1.1.1" +description = "iniconfig: brain-dead simple config-ini parsing" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "ipdb" +version = "0.13.4" +description = "IPython-enabled pdb" +category = "dev" +optional = false +python-versions = ">=2.7" + +[package.dependencies] +setuptools = "*" + +[package.dependencies.ipython] +version = ">=5.1.0" +python = ">=3.4" + +[[package]] +name = "ipython" +version = "7.19.0" +description = "IPython: Productive Interactive Computing" +category = "dev" +optional = false +python-versions = ">=3.7" +marker = "python_version >= \"3.4\"" + +[package.extras] +all = ["Sphinx (>=1.3)", "ipykernel", "ipyparallel", "ipywidgets", "nbconvert", "nbformat", "nose (>=0.10.1)", "notebook", "numpy (>=1.14)", "pygments", "qtconsole", "requests", "testpath"] +doc = ["Sphinx (>=1.3)"] +kernel = ["ipykernel"] +nbconvert = ["nbconvert"] +nbformat = ["nbformat"] +notebook = ["notebook", "ipywidgets"] +parallel = ["ipyparallel"] +qtconsole = ["qtconsole"] +test = ["nose (>=0.10.1)", "requests", "testpath", "pygments", "nbformat", "ipykernel", "numpy (>=1.14)"] + +[package.dependencies] +appnope = "*" +backcall = "*" +colorama = "*" +decorator = "*" +jedi = ">=0.10" +pexpect = ">4.3" +pickleshare = "*" +prompt-toolkit = ">=2.0.0,<3.0.0 || >3.0.0,<3.0.1 || >3.0.1,<3.1.0" +pygments = "*" +setuptools = ">=18.5" +traitlets = ">=4.2" + +[[package]] +name = "ipython-genutils" +version = "0.2.0" +description = "Vestigial utilities from IPython" +category = "dev" +optional = false +python-versions = "*" +marker = "python_version >= \"3.4\"" + +[[package]] +name = "isort" +version = "4.3.21" +description = "A Python utility / library to sort Python imports." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.extras] +pipfile = ["pipreqs", "requirementslib"] +pyproject = ["toml"] +requirements = ["pipreqs", "pip-api"] +xdg_home = ["appdirs (>=1.4.0)"] + +[[package]] +name = "jedi" +version = "0.17.2" +description = "An autocompletion tool for Python that can be used for text editors." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +marker = "python_version >= \"3.4\"" + +[package.extras] +qa = ["flake8 (3.7.9)"] +testing = ["Django (<3.1)", "colorama", "docopt", "pytest (>=3.9.0,<5.0.0)"] + +[package.dependencies] +parso = ">=0.7.0,<0.8.0" + +[[package]] +name = "mccabe" +version = "0.6.1" +description = "McCabe checker, plugin for flake8" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "multidict" +version = "5.1.0" +description = "multidict implementation" +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "packaging" +version = "20.8" +description = "Core utilities for Python packages" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.dependencies] +pyparsing = ">=2.0.2" + +[[package]] +name = "parso" +version = "0.7.1" +description = "A Python Parser" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +marker = "python_version >= \"3.4\"" + +[package.extras] +testing = ["docopt", "pytest (>=3.0.7)"] + +[[package]] +name = "pexpect" +version = "4.8.0" +description = "Pexpect allows easy control of interactive console applications." +category = "dev" +optional = false +python-versions = "*" +marker = "python_version >= \"3.4\" and sys_platform != \"win32\"" + +[package.dependencies] +ptyprocess = ">=0.5" + +[[package]] +name = "pickleshare" +version = "0.7.5" +description = "Tiny 'shelve'-like database with concurrency support" +category = "dev" +optional = false +python-versions = "*" +marker = "python_version >= \"3.4\"" + +[[package]] +name = "pluggy" +version = "0.13.1" +description = "plugin and hook calling mechanisms for python" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.extras] +dev = ["pre-commit", "tox"] + +[[package]] +name = "prompt-toolkit" +version = "3.0.8" +description = "Library for building powerful interactive command lines in Python" +category = "dev" +optional = false +python-versions = ">=3.6.1" +marker = "python_version >= \"3.4\"" + +[package.dependencies] +wcwidth = "*" + +[[package]] +name = "ptyprocess" +version = "0.6.0" +description = "Run a subprocess in a pseudo terminal" +category = "dev" +optional = false +python-versions = "*" +marker = "python_version >= \"3.4\" and sys_platform != \"win32\"" + +[[package]] +name = "py" +version = "1.10.0" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "pycodestyle" +version = "2.6.0" +description = "Python style guide checker" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "pyflakes" +version = "2.2.0" +description = "passive checker of Python programs" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "pygments" +version = "2.7.3" +description = "Pygments is a syntax highlighting package written in Python." +category = "dev" +optional = false +python-versions = ">=3.5" +marker = "python_version >= \"3.4\"" + +[[package]] +name = "pyparsing" +version = "2.4.7" +description = "Python parsing module" +category = "dev" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "pytest" +version = "6.2.1" +description = "pytest: simple powerful testing with Python" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] + +[package.dependencies] +atomicwrites = ">=1.0" +attrs = ">=19.2.0" +colorama = "*" +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<1.0.0a1" +py = ">=1.8.2" +toml = "*" + +[[package]] +name = "pytest-aiohttp" +version = "0.3.0" +description = "pytest plugin for aiohttp support" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +aiohttp = ">=2.3.5" +pytest = "*" + +[[package]] +name = "pytest-cov" +version = "2.10.1" +description = "Pytest plugin for measuring coverage." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.extras] +testing = ["fields", "hunter", "process-tests (2.0.2)", "six", "pytest-xdist", "virtualenv"] + +[package.dependencies] +coverage = ">=4.4" +pytest = ">=4.6" + +[[package]] +name = "six" +version = "1.15.0" +description = "Python 2 and 3 compatibility utilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +category = "dev" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "tox" +version = "3.20.1" +description = "tox is a generic virtualenv management and test command line tool" +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" + +[package.extras] +docs = ["pygments-github-lexers (>=0.0.5)", "sphinx (>=2.0.0)", "sphinxcontrib-autoprogram (>=0.1.5)", "towncrier (>=18.5.0)"] +testing = ["flaky (>=3.4.0)", "freezegun (>=0.3.11)", "pathlib2 (>=2.3.3)", "psutil (>=5.6.1)", "pytest (>=4.0.0)", "pytest-cov (>=2.5.1)", "pytest-mock (>=1.10.0)", "pytest-randomly (>=1.0.0)", "pytest-xdist (>=1.22.2)"] + +[package.dependencies] +colorama = ">=0.4.1" +filelock = ">=3.0.0" +packaging = ">=14" +pluggy = ">=0.12.0" +py = ">=1.4.17" +six = ">=1.14.0" +toml = ">=0.9.4" +virtualenv = ">=16.0.0,<20.0.0 || >20.0.0,<20.0.1 || >20.0.1,<20.0.2 || >20.0.2,<20.0.3 || >20.0.3,<20.0.4 || >20.0.4,<20.0.5 || >20.0.5,<20.0.6 || >20.0.6,<20.0.7 || >20.0.7" + +[[package]] +name = "traitlets" +version = "5.0.5" +description = "Traitlets Python configuration system" +category = "dev" +optional = false +python-versions = ">=3.7" +marker = "python_version >= \"3.4\"" + +[package.extras] +test = ["pytest"] + +[package.dependencies] +ipython-genutils = "*" + +[[package]] +name = "typing-extensions" +version = "3.7.4.3" +description = "Backported and Experimental Type Hints for Python 3.5+" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "virtualenv" +version = "20.2.2" +description = "Virtual Python Environment builder" +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" + +[package.extras] +docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=19.9.0rc1)"] +testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "pytest-xdist (>=1.31.0)", "packaging (>=20.0)", "xonsh (>=0.9.16)"] + +[package.dependencies] +appdirs = ">=1.4.3,<2" +distlib = ">=0.3.1,<1" +filelock = ">=3.0.0,<4" +six = ">=1.9.0,<2" + +[[package]] +name = "wcwidth" +version = "0.2.5" +description = "Measures the displayed width of unicode strings in a terminal" +category = "dev" +optional = false +python-versions = "*" +marker = "python_version >= \"3.4\"" + +[[package]] +name = "yarl" +version = "1.6.3" +description = "Yet another URL library" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +idna = ">=2.0" +multidict = ">=4.0" + +[metadata] +python-versions = ">=3.8.3, <4" +content-hash = "292cc9903afc09a3f816bbbef2b9f4d1cfc37577470aaa4eded47442d7064907" + +[metadata.files] +aiohttp = [ + {file = "aiohttp-3.7.3-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:328b552513d4f95b0a2eea4c8573e112866107227661834652a8984766aa7656"}, + {file = "aiohttp-3.7.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:c733ef3bdcfe52a1a75564389bad4064352274036e7e234730526d155f04d914"}, + {file = "aiohttp-3.7.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:2858b2504c8697beb9357be01dc47ef86438cc1cb36ecb6991796d19475faa3e"}, + {file = "aiohttp-3.7.3-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:d2cfac21e31e841d60dc28c0ec7d4ec47a35c608cb8906435d47ef83ffb22150"}, + {file = "aiohttp-3.7.3-cp36-cp36m-manylinux2014_ppc64le.whl", hash = "sha256:3228b7a51e3ed533f5472f54f70fd0b0a64c48dc1649a0f0e809bec312934d7a"}, + {file = "aiohttp-3.7.3-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:dcc119db14757b0c7bce64042158307b9b1c76471e655751a61b57f5a0e4d78e"}, + {file = "aiohttp-3.7.3-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:7d9b42127a6c0bdcc25c3dcf252bb3ddc70454fac593b1b6933ae091396deb13"}, + {file = "aiohttp-3.7.3-cp36-cp36m-win32.whl", hash = "sha256:df48a623c58180874d7407b4d9ec06a19b84ed47f60a3884345b1a5099c1818b"}, + {file = "aiohttp-3.7.3-cp36-cp36m-win_amd64.whl", hash = "sha256:0b795072bb1bf87b8620120a6373a3c61bfcb8da7e5c2377f4bb23ff4f0b62c9"}, + {file = "aiohttp-3.7.3-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:0d438c8ca703b1b714e82ed5b7a4412c82577040dadff479c08405e2a715564f"}, + {file = "aiohttp-3.7.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:8389d6044ee4e2037dca83e3f6994738550f6ee8cfb746762283fad9b932868f"}, + {file = "aiohttp-3.7.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:3ea8c252d8df5e9166bcf3d9edced2af132f4ead8ac422eac723c5781063709a"}, + {file = "aiohttp-3.7.3-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:78e2f18a82b88cbc37d22365cf8d2b879a492faedb3f2975adb4ed8dfe994d3a"}, + {file = "aiohttp-3.7.3-cp37-cp37m-manylinux2014_ppc64le.whl", hash = "sha256:df3a7b258cc230a65245167a202dd07320a5af05f3d41da1488ba0fa05bc9347"}, + {file = "aiohttp-3.7.3-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:f326b3c1bbfda5b9308252ee0dcb30b612ee92b0e105d4abec70335fab5b1245"}, + {file = "aiohttp-3.7.3-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:5e479df4b2d0f8f02133b7e4430098699450e1b2a826438af6bec9a400530957"}, + {file = "aiohttp-3.7.3-cp37-cp37m-win32.whl", hash = "sha256:6d42debaf55450643146fabe4b6817bb2a55b23698b0434107e892a43117285e"}, + {file = "aiohttp-3.7.3-cp37-cp37m-win_amd64.whl", hash = "sha256:c9c58b0b84055d8bc27b7df5a9d141df4ee6ff59821f922dd73155861282f6a3"}, + {file = "aiohttp-3.7.3-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:f411cb22115cb15452d099fec0ee636b06cf81bfb40ed9c02d30c8dc2bc2e3d1"}, + {file = "aiohttp-3.7.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:c1e0920909d916d3375c7a1fdb0b1c78e46170e8bb42792312b6eb6676b2f87f"}, + {file = "aiohttp-3.7.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:59d11674964b74a81b149d4ceaff2b674b3b0e4d0f10f0be1533e49c4a28408b"}, + {file = "aiohttp-3.7.3-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:41608c0acbe0899c852281978492f9ce2c6fbfaf60aff0cefc54a7c4516b822c"}, + {file = "aiohttp-3.7.3-cp38-cp38-manylinux2014_ppc64le.whl", hash = "sha256:16a3cb5df5c56f696234ea9e65e227d1ebe9c18aa774d36ff42f532139066a5f"}, + {file = "aiohttp-3.7.3-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:6ccc43d68b81c424e46192a778f97da94ee0630337c9bbe5b2ecc9b0c1c59001"}, + {file = "aiohttp-3.7.3-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:d03abec50df423b026a5aa09656bd9d37f1e6a49271f123f31f9b8aed5dc3ea3"}, + {file = "aiohttp-3.7.3-cp38-cp38-win32.whl", hash = "sha256:39f4b0a6ae22a1c567cb0630c30dd082481f95c13ca528dc501a7766b9c718c0"}, + {file = "aiohttp-3.7.3-cp38-cp38-win_amd64.whl", hash = "sha256:c68fdf21c6f3573ae19c7ee65f9ff185649a060c9a06535e9c3a0ee0bbac9235"}, + {file = "aiohttp-3.7.3-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:710376bf67d8ff4500a31d0c207b8941ff4fba5de6890a701d71680474fe2a60"}, + {file = "aiohttp-3.7.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:2406dc1dda01c7f6060ab586e4601f18affb7a6b965c50a8c90ff07569cf782a"}, + {file = "aiohttp-3.7.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:2a7b7640167ab536c3cb90cfc3977c7094f1c5890d7eeede8b273c175c3910fd"}, + {file = "aiohttp-3.7.3-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:684850fb1e3e55c9220aad007f8386d8e3e477c4ec9211ae54d968ecdca8c6f9"}, + {file = "aiohttp-3.7.3-cp39-cp39-manylinux2014_ppc64le.whl", hash = "sha256:1edfd82a98c5161497bbb111b2b70c0813102ad7e0aa81cbeb34e64c93863005"}, + {file = "aiohttp-3.7.3-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:77149002d9386fae303a4a162e6bce75cc2161347ad2ba06c2f0182561875d45"}, + {file = "aiohttp-3.7.3-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:756ae7efddd68d4ea7d89c636b703e14a0c686688d42f588b90778a3c2fc0564"}, + {file = "aiohttp-3.7.3-cp39-cp39-win32.whl", hash = "sha256:3b0036c978cbcc4a4512278e98e3e6d9e6b834dc973206162eddf98b586ef1c6"}, + {file = "aiohttp-3.7.3-cp39-cp39-win_amd64.whl", hash = "sha256:e1b95972a0ae3f248a899cdbac92ba2e01d731225f566569311043ce2226f5e7"}, + {file = "aiohttp-3.7.3.tar.gz", hash = "sha256:9c1a81af067e72261c9cbe33ea792893e83bc6aa987bfbd6fdc1e5e7b22777c4"}, +] +aiohttp-session = [] +aioredis = [ + {file = "aioredis-1.3.1-py3-none-any.whl", hash = "sha256:b61808d7e97b7cd5a92ed574937a079c9387fdadd22bfbfa7ad2fd319ecc26e3"}, + {file = "aioredis-1.3.1.tar.gz", hash = "sha256:15f8af30b044c771aee6787e5ec24694c048184c7b9e54c3b60c750a4b93273a"}, +] +appdirs = [ + {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, + {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, +] +appnope = [ + {file = "appnope-0.1.2-py2.py3-none-any.whl", hash = "sha256:93aa393e9d6c54c5cd570ccadd8edad61ea0c4b9ea7a01409020c9aa019eb442"}, + {file = "appnope-0.1.2.tar.gz", hash = "sha256:dd83cd4b5b460958838f6eb3000c660b1f9caf2a5b1de4264e941512f603258a"}, +] +async-timeout = [ + {file = "async-timeout-3.0.1.tar.gz", hash = "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f"}, + {file = "async_timeout-3.0.1-py3-none-any.whl", hash = "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"}, +] +atomicwrites = [ + {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, + {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, +] +attrs = [ + {file = "attrs-20.3.0-py2.py3-none-any.whl", hash = "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6"}, + {file = "attrs-20.3.0.tar.gz", hash = "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"}, +] +backcall = [ + {file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"}, + {file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"}, +] +blake3 = [ + {file = "blake3-0.1.8-cp35-cp35m-macosx_10_7_x86_64.whl", hash = "sha256:71f1a49ca7b8b5cbefcac64cfb23d432493e4ae9e4ed421b1834484815ccba2e"}, + {file = "blake3-0.1.8-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:62198369bd794087882216db94fb1deb2ad8144d3ea5ac5c8c200a5b7c2180bf"}, + {file = "blake3-0.1.8-cp35-none-win32.whl", hash = "sha256:5b3f48ae9adc3d6bfc97f3a3aebd8f27a579505e5453e05a2f8ee63fb81eb975"}, + {file = "blake3-0.1.8-cp35-none-win_amd64.whl", hash = "sha256:8b6f925b454d58a194deed54f15d24131da45dfdd21714f103a33b0ffbe3e318"}, + {file = "blake3-0.1.8-cp36-cp36m-macosx_10_7_x86_64.whl", hash = "sha256:d1af4a89e3755e78d95e54c05c1c967d2bc0da1939b5bc034766fa5131f35fc0"}, + {file = "blake3-0.1.8-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:28d02decd14bbbc65e0f04bf8c9b389f31c53e4cc3685cfbb5f7ba3e123e7670"}, + {file = "blake3-0.1.8-cp36-none-win32.whl", hash = "sha256:f6d34840dc0c8b2a9c920a91db1e9c4917c4ff156af42f247f87fa85f19850f1"}, + {file = "blake3-0.1.8-cp36-none-win_amd64.whl", hash = "sha256:1b58114cd1cc849c0af6e63e8b543c89f5c5804a34ec61b82d8baaffa4e11d94"}, + {file = "blake3-0.1.8-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:8c174da153b739e2aa46d362a11a5d224420e50e29f32d1ba0b3220babbbc22e"}, + {file = "blake3-0.1.8-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:897717f157b6d9f7fd1670cd0b07bb58e761f3147b1a0e5e542412210f581f02"}, + {file = "blake3-0.1.8-cp37-none-win32.whl", hash = "sha256:3c94995ea9477200e438451d42ddfedc210c596f166415068ed87e6db2abfa03"}, + {file = "blake3-0.1.8-cp37-none-win_amd64.whl", hash = "sha256:891fa7fd3062cc0c59b0458e0ef971f6c65ab5a54b8b4efd99901b47c05a7de4"}, + {file = "blake3-0.1.8-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:5f0f2c9ec12175c54f593a55b49e467a1fa8839db9087b11a6297b2afe6c8c25"}, + {file = "blake3-0.1.8-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:a2cbeeda01fee7d71e1198eb2b9a7dbca53b1bf4ecdf69bf8f65b4ef7aeb3642"}, + {file = "blake3-0.1.8-cp38-none-win32.whl", hash = "sha256:494cbc6d3ec0da44e196cbe1dbfd7dd01cd1ba32420c19d53e47bbc909921654"}, + {file = "blake3-0.1.8-cp38-none-win_amd64.whl", hash = "sha256:5422f98d49afb3a89f0c2e56045275148b63370ff8aa25357ee4739b34f5c8a9"}, + {file = "blake3-0.1.8-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:13f460849ed4f399d53129353723524c0ac5b67e3bed7c50152e57e20deb54ff"}, + {file = "blake3-0.1.8-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:9a97aba70bcc131d9b4f059a7a295717ec434a3a82b84290e86b95cfa61c9272"}, + {file = "blake3-0.1.8-cp39-none-win32.whl", hash = "sha256:b70c0d157fe12ca3e43c630da86afd2122be206b5ad6cd29bdf8660be7e03656"}, + {file = "blake3-0.1.8-cp39-none-win_amd64.whl", hash = "sha256:c5d1cd1218089e105f75b5472878bd7cabfaad13f83c5511dab326858fff9890"}, + {file = "blake3-0.1.8.tar.gz", hash = "sha256:b131129196ac4242bc9127a425daad46a8e7a451daef21a9337fcec17db445a8"}, +] +chardet = [ + {file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"}, + {file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"}, +] +colorama = [ + {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, +] +coverage = [ + {file = "coverage-5.3.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:fabeeb121735d47d8eab8671b6b031ce08514c86b7ad8f7d5490a7b6dcd6267d"}, + {file = "coverage-5.3.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:7e4d159021c2029b958b2363abec4a11db0ce8cd43abb0d9ce44284cb97217e7"}, + {file = "coverage-5.3.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:378ac77af41350a8c6b8801a66021b52da8a05fd77e578b7380e876c0ce4f528"}, + {file = "coverage-5.3.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:e448f56cfeae7b1b3b5bcd99bb377cde7c4eb1970a525c770720a352bc4c8044"}, + {file = "coverage-5.3.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:cc44e3545d908ecf3e5773266c487ad1877be718d9dc65fc7eb6e7d14960985b"}, + {file = "coverage-5.3.1-cp27-cp27m-win32.whl", hash = "sha256:08b3ba72bd981531fd557f67beee376d6700fba183b167857038997ba30dd297"}, + {file = "coverage-5.3.1-cp27-cp27m-win_amd64.whl", hash = "sha256:8dacc4073c359f40fcf73aede8428c35f84639baad7e1b46fce5ab7a8a7be4bb"}, + {file = "coverage-5.3.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:ee2f1d1c223c3d2c24e3afbb2dd38be3f03b1a8d6a83ee3d9eb8c36a52bee899"}, + {file = "coverage-5.3.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:9a9d4ff06804920388aab69c5ea8a77525cf165356db70131616acd269e19b36"}, + {file = "coverage-5.3.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:782a5c7df9f91979a7a21792e09b34a658058896628217ae6362088b123c8500"}, + {file = "coverage-5.3.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:fda29412a66099af6d6de0baa6bd7c52674de177ec2ad2630ca264142d69c6c7"}, + {file = "coverage-5.3.1-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:f2c6888eada180814b8583c3e793f3f343a692fc802546eed45f40a001b1169f"}, + {file = "coverage-5.3.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:8f33d1156241c43755137288dea619105477961cfa7e47f48dbf96bc2c30720b"}, + {file = "coverage-5.3.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:b239711e774c8eb910e9b1ac719f02f5ae4bf35fa0420f438cdc3a7e4e7dd6ec"}, + {file = "coverage-5.3.1-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:f54de00baf200b4539a5a092a759f000b5f45fd226d6d25a76b0dff71177a714"}, + {file = "coverage-5.3.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:be0416074d7f253865bb67630cf7210cbc14eb05f4099cc0f82430135aaa7a3b"}, + {file = "coverage-5.3.1-cp35-cp35m-win32.whl", hash = "sha256:c46643970dff9f5c976c6512fd35768c4a3819f01f61169d8cdac3f9290903b7"}, + {file = "coverage-5.3.1-cp35-cp35m-win_amd64.whl", hash = "sha256:9a4f66259bdd6964d8cf26142733c81fb562252db74ea367d9beb4f815478e72"}, + {file = "coverage-5.3.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c6e5174f8ca585755988bc278c8bb5d02d9dc2e971591ef4a1baabdf2d99589b"}, + {file = "coverage-5.3.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:3911c2ef96e5ddc748a3c8b4702c61986628bb719b8378bf1e4a6184bbd48fe4"}, + {file = "coverage-5.3.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:c5ec71fd4a43b6d84ddb88c1df94572479d9a26ef3f150cef3dacefecf888105"}, + {file = "coverage-5.3.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f51dbba78d68a44e99d484ca8c8f604f17e957c1ca09c3ebc2c7e3bbd9ba0448"}, + {file = "coverage-5.3.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:a2070c5affdb3a5e751f24208c5c4f3d5f008fa04d28731416e023c93b275277"}, + {file = "coverage-5.3.1-cp36-cp36m-win32.whl", hash = "sha256:535dc1e6e68fad5355f9984d5637c33badbdc987b0c0d303ee95a6c979c9516f"}, + {file = "coverage-5.3.1-cp36-cp36m-win_amd64.whl", hash = "sha256:a4857f7e2bc6921dbd487c5c88b84f5633de3e7d416c4dc0bb70256775551a6c"}, + {file = "coverage-5.3.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fac3c432851038b3e6afe086f777732bcf7f6ebbfd90951fa04ee53db6d0bcdd"}, + {file = "coverage-5.3.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:cd556c79ad665faeae28020a0ab3bda6cd47d94bec48e36970719b0b86e4dcf4"}, + {file = "coverage-5.3.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:a66ca3bdf21c653e47f726ca57f46ba7fc1f260ad99ba783acc3e58e3ebdb9ff"}, + {file = "coverage-5.3.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:ab110c48bc3d97b4d19af41865e14531f300b482da21783fdaacd159251890e8"}, + {file = "coverage-5.3.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:e52d3d95df81c8f6b2a1685aabffadf2d2d9ad97203a40f8d61e51b70f191e4e"}, + {file = "coverage-5.3.1-cp37-cp37m-win32.whl", hash = "sha256:fa10fee7e32213f5c7b0d6428ea92e3a3fdd6d725590238a3f92c0de1c78b9d2"}, + {file = "coverage-5.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:ce6f3a147b4b1a8b09aae48517ae91139b1b010c5f36423fa2b866a8b23df879"}, + {file = "coverage-5.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:93a280c9eb736a0dcca19296f3c30c720cb41a71b1f9e617f341f0a8e791a69b"}, + {file = "coverage-5.3.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:3102bb2c206700a7d28181dbe04d66b30780cde1d1c02c5f3c165cf3d2489497"}, + {file = "coverage-5.3.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8ffd4b204d7de77b5dd558cdff986a8274796a1e57813ed005b33fd97e29f059"}, + {file = "coverage-5.3.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:a607ae05b6c96057ba86c811d9c43423f35e03874ffb03fbdcd45e0637e8b631"}, + {file = "coverage-5.3.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:3a3c3f8863255f3c31db3889f8055989527173ef6192a283eb6f4db3c579d830"}, + {file = "coverage-5.3.1-cp38-cp38-win32.whl", hash = "sha256:ff1330e8bc996570221b450e2d539134baa9465f5cb98aff0e0f73f34172e0ae"}, + {file = "coverage-5.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:3498b27d8236057def41de3585f317abae235dd3a11d33e01736ffedb2ef8606"}, + {file = "coverage-5.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ceb499d2b3d1d7b7ba23abe8bf26df5f06ba8c71127f188333dddcf356b4b63f"}, + {file = "coverage-5.3.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:3b14b1da110ea50c8bcbadc3b82c3933974dbeea1832e814aab93ca1163cd4c1"}, + {file = "coverage-5.3.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:76b2775dda7e78680d688daabcb485dc87cf5e3184a0b3e012e1d40e38527cc8"}, + {file = "coverage-5.3.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:cef06fb382557f66d81d804230c11ab292d94b840b3cb7bf4450778377b592f4"}, + {file = "coverage-5.3.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:6f61319e33222591f885c598e3e24f6a4be3533c1d70c19e0dc59e83a71ce27d"}, + {file = "coverage-5.3.1-cp39-cp39-win32.whl", hash = "sha256:cc6f8246e74dd210d7e2b56c76ceaba1cc52b025cd75dbe96eb48791e0250e98"}, + {file = "coverage-5.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:2757fa64e11ec12220968f65d086b7a29b6583d16e9a544c889b22ba98555ef1"}, + {file = "coverage-5.3.1-pp36-none-any.whl", hash = "sha256:723d22d324e7997a651478e9c5a3120a0ecbc9a7e94071f7e1954562a8806cf3"}, + {file = "coverage-5.3.1-pp37-none-any.whl", hash = "sha256:c89b558f8a9a5a6f2cfc923c304d49f0ce629c3bd85cb442ca258ec20366394c"}, + {file = "coverage-5.3.1.tar.gz", hash = "sha256:38f16b1317b8dd82df67ed5daa5f5e7c959e46579840d77a67a4ceb9cef0a50b"}, +] +decorator = [ + {file = "decorator-4.4.2-py2.py3-none-any.whl", hash = "sha256:41fa54c2a0cc4ba648be4fd43cff00aedf5b9465c9bf18d64325bc225f08f760"}, + {file = "decorator-4.4.2.tar.gz", hash = "sha256:e3a62f0520172440ca0dcc823749319382e377f37f140a0b99ef45fecb84bfe7"}, +] +distlib = [ + {file = "distlib-0.3.1-py2.py3-none-any.whl", hash = "sha256:8c09de2c67b3e7deef7184574fc060ab8a793e7adbb183d942c389c8b13c52fb"}, + {file = "distlib-0.3.1.zip", hash = "sha256:edf6116872c863e1aa9d5bb7cb5e05a022c519a4594dc703843343a9ddd9bff1"}, +] +filelock = [ + {file = "filelock-3.0.12-py3-none-any.whl", hash = "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"}, + {file = "filelock-3.0.12.tar.gz", hash = "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59"}, +] +flake8 = [ + {file = "flake8-3.8.4-py2.py3-none-any.whl", hash = "sha256:749dbbd6bfd0cf1318af27bf97a14e28e5ff548ef8e5b1566ccfb25a11e7c839"}, + {file = "flake8-3.8.4.tar.gz", hash = "sha256:aadae8761ec651813c24be05c6f7b4680857ef6afaae4651a4eccaef97ce6c3b"}, +] +hiredis = [ + {file = "hiredis-1.1.0-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:289b31885b4996ce04cadfd5fc03d034dce8e2a8234479f7c9e23b9e245db06b"}, + {file = "hiredis-1.1.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:7b0f63f10a166583ab744a58baad04e0f52cfea1ac27bfa1b0c21a48d1003c23"}, + {file = "hiredis-1.1.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:6996883a8a6ff9117cbb3d6f5b0dcbbae6fb9e31e1a3e4e2f95e0214d9a1c655"}, + {file = "hiredis-1.1.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:b33aea449e7f46738811fbc6f0b3177c6777a572207412bbbf6f525ffed001ae"}, + {file = "hiredis-1.1.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:8daecd778c1da45b8bd54fd41ffcd471a86beed3d8e57a43acf7a8d63bba4058"}, + {file = "hiredis-1.1.0-cp27-cp27m-win32.whl", hash = "sha256:e82d6b930e02e80e5109b678c663a9ed210680ded81c1abaf54635d88d1da298"}, + {file = "hiredis-1.1.0-cp27-cp27m-win_amd64.whl", hash = "sha256:d2c0caffa47606d6d7c8af94ba42547bd2a441f06c74fd90a1ffe328524a6c64"}, + {file = "hiredis-1.1.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:47bcf3c5e6c1e87ceb86cdda2ee983fa0fe56a999e6185099b3c93a223f2fa9b"}, + {file = "hiredis-1.1.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:dcb2db95e629962db5a355047fb8aefb012df6c8ae608930d391619dbd96fd86"}, + {file = "hiredis-1.1.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:7332d5c3e35154cd234fd79573736ddcf7a0ade7a986db35b6196b9171493e75"}, + {file = "hiredis-1.1.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:6c96f64a54f030366657a54bb90b3093afc9c16c8e0dfa29fc0d6dbe169103a5"}, + {file = "hiredis-1.1.0-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:b44f9421c4505c548435244d74037618f452844c5d3c67719d8a55e2613549da"}, + {file = "hiredis-1.1.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:abfb15a6a7822f0fae681785cb38860e7a2cb1616a708d53df557b3d76c5bfd4"}, + {file = "hiredis-1.1.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:89ebf69cb19a33d625db72d2ac589d26e936b8f7628531269accf4a3196e7872"}, + {file = "hiredis-1.1.0-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:5b1451727f02e7acbdf6aae4e06d75f66ee82966ff9114550381c3271a90f56c"}, + {file = "hiredis-1.1.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:7885b6f32c4a898e825bb7f56f36a02781ac4a951c63e4169f0afcf9c8c30dfb"}, + {file = "hiredis-1.1.0-cp35-cp35m-win32.whl", hash = "sha256:a04901757cb0fb0f5602ac11dda48f5510f94372144d06c2563ba56c480b467c"}, + {file = "hiredis-1.1.0-cp35-cp35m-win_amd64.whl", hash = "sha256:3bb9b63d319402cead8bbd9dd55dca3b667d2997e9a0d8a1f9b6cc274db4baee"}, + {file = "hiredis-1.1.0-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:e0eeb9c112fec2031927a1745788a181d0eecbacbed941fc5c4f7bc3f7b273bf"}, + {file = "hiredis-1.1.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:18402d9e54fb278cb9a8c638df6f1550aca36a009d47ecf5aa263a38600f35b0"}, + {file = "hiredis-1.1.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:cdfd501c7ac5b198c15df800a3a34c38345f5182e5f80770caf362bccca65628"}, + {file = "hiredis-1.1.0-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:43b8ed3dbfd9171e44c554cb4acf4ee4505caa84c5e341858b50ea27dd2b6e12"}, + {file = "hiredis-1.1.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:c2851deeabd96d3f6283e9c6b26e0bfed4de2dc6fb15edf913e78b79fc5909ed"}, + {file = "hiredis-1.1.0-cp36-cp36m-win32.whl", hash = "sha256:955ba8ea73cf3ed8bd2f963b4cb9f8f0dcb27becd2f4b3dd536fd24c45533454"}, + {file = "hiredis-1.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:5263db1e2e1e8ae30500cdd75a979ff99dcc184201e6b4b820d0de74834d2323"}, + {file = "hiredis-1.1.0-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:e154891263306200260d7f3051982774d7b9ef35af3509d5adbbe539afd2610c"}, + {file = "hiredis-1.1.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:964f18a59f5a64c0170f684c417f4fe3e695a536612e13074c4dd5d1c6d7c882"}, + {file = "hiredis-1.1.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:23344e3c2177baf6975fbfa361ed92eb7d36d08f454636e5054b3faa7c2aff8a"}, + {file = "hiredis-1.1.0-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:b27f082f47d23cffc4cf1388b84fdc45c4ef6015f906cd7e0d988d9e35d36349"}, + {file = "hiredis-1.1.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:aa0af2deb166a5e26e0d554b824605e660039b161e37ed4f01b8d04beec184f3"}, + {file = "hiredis-1.1.0-cp37-cp37m-win32.whl", hash = "sha256:819f95d4eba3f9e484dd115ab7ab72845cf766b84286a00d4ecf76d33f1edca1"}, + {file = "hiredis-1.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2c1c570ae7bf1bab304f29427e2475fe1856814312c4a1cf1cd0ee133f07a3c6"}, + {file = "hiredis-1.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9e9c9078a7ce07e6fce366bd818be89365a35d2e4b163268f0ca9ba7e13bb2f6"}, + {file = "hiredis-1.1.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:2c227c0ed371771ffda256034427320870e8ea2e4fd0c0a618c766e7c49aad73"}, + {file = "hiredis-1.1.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:0a909bf501459062aa1552be1461456518f367379fdc9fdb1f2ca5e4a1fdd7c0"}, + {file = "hiredis-1.1.0-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:1e4cbbc3858ec7e680006e5ca590d89a5e083235988f26a004acf7244389ac01"}, + {file = "hiredis-1.1.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:a7bf1492429f18d205f3a818da3ff1f242f60aa59006e53dee00b4ef592a3363"}, + {file = "hiredis-1.1.0-cp38-cp38-win32.whl", hash = "sha256:bcc371151d1512201d0214c36c0c150b1dc64f19c2b1a8c9cb1d7c7c15ebd93f"}, + {file = "hiredis-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:e64be68255234bb489a574c4f2f8df7029c98c81ec4d160d6cd836e7f0679390"}, + {file = "hiredis-1.1.0-pp27-pypy_73-manylinux1_x86_64.whl", hash = "sha256:8968eeaa4d37a38f8ca1f9dbe53526b69628edc9c42229a5b2f56d98bb828c1f"}, + {file = "hiredis-1.1.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:b253fe4df2afea4dfa6b1fa8c5fef212aff8bcaaeb4207e81eed05cb5e4a7919"}, + {file = "hiredis-1.1.0-pp27-pypy_73-win32.whl", hash = "sha256:969843fbdfbf56cdb71da6f0bdf50f9985b8b8aeb630102945306cf10a9c6af2"}, + {file = "hiredis-1.1.0-pp36-pypy36_pp73-manylinux1_x86_64.whl", hash = "sha256:e2e023a42dcbab8ed31f97c2bcdb980b7fbe0ada34037d87ba9d799664b58ded"}, + {file = "hiredis-1.1.0-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:06a039208f83744a702279b894c8cf24c14fd63c59cd917dcde168b79eef0680"}, + {file = "hiredis-1.1.0-pp36-pypy36_pp73-win32.whl", hash = "sha256:3ef2183de67b59930d2db8b8e8d4d58e00a50fcc5e92f4f678f6eed7a1c72d55"}, + {file = "hiredis-1.1.0.tar.gz", hash = "sha256:996021ef33e0f50b97ff2d6b5f422a0fe5577de21a8873b58a779a5ddd1c3132"}, +] +idna = [ + {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, + {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, +] +iniconfig = [ + {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, + {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, +] +ipdb = [ + {file = "ipdb-0.13.4.tar.gz", hash = "sha256:c85398b5fb82f82399fc38c44fe3532c0dde1754abee727d8f5cfcc74547b334"}, +] +ipython = [ + {file = "ipython-7.19.0-py3-none-any.whl", hash = "sha256:c987e8178ced651532b3b1ff9965925bfd445c279239697052561a9ab806d28f"}, + {file = "ipython-7.19.0.tar.gz", hash = "sha256:cbb2ef3d5961d44e6a963b9817d4ea4e1fa2eb589c371a470fed14d8d40cbd6a"}, +] +ipython-genutils = [ + {file = "ipython_genutils-0.2.0-py2.py3-none-any.whl", hash = "sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8"}, + {file = "ipython_genutils-0.2.0.tar.gz", hash = "sha256:eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8"}, +] +isort = [ + {file = "isort-4.3.21-py2.py3-none-any.whl", hash = "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd"}, + {file = "isort-4.3.21.tar.gz", hash = "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1"}, +] +jedi = [ + {file = "jedi-0.17.2-py2.py3-none-any.whl", hash = "sha256:98cc583fa0f2f8304968199b01b6b4b94f469a1f4a74c1560506ca2a211378b5"}, + {file = "jedi-0.17.2.tar.gz", hash = "sha256:86ed7d9b750603e4ba582ea8edc678657fb4007894a12bcf6f4bb97892f31d20"}, +] +mccabe = [ + {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, + {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, +] +multidict = [ + {file = "multidict-5.1.0-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:b7993704f1a4b204e71debe6095150d43b2ee6150fa4f44d6d966ec356a8d61f"}, + {file = "multidict-5.1.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:9dd6e9b1a913d096ac95d0399bd737e00f2af1e1594a787e00f7975778c8b2bf"}, + {file = "multidict-5.1.0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:f21756997ad8ef815d8ef3d34edd98804ab5ea337feedcd62fb52d22bf531281"}, + {file = "multidict-5.1.0-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:1ab820665e67373de5802acae069a6a05567ae234ddb129f31d290fc3d1aa56d"}, + {file = "multidict-5.1.0-cp36-cp36m-manylinux2014_ppc64le.whl", hash = "sha256:9436dc58c123f07b230383083855593550c4d301d2532045a17ccf6eca505f6d"}, + {file = "multidict-5.1.0-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:830f57206cc96ed0ccf68304141fec9481a096c4d2e2831f311bde1c404401da"}, + {file = "multidict-5.1.0-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:2e68965192c4ea61fff1b81c14ff712fc7dc15d2bd120602e4a3494ea6584224"}, + {file = "multidict-5.1.0-cp36-cp36m-win32.whl", hash = "sha256:2f1a132f1c88724674271d636e6b7351477c27722f2ed789f719f9e3545a3d26"}, + {file = "multidict-5.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:3a4f32116f8f72ecf2a29dabfb27b23ab7cdc0ba807e8459e59a93a9be9506f6"}, + {file = "multidict-5.1.0-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:46c73e09ad374a6d876c599f2328161bcd95e280f84d2060cf57991dec5cfe76"}, + {file = "multidict-5.1.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:018132dbd8688c7a69ad89c4a3f39ea2f9f33302ebe567a879da8f4ca73f0d0a"}, + {file = "multidict-5.1.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:4b186eb7d6ae7c06eb4392411189469e6a820da81447f46c0072a41c748ab73f"}, + {file = "multidict-5.1.0-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:3a041b76d13706b7fff23b9fc83117c7b8fe8d5fe9e6be45eee72b9baa75f348"}, + {file = "multidict-5.1.0-cp37-cp37m-manylinux2014_ppc64le.whl", hash = "sha256:051012ccee979b2b06be928a6150d237aec75dd6bf2d1eeeb190baf2b05abc93"}, + {file = "multidict-5.1.0-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:6a4d5ce640e37b0efcc8441caeea8f43a06addace2335bd11151bc02d2ee31f9"}, + {file = "multidict-5.1.0-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:5cf3443199b83ed9e955f511b5b241fd3ae004e3cb81c58ec10f4fe47c7dce37"}, + {file = "multidict-5.1.0-cp37-cp37m-win32.whl", hash = "sha256:f200755768dc19c6f4e2b672421e0ebb3dd54c38d5a4f262b872d8cfcc9e93b5"}, + {file = "multidict-5.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:05c20b68e512166fddba59a918773ba002fdd77800cad9f55b59790030bab632"}, + {file = "multidict-5.1.0-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:54fd1e83a184e19c598d5e70ba508196fd0bbdd676ce159feb412a4a6664f952"}, + {file = "multidict-5.1.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:0e3c84e6c67eba89c2dbcee08504ba8644ab4284863452450520dad8f1e89b79"}, + {file = "multidict-5.1.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:dc862056f76443a0db4509116c5cd480fe1b6a2d45512a653f9a855cc0517456"}, + {file = "multidict-5.1.0-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:0e929169f9c090dae0646a011c8b058e5e5fb391466016b39d21745b48817fd7"}, + {file = "multidict-5.1.0-cp38-cp38-manylinux2014_ppc64le.whl", hash = "sha256:d81eddcb12d608cc08081fa88d046c78afb1bf8107e6feab5d43503fea74a635"}, + {file = "multidict-5.1.0-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:585fd452dd7782130d112f7ddf3473ffdd521414674c33876187e101b588738a"}, + {file = "multidict-5.1.0-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:37e5438e1c78931df5d3c0c78ae049092877e5e9c02dd1ff5abb9cf27a5914ea"}, + {file = "multidict-5.1.0-cp38-cp38-win32.whl", hash = "sha256:07b42215124aedecc6083f1ce6b7e5ec5b50047afa701f3442054373a6deb656"}, + {file = "multidict-5.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:929006d3c2d923788ba153ad0de8ed2e5ed39fdbe8e7be21e2f22ed06c6783d3"}, + {file = "multidict-5.1.0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:b797515be8743b771aa868f83563f789bbd4b236659ba52243b735d80b29ed93"}, + {file = "multidict-5.1.0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:d5c65bdf4484872c4af3150aeebe101ba560dcfb34488d9a8ff8dbcd21079647"}, + {file = "multidict-5.1.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b47a43177a5e65b771b80db71e7be76c0ba23cc8aa73eeeb089ed5219cdbe27d"}, + {file = "multidict-5.1.0-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:806068d4f86cb06af37cd65821554f98240a19ce646d3cd24e1c33587f313eb8"}, + {file = "multidict-5.1.0-cp39-cp39-manylinux2014_ppc64le.whl", hash = "sha256:46dd362c2f045095c920162e9307de5ffd0a1bfbba0a6e990b344366f55a30c1"}, + {file = "multidict-5.1.0-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:ace010325c787c378afd7f7c1ac66b26313b3344628652eacd149bdd23c68841"}, + {file = "multidict-5.1.0-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:ecc771ab628ea281517e24fd2c52e8f31c41e66652d07599ad8818abaad38cda"}, + {file = "multidict-5.1.0-cp39-cp39-win32.whl", hash = "sha256:fc13a9524bc18b6fb6e0dbec3533ba0496bbed167c56d0aabefd965584557d80"}, + {file = "multidict-5.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:7df80d07818b385f3129180369079bd6934cf70469f99daaebfac89dca288359"}, + {file = "multidict-5.1.0.tar.gz", hash = "sha256:25b4e5f22d3a37ddf3effc0710ba692cfc792c2b9edfb9c05aefe823256e84d5"}, +] +packaging = [ + {file = "packaging-20.8-py2.py3-none-any.whl", hash = "sha256:24e0da08660a87484d1602c30bb4902d74816b6985b93de36926f5bc95741858"}, + {file = "packaging-20.8.tar.gz", hash = "sha256:78598185a7008a470d64526a8059de9aaa449238f280fc9eb6b13ba6c4109093"}, +] +parso = [ + {file = "parso-0.7.1-py2.py3-none-any.whl", hash = "sha256:97218d9159b2520ff45eb78028ba8b50d2bc61dcc062a9682666f2dc4bd331ea"}, + {file = "parso-0.7.1.tar.gz", hash = "sha256:caba44724b994a8a5e086460bb212abc5a8bc46951bf4a9a1210745953622eb9"}, +] +pexpect = [ + {file = "pexpect-4.8.0-py2.py3-none-any.whl", hash = "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"}, + {file = "pexpect-4.8.0.tar.gz", hash = "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"}, +] +pickleshare = [ + {file = "pickleshare-0.7.5-py2.py3-none-any.whl", hash = "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"}, + {file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"}, +] +pluggy = [ + {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, + {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, +] +prompt-toolkit = [ + {file = "prompt_toolkit-3.0.8-py3-none-any.whl", hash = "sha256:7debb9a521e0b1ee7d2fe96ee4bd60ef03c6492784de0547337ca4433e46aa63"}, + {file = "prompt_toolkit-3.0.8.tar.gz", hash = "sha256:25c95d2ac813909f813c93fde734b6e44406d1477a9faef7c915ff37d39c0a8c"}, +] +ptyprocess = [ + {file = "ptyprocess-0.6.0-py2.py3-none-any.whl", hash = "sha256:d7cc528d76e76342423ca640335bd3633420dc1366f258cb31d05e865ef5ca1f"}, + {file = "ptyprocess-0.6.0.tar.gz", hash = "sha256:923f299cc5ad920c68f2bc0bc98b75b9f838b93b599941a6b63ddbc2476394c0"}, +] +py = [ + {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, + {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"}, +] +pycodestyle = [ + {file = "pycodestyle-2.6.0-py2.py3-none-any.whl", hash = "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367"}, + {file = "pycodestyle-2.6.0.tar.gz", hash = "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e"}, +] +pyflakes = [ + {file = "pyflakes-2.2.0-py2.py3-none-any.whl", hash = "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92"}, + {file = "pyflakes-2.2.0.tar.gz", hash = "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8"}, +] +pygments = [ + {file = "Pygments-2.7.3-py3-none-any.whl", hash = "sha256:f275b6c0909e5dafd2d6269a656aa90fa58ebf4a74f8fcf9053195d226b24a08"}, + {file = "Pygments-2.7.3.tar.gz", hash = "sha256:ccf3acacf3782cbed4a989426012f1c535c9a90d3a7fc3f16d231b9372d2b716"}, +] +pyparsing = [ + {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, + {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, +] +pytest = [ + {file = "pytest-6.2.1-py3-none-any.whl", hash = "sha256:1969f797a1a0dbd8ccf0fecc80262312729afea9c17f1d70ebf85c5e76c6f7c8"}, + {file = "pytest-6.2.1.tar.gz", hash = "sha256:66e419b1899bc27346cb2c993e12c5e5e8daba9073c1fbce33b9807abc95c306"}, +] +pytest-aiohttp = [ + {file = "pytest-aiohttp-0.3.0.tar.gz", hash = "sha256:c929854339637977375838703b62fef63528598bc0a9d451639eba95f4aaa44f"}, + {file = "pytest_aiohttp-0.3.0-py3-none-any.whl", hash = "sha256:0b9b660b146a65e1313e2083d0d2e1f63047797354af9a28d6b7c9f0726fa33d"}, +] +pytest-cov = [ + {file = "pytest-cov-2.10.1.tar.gz", hash = "sha256:47bd0ce14056fdd79f93e1713f88fad7bdcc583dcd7783da86ef2f085a0bb88e"}, + {file = "pytest_cov-2.10.1-py2.py3-none-any.whl", hash = "sha256:45ec2d5182f89a81fc3eb29e3d1ed3113b9e9a873bcddb2a71faaab066110191"}, +] +six = [ + {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, + {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, +] +toml = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] +tox = [ + {file = "tox-3.20.1-py2.py3-none-any.whl", hash = "sha256:42ce19ce5dc2f6d6b1fdc5666c476e1f1e2897359b47e0aa3a5b774f335d57c2"}, + {file = "tox-3.20.1.tar.gz", hash = "sha256:4321052bfe28f9d85082341ca8e233e3ea901fdd14dab8a5d3fbd810269fbaf6"}, +] +traitlets = [ + {file = "traitlets-5.0.5-py3-none-any.whl", hash = "sha256:69ff3f9d5351f31a7ad80443c2674b7099df13cc41fc5fa6e2f6d3b0330b0426"}, + {file = "traitlets-5.0.5.tar.gz", hash = "sha256:178f4ce988f69189f7e523337a3e11d91c786ded9360174a3d9ca83e79bc5396"}, +] +typing-extensions = [ + {file = "typing_extensions-3.7.4.3-py2-none-any.whl", hash = "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"}, + {file = "typing_extensions-3.7.4.3-py3-none-any.whl", hash = "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918"}, + {file = "typing_extensions-3.7.4.3.tar.gz", hash = "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c"}, +] +virtualenv = [ + {file = "virtualenv-20.2.2-py2.py3-none-any.whl", hash = "sha256:54b05fc737ea9c9ee9f8340f579e5da5b09fb64fd010ab5757eb90268616907c"}, + {file = "virtualenv-20.2.2.tar.gz", hash = "sha256:b7a8ec323ee02fb2312f098b6b4c9de99559b462775bc8fe3627a73706603c1b"}, +] +wcwidth = [ + {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, + {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, +] +yarl = [ + {file = "yarl-1.6.3-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:0355a701b3998dcd832d0dc47cc5dedf3874f966ac7f870e0f3a6788d802d434"}, + {file = "yarl-1.6.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:bafb450deef6861815ed579c7a6113a879a6ef58aed4c3a4be54400ae8871478"}, + {file = "yarl-1.6.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:547f7665ad50fa8563150ed079f8e805e63dd85def6674c97efd78eed6c224a6"}, + {file = "yarl-1.6.3-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:63f90b20ca654b3ecc7a8d62c03ffa46999595f0167d6450fa8383bab252987e"}, + {file = "yarl-1.6.3-cp36-cp36m-manylinux2014_ppc64le.whl", hash = "sha256:97b5bdc450d63c3ba30a127d018b866ea94e65655efaf889ebeabc20f7d12406"}, + {file = "yarl-1.6.3-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:d8d07d102f17b68966e2de0e07bfd6e139c7c02ef06d3a0f8d2f0f055e13bb76"}, + {file = "yarl-1.6.3-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:15263c3b0b47968c1d90daa89f21fcc889bb4b1aac5555580d74565de6836366"}, + {file = "yarl-1.6.3-cp36-cp36m-win32.whl", hash = "sha256:b5dfc9a40c198334f4f3f55880ecf910adebdcb2a0b9a9c23c9345faa9185721"}, + {file = "yarl-1.6.3-cp36-cp36m-win_amd64.whl", hash = "sha256:b2e9a456c121e26d13c29251f8267541bd75e6a1ccf9e859179701c36a078643"}, + {file = "yarl-1.6.3-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:ce3beb46a72d9f2190f9e1027886bfc513702d748047b548b05dab7dfb584d2e"}, + {file = "yarl-1.6.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:2ce4c621d21326a4a5500c25031e102af589edb50c09b321049e388b3934eec3"}, + {file = "yarl-1.6.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:d26608cf178efb8faa5ff0f2d2e77c208f471c5a3709e577a7b3fd0445703ac8"}, + {file = "yarl-1.6.3-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:4c5bcfc3ed226bf6419f7a33982fb4b8ec2e45785a0561eb99274ebbf09fdd6a"}, + {file = "yarl-1.6.3-cp37-cp37m-manylinux2014_ppc64le.whl", hash = "sha256:4736eaee5626db8d9cda9eb5282028cc834e2aeb194e0d8b50217d707e98bb5c"}, + {file = "yarl-1.6.3-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:68dc568889b1c13f1e4745c96b931cc94fdd0defe92a72c2b8ce01091b22e35f"}, + {file = "yarl-1.6.3-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:7356644cbed76119d0b6bd32ffba704d30d747e0c217109d7979a7bc36c4d970"}, + {file = "yarl-1.6.3-cp37-cp37m-win32.whl", hash = "sha256:00d7ad91b6583602eb9c1d085a2cf281ada267e9a197e8b7cae487dadbfa293e"}, + {file = "yarl-1.6.3-cp37-cp37m-win_amd64.whl", hash = "sha256:69ee97c71fee1f63d04c945f56d5d726483c4762845400a6795a3b75d56b6c50"}, + {file = "yarl-1.6.3-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:e46fba844f4895b36f4c398c5af062a9808d1f26b2999c58909517384d5deda2"}, + {file = "yarl-1.6.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:31ede6e8c4329fb81c86706ba8f6bf661a924b53ba191b27aa5fcee5714d18ec"}, + {file = "yarl-1.6.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:fcbb48a93e8699eae920f8d92f7160c03567b421bc17362a9ffbbd706a816f71"}, + {file = "yarl-1.6.3-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:72a660bdd24497e3e84f5519e57a9ee9220b6f3ac4d45056961bf22838ce20cc"}, + {file = "yarl-1.6.3-cp38-cp38-manylinux2014_ppc64le.whl", hash = "sha256:324ba3d3c6fee56e2e0b0d09bf5c73824b9f08234339d2b788af65e60040c959"}, + {file = "yarl-1.6.3-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:e6b5460dc5ad42ad2b36cca524491dfcaffbfd9c8df50508bddc354e787b8dc2"}, + {file = "yarl-1.6.3-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:6d6283d8e0631b617edf0fd726353cb76630b83a089a40933043894e7f6721e2"}, + {file = "yarl-1.6.3-cp38-cp38-win32.whl", hash = "sha256:9ede61b0854e267fd565e7527e2f2eb3ef8858b301319be0604177690e1a3896"}, + {file = "yarl-1.6.3-cp38-cp38-win_amd64.whl", hash = "sha256:f0b059678fd549c66b89bed03efcabb009075bd131c248ecdf087bdb6faba24a"}, + {file = "yarl-1.6.3-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:329412812ecfc94a57cd37c9d547579510a9e83c516bc069470db5f75684629e"}, + {file = "yarl-1.6.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:c49ff66d479d38ab863c50f7bb27dee97c6627c5fe60697de15529da9c3de724"}, + {file = "yarl-1.6.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f040bcc6725c821a4c0665f3aa96a4d0805a7aaf2caf266d256b8ed71b9f041c"}, + {file = "yarl-1.6.3-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:d5c32c82990e4ac4d8150fd7652b972216b204de4e83a122546dce571c1bdf25"}, + {file = "yarl-1.6.3-cp39-cp39-manylinux2014_ppc64le.whl", hash = "sha256:d597767fcd2c3dc49d6eea360c458b65643d1e4dbed91361cf5e36e53c1f8c96"}, + {file = "yarl-1.6.3-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:8aa3decd5e0e852dc68335abf5478a518b41bf2ab2f330fe44916399efedfae0"}, + {file = "yarl-1.6.3-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:73494d5b71099ae8cb8754f1df131c11d433b387efab7b51849e7e1e851f07a4"}, + {file = "yarl-1.6.3-cp39-cp39-win32.whl", hash = "sha256:5b883e458058f8d6099e4420f0cc2567989032b5f34b271c0827de9f1079a424"}, + {file = "yarl-1.6.3-cp39-cp39-win_amd64.whl", hash = "sha256:4953fb0b4fdb7e08b2f3b3be80a00d28c5c8a2056bb066169de00e6501b986b6"}, + {file = "yarl-1.6.3.tar.gz", hash = "sha256:8a9066529240171b68893d60dca86a763eae2139dd42f42106b03cf4b426bf10"}, +] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..4345e82 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,23 @@ +[tool] +[tool.poetry] +name = "aiohttp_csrf" +version = "0.0.4" +description = "CSRF protection for aiohttp-server" +authors = ["TensorTom"] +license = "MIT" +readme = "README.md" + +[tool.poetry.dependencies] +python = ">=3.8.3, <4" +aiohttp = ">=3.6.2, <3.8" +aiohttp-session = {extras = ["aioredis"], git = "https://github.com/TheDoctorAI/aiohttp-session", branch = "master"} +blake3 = "^0.1.8" + +[tool.poetry.dev-dependencies] +flake8 = ">=3.7.9" +ipdb = "*" +isort = "==4.3.21" +pytest = ">=5.3.5" +pytest-aiohttp = "^0.3" +pytest-cov = ">=2.8.1" +tox = ">=3.14.5" diff --git a/setup.py b/setup.py index 0fdc06e..8118269 100644 --- a/setup.py +++ b/setup.py @@ -1,70 +1,33 @@ -import codecs -import io -import os -import re - +# -*- coding: utf-8 -*- from setuptools import setup - -def get_version(): - with codecs.open( - os.path.join( - os.path.abspath( - os.path.dirname(__file__), - ), - 'aiohttp_csrf', - '__init__.py', - ), - 'r', - 'utf-8', - ) as fp: - try: - return re.findall(r"^__version__ = '([^']+)'$", fp.read(), re.M)[0] - except IndexError: - raise RuntimeError('Unable to determine version.') - - -def read(*parts): - filename = os.path.join(os.path.abspath(os.path.dirname(__file__)), *parts) - - with io.open(filename, encoding='utf-8', mode='rt') as fp: - return fp.read() - - -install_requires = ['aiohttp>=3.2.0'] -extras_require = { - 'session': ['aiohttp-session>=2.4.0'], +packages = \ +['aiohttp_csrf'] + +package_data = \ +{'': ['*']} + +install_requires = \ +['aiohttp-session[aioredis] @ ' + 'git+https://github.com/TheDoctorAI/aiohttp-session@master', + 'aiohttp>=3.6.2,<3.8', + 'blake3>=0.1.8,<0.2.0'] + +setup_kwargs = { + 'name': 'aiohttp-csrf', + 'version': '0.0.4', + 'description': 'CSRF protection for aiohttp-server', + 'long_description': 'aiohttp_csrf\n=============\n\nThe library provides csrf (xsrf) protection for [aiohttp.web](https://docs.aiohttp.org/en/latest/web.html).\n\n**note:** The package [aiohttp-csrf-fixed](https://pypi.org/project/aiohttp-csrf-fixed) is aiohttp_csrf 0.0.2 +\n[this commit](https://github.com/oplik0/aiohttp-csrf/commit/b1bd9207f43a2abf30e32e72ecdb10983a251823). The maintainer\ndidn\'t submit a PR so I just saw it by chance. I haven\'t had time to closely examine it but I think it\'s just removing\nthe HTTP security error that happens if no CSRF is provided. Why do that? An HTTP error is good because it tells the\nclient what happened and lets you handle it by middleware.\n\nSince they bumped to 0.0.3, I\'m skipping that version and\ngoing to 0.0.4 then merging their repo to a new branch.\n\n![image](https://img.shields.io/travis/wikibusiness/aiohttp-csrf.svg%0A%20:target:%20https://travis-ci.org/wikibusiness/aiohttp-csrf)\n\nBasic usage\n-----------\n\nThe library allows you to implement csrf (xsrf) protection for requests\n\nBasic usage example:\n\n``` {.sourceCode .python}\nimport aiohttp_csrf\nfrom aiohttp import web\n\nFORM_FIELD_NAME = \'_csrf_token\'\nCOOKIE_NAME = \'csrf_token\'\n\n\ndef make_app():\n csrf_policy = aiohttp_csrf.policy.FormPolicy(FORM_FIELD_NAME)\n\n csrf_storage = aiohttp_csrf.storage.CookieStorage(COOKIE_NAME)\n\n app = web.Application()\n\n aiohttp_csrf.setup(app, policy=csrf_policy, storage=csrf_storage)\n\n app.middlewares.append(aiohttp_csrf.csrf_middleware)\n\n async def handler_get_form_with_token(request):\n token = await aiohttp_csrf.generate_token(request)\n\n\n body = \'\'\'\n \n Form with csrf protection\n \n
\n \n \n \n
\n \n \n \'\'\' # noqa\n\n body = body.format(field_name=FORM_FIELD_NAME, token=token)\n\n return web.Response(\n body=body.encode(\'utf-8\'),\n content_type=\'text/html\',\n )\n\n async def handler_post_check(request):\n post = await request.post()\n\n body = \'Hello, {name}\'.format(name=post[\'name\'])\n\n return web.Response(\n body=body.encode(\'utf-8\'),\n content_type=\'text/html\',\n )\n\n app.router.add_route(\n \'GET\',\n \'/\',\n handler_get_form_with_token,\n )\n\n app.router.add_route(\n \'POST\',\n \'/\',\n handler_post_check,\n )\n\n return app\n\n\nweb.run_app(make_app())\n```\n\n### Initialize\n\nFirst of all, you need to initialize `aiohttp_csrf` in your application:\n\n``` {.sourceCode .python}\napp = web.Application()\n\ncsrf_policy = aiohttp_csrf.policy.FormPolicy(FORM_FIELD_NAME)\n\ncsrf_storage = aiohttp_csrf.storage.CookieStorage(COOKIE_NAME)\n\naiohttp_csrf.setup(app, policy=csrf_policy, storage=csrf_storage)\n```\n\n### Middleware and decorators\n\nAfter initialize you can use `@aiohttp_csrf.csrf_protect` for handlers, that you want to protect. Or you can\ninitialize `aiohttp_csrf.csrf_middleware` and do not disturb about using\ndecorator ([full middleware example here](demo/middleware.py)):\n\n``` {.sourceCode .python}\n...\napp.middlewares.append(aiohttp_csrf.csrf_middleware)\n...\n```\n\nIn this case all your handlers will be protected.\n\n**Note:** we strongly recommend to use `aiohttp_csrf.csrf_middleware` and `@aiohttp_csrf.csrf_exempt` instead of\nmanually managing with `@aiohttp_csrf.csrf_protect`. But if you prefer to use `@aiohttp_csrf.csrf_protect`, don\'t forget\nto use `@aiohttp_csrf.csrf_protect` for both methods: GET and\nPOST ([manual protection example](demo/manual_protection.py))\n\nIf you want to use middleware, but need handlers without protection, you can use `@aiohttp_csrf.csrf_exempt`. Mark you\nhandler with this decorator and this handler will not check the token:\n\n``` {.sourceCode .python}\n@aiohttp_csrf.csrf_exempt\nasync def handler_post_not_check(request):\n ...\n```\n\n### Generate token\n\nFor generate token you need to call `aiohttp_csrf.generate_token` in your handler:\n\n``` {.sourceCode .python}\n@aiohttp_csrf.csrf_protect\nasync def handler_get(request):\n token = await aiohttp_csrf.generate_token(request)\n ...\n```\n\nAdvanced usage\n--------------\n\n### Policies\n\nYou can use different policies for check tokens. Library provides 3 types of policy:\n\n- **FormPolicy**. This policy will search token in the body of your POST request (Usually use for forms) or as a GET\n variable of the same name. You need to specify name of field that will be checked.\n- **HeaderPolicy**. This policy will search token in headers of your POST request (Usually use for AJAX requests). You\n need to specify name of header that will be checked.\n- **FormAndHeaderPolicy**. This policy combines behavior of **FormPolicy** and **HeaderPolicy**.\n\nYou can implement your custom policies if needed. But make sure that your custom policy\nimplements `aiohttp_csrf.policy.AbstractPolicy` interface.\n\n### Storages\n\nYou can use different types of storages for storing token. Library provides 2 types of storage:\n\n- **CookieStorage**. Your token will be stored in cookie variable. You need to specify cookie name.\n- **SessionStorage**. Your token will be stored in session. You need to specify session variable name.\n\n**Important:** If you want to use session storage, you need setup aiohttp\\_session in your\napplication ([session storage example](demo/session_storage.py#L22))\n\nYou can implement your custom storages if needed. But make sure that your custom storage\nimplements `aiohttp_csrf.storage.AbstractStorage` interface.\n\n### Token generators\n\nYou can use different token generator in your application. By default storages\nusing `aiohttp_csrf.token_generator.SimpleTokenGenerator`\n\nBut if you need more secure token generator - you can use `aiohttp_csrf.token_generator.HashedTokenGenerator`\n\nAnd you can implement your custom token generators if needed. But make sure that your custom token generator\nimplements `aiohttp_csrf.token_generator.AbstractTokenGenerator` interface.\n\n### Invalid token behavior\n\nBy default, if token is invalid, `aiohttp_csrf` will raise `aiohttp.web.HTTPForbidden` exception.\n\nYou have ability to specify your custom error handler. It can be:\n\n- **callable instance. Input parameter - aiohttp request.**\n\n``` {.sourceCode .python}\ndef custom_error_handler(request):\n # do something\n return aiohttp.web.Response(status=403)\n\n# or\n\nasync def custom_async_error_handler(request):\n # await do something\n return aiohttp.web.Response(status=403)\n```\n\nIt will be called instead of protected handler.\n\n- **sub class of Exception**. In this case this Exception will be raised.\n\n``` {.sourceCode .python}\nclass CustomException(Exception):\n pass\n```\n\nYou can specify custom error handler globally, when initialize `aiohttp_csrf` in your application:\n\n``` {.sourceCode .python}\n...\nclass CustomException(Exception):\n pass\n\n...\naiohttp_csrf.setup(app, policy=csrf_policy, storage=csrf_storage, error_renderer=CustomException)\n...\n```\n\nIn this case custom error handler will be applied to all protected handlers.\n\nOr you can specify custom error handler locally, for specific handler:\n\n``` {.sourceCode .python}\n...\nclass CustomException(Exception):\n pass\n\n...\n@aiohttp_csrf.csrf_protect(error_renderer=CustomException)\ndef handler_with_custom_csrf_error(request):\n ...\n```\n\nIn this case custom error handler will be applied to this handler only. For all other handlers will be applied global\nerror handler.\n', + 'author': 'TensorTom', + 'author_email': None, + 'maintainer': None, + 'maintainer_email': None, + 'url': None, + 'packages': packages, + 'package_data': package_data, + 'install_requires': install_requires, + 'python_requires': '>=3.8.3,<4', } -setup( - name='aiohttp-csrf', - version=get_version(), - description=('CSRF protection for aiohttp.web',), - long_description=read('README.rst'), - author='TensorTom', - url='https://github.com/TensorTom/aiohttp-csrf', - packages=['aiohttp_csrf'], - include_package_data=True, - install_requires=install_requires, - extras_require=extras_require, - zip_safe=False, - classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: MIT License', - 'Operating System :: POSIX', - 'Operating System :: MacOS :: MacOS X', - 'Operating System :: Microsoft :: Windows', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.8', - ], - keywords=[ - 'csrf', - 'xsrf', - 'aiohttp', - ], -) +setup(**setup_kwargs) From 7d9d629bdc6d8d0d3820e4c21b451f25aa2d9156 Mon Sep 17 00:00:00 2001 From: TensorTom <14287229+TensorTom@users.noreply.github.com> Date: Sun, 20 Dec 2020 04:15:43 -0500 Subject: [PATCH 15/23] fix hash test --- tests/test_token_generator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_token_generator.py b/tests/test_token_generator.py index e4ac6b0..f2ee926 100644 --- a/tests/test_token_generator.py +++ b/tests/test_token_generator.py @@ -1,4 +1,4 @@ -import hashlib +from blake3 import blake3 import uuid from unittest import mock @@ -29,7 +29,7 @@ def test_hashed_token_generator(): u = uuid.uuid4() token_string = u.hex + 'secret' - hasher = hashlib.sha256(token_string.encode(encoding=encoding)) + hasher = blake3(token_string.encode(encoding=encoding)) with mock.patch('hashlib.sha256', return_value=hasher): token = token_generator.generate() From dd4ff8773feb062ed26a0915cac114f96d4c5198 Mon Sep 17 00:00:00 2001 From: TensorTom <14287229+TensorTom@users.noreply.github.com> Date: Sun, 20 Dec 2020 04:46:22 -0500 Subject: [PATCH 16/23] Blake3 hash tokens by default --- aiohttp_csrf/__init__.py | 2 +- aiohttp_csrf/storage.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/aiohttp_csrf/__init__.py b/aiohttp_csrf/__init__.py index 2b625ed..ea370d4 100644 --- a/aiohttp_csrf/__init__.py +++ b/aiohttp_csrf/__init__.py @@ -9,7 +9,7 @@ from .storage import AbstractStorage -__version__ = '0.0.2' +__version__ = '0.0.4' APP_POLICY_KEY = 'aiohttp_csrf_policy' APP_STORAGE_KEY = 'aiohttp_csrf_storage' diff --git a/aiohttp_csrf/storage.py b/aiohttp_csrf/storage.py index c77a227..06aa304 100644 --- a/aiohttp_csrf/storage.py +++ b/aiohttp_csrf/storage.py @@ -1,6 +1,6 @@ import abc -from .token_generator import AbstractTokenGenerator, SimpleTokenGenerator +from .token_generator import AbstractTokenGenerator, HashedTokenGenerator try: from aiohttp_session import get_session @@ -30,7 +30,7 @@ class BaseStorage(AbstractStorage, metaclass=abc.ABCMeta): def __init__(self, token_generator=None): if token_generator is None: - token_generator = SimpleTokenGenerator() + token_generator = HashedTokenGenerator() elif not isinstance(token_generator, AbstractTokenGenerator): raise TypeError( 'Token generator must be instance of AbstractTokenGenerator', From 60557fd77e02124fd711ef67419197959bfb366f Mon Sep 17 00:00:00 2001 From: TensorTom <14287229+TensorTom@users.noreply.github.com> Date: Sun, 20 Dec 2020 04:58:57 -0500 Subject: [PATCH 17/23] Hash is now default --- aiohttp_csrf/storage.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/aiohttp_csrf/storage.py b/aiohttp_csrf/storage.py index 06aa304..89a332b 100644 --- a/aiohttp_csrf/storage.py +++ b/aiohttp_csrf/storage.py @@ -28,9 +28,11 @@ async def save_token(self, request, response): class BaseStorage(AbstractStorage, metaclass=abc.ABCMeta): - def __init__(self, token_generator=None): + def __init__(self, token_generator=None, secret_phrase=None): if token_generator is None: - token_generator = HashedTokenGenerator() + if secret_phrase is None: + raise TypeError('secret_phrase is required for default token type (Hash)') + token_generator = HashedTokenGenerator(secret_phrase) elif not isinstance(token_generator, AbstractTokenGenerator): raise TypeError( 'Token generator must be instance of AbstractTokenGenerator', From 440c5a7bf640c2bbd9907c283c4d516cfc129bc7 Mon Sep 17 00:00:00 2001 From: TensorTom <14287229+TensorTom@users.noreply.github.com> Date: Sun, 20 Dec 2020 05:06:31 -0500 Subject: [PATCH 18/23] Enforce secret_phrase requirement. Bump version. --- README.md | 4 ++-- README.rst | 4 ++-- aiohttp_csrf/__init__.py | 2 +- pyproject.toml | 2 +- setup.py | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index ee2175b..04eac80 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,8 @@ didn't submit a PR so I just saw it by chance. I haven't had time to closely ex the HTTP security error that happens if no CSRF is provided. Why do that? An HTTP error is good because it tells the client what happened and lets you handle it by middleware. -Since they bumped to 0.0.3, I'm skipping that version and -going to 0.0.4 then merging their repo to a new branch. +**Breaking Change:** New in 0.1.0 is Blake3 hashes are used by default. This means you must pass `secret_phrase` to +`aiohttp_csrf.storage.SessionStorage` ![image](https://img.shields.io/travis/wikibusiness/aiohttp-csrf.svg%0A%20:target:%20https://travis-ci.org/wikibusiness/aiohttp-csrf) diff --git a/README.rst b/README.rst index efa3780..c544c6e 100644 --- a/README.rst +++ b/README.rst @@ -11,8 +11,8 @@ didn't submit a PR so I just saw it by chance. I haven't had time to closely ex the HTTP security error that happens if no CSRF is provided. Why do that? An HTTP error is good because it tells the client what happened and lets you handle it by middleware. -Since they bumped to 0.0.3, I'm skipping that version and -going to 0.0.4 then merging their repo to a new branch. +**Breaking Change:** New in 0.1.0 is Blake3 hashes are used by default. This means you must pass `secret_phrase` to +`aiohttp_csrf.storage.SessionStorage` .. _aiohttp_web: https://docs.aiohttp.org/en/latest/web.html diff --git a/aiohttp_csrf/__init__.py b/aiohttp_csrf/__init__.py index ea370d4..f47baf7 100644 --- a/aiohttp_csrf/__init__.py +++ b/aiohttp_csrf/__init__.py @@ -9,7 +9,7 @@ from .storage import AbstractStorage -__version__ = '0.0.4' +__version__ = '0.1.0' APP_POLICY_KEY = 'aiohttp_csrf_policy' APP_STORAGE_KEY = 'aiohttp_csrf_storage' diff --git a/pyproject.toml b/pyproject.toml index 4345e82..3a0bb57 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [tool] [tool.poetry] name = "aiohttp_csrf" -version = "0.0.4" +version = "0.1.0" description = "CSRF protection for aiohttp-server" authors = ["TensorTom"] license = "MIT" diff --git a/setup.py b/setup.py index 8118269..210a693 100644 --- a/setup.py +++ b/setup.py @@ -15,9 +15,9 @@ setup_kwargs = { 'name': 'aiohttp-csrf', - 'version': '0.0.4', + 'version': '0.1.0', 'description': 'CSRF protection for aiohttp-server', - 'long_description': 'aiohttp_csrf\n=============\n\nThe library provides csrf (xsrf) protection for [aiohttp.web](https://docs.aiohttp.org/en/latest/web.html).\n\n**note:** The package [aiohttp-csrf-fixed](https://pypi.org/project/aiohttp-csrf-fixed) is aiohttp_csrf 0.0.2 +\n[this commit](https://github.com/oplik0/aiohttp-csrf/commit/b1bd9207f43a2abf30e32e72ecdb10983a251823). The maintainer\ndidn\'t submit a PR so I just saw it by chance. I haven\'t had time to closely examine it but I think it\'s just removing\nthe HTTP security error that happens if no CSRF is provided. Why do that? An HTTP error is good because it tells the\nclient what happened and lets you handle it by middleware.\n\nSince they bumped to 0.0.3, I\'m skipping that version and\ngoing to 0.0.4 then merging their repo to a new branch.\n\n![image](https://img.shields.io/travis/wikibusiness/aiohttp-csrf.svg%0A%20:target:%20https://travis-ci.org/wikibusiness/aiohttp-csrf)\n\nBasic usage\n-----------\n\nThe library allows you to implement csrf (xsrf) protection for requests\n\nBasic usage example:\n\n``` {.sourceCode .python}\nimport aiohttp_csrf\nfrom aiohttp import web\n\nFORM_FIELD_NAME = \'_csrf_token\'\nCOOKIE_NAME = \'csrf_token\'\n\n\ndef make_app():\n csrf_policy = aiohttp_csrf.policy.FormPolicy(FORM_FIELD_NAME)\n\n csrf_storage = aiohttp_csrf.storage.CookieStorage(COOKIE_NAME)\n\n app = web.Application()\n\n aiohttp_csrf.setup(app, policy=csrf_policy, storage=csrf_storage)\n\n app.middlewares.append(aiohttp_csrf.csrf_middleware)\n\n async def handler_get_form_with_token(request):\n token = await aiohttp_csrf.generate_token(request)\n\n\n body = \'\'\'\n \n Form with csrf protection\n \n
\n \n \n \n
\n \n \n \'\'\' # noqa\n\n body = body.format(field_name=FORM_FIELD_NAME, token=token)\n\n return web.Response(\n body=body.encode(\'utf-8\'),\n content_type=\'text/html\',\n )\n\n async def handler_post_check(request):\n post = await request.post()\n\n body = \'Hello, {name}\'.format(name=post[\'name\'])\n\n return web.Response(\n body=body.encode(\'utf-8\'),\n content_type=\'text/html\',\n )\n\n app.router.add_route(\n \'GET\',\n \'/\',\n handler_get_form_with_token,\n )\n\n app.router.add_route(\n \'POST\',\n \'/\',\n handler_post_check,\n )\n\n return app\n\n\nweb.run_app(make_app())\n```\n\n### Initialize\n\nFirst of all, you need to initialize `aiohttp_csrf` in your application:\n\n``` {.sourceCode .python}\napp = web.Application()\n\ncsrf_policy = aiohttp_csrf.policy.FormPolicy(FORM_FIELD_NAME)\n\ncsrf_storage = aiohttp_csrf.storage.CookieStorage(COOKIE_NAME)\n\naiohttp_csrf.setup(app, policy=csrf_policy, storage=csrf_storage)\n```\n\n### Middleware and decorators\n\nAfter initialize you can use `@aiohttp_csrf.csrf_protect` for handlers, that you want to protect. Or you can\ninitialize `aiohttp_csrf.csrf_middleware` and do not disturb about using\ndecorator ([full middleware example here](demo/middleware.py)):\n\n``` {.sourceCode .python}\n...\napp.middlewares.append(aiohttp_csrf.csrf_middleware)\n...\n```\n\nIn this case all your handlers will be protected.\n\n**Note:** we strongly recommend to use `aiohttp_csrf.csrf_middleware` and `@aiohttp_csrf.csrf_exempt` instead of\nmanually managing with `@aiohttp_csrf.csrf_protect`. But if you prefer to use `@aiohttp_csrf.csrf_protect`, don\'t forget\nto use `@aiohttp_csrf.csrf_protect` for both methods: GET and\nPOST ([manual protection example](demo/manual_protection.py))\n\nIf you want to use middleware, but need handlers without protection, you can use `@aiohttp_csrf.csrf_exempt`. Mark you\nhandler with this decorator and this handler will not check the token:\n\n``` {.sourceCode .python}\n@aiohttp_csrf.csrf_exempt\nasync def handler_post_not_check(request):\n ...\n```\n\n### Generate token\n\nFor generate token you need to call `aiohttp_csrf.generate_token` in your handler:\n\n``` {.sourceCode .python}\n@aiohttp_csrf.csrf_protect\nasync def handler_get(request):\n token = await aiohttp_csrf.generate_token(request)\n ...\n```\n\nAdvanced usage\n--------------\n\n### Policies\n\nYou can use different policies for check tokens. Library provides 3 types of policy:\n\n- **FormPolicy**. This policy will search token in the body of your POST request (Usually use for forms) or as a GET\n variable of the same name. You need to specify name of field that will be checked.\n- **HeaderPolicy**. This policy will search token in headers of your POST request (Usually use for AJAX requests). You\n need to specify name of header that will be checked.\n- **FormAndHeaderPolicy**. This policy combines behavior of **FormPolicy** and **HeaderPolicy**.\n\nYou can implement your custom policies if needed. But make sure that your custom policy\nimplements `aiohttp_csrf.policy.AbstractPolicy` interface.\n\n### Storages\n\nYou can use different types of storages for storing token. Library provides 2 types of storage:\n\n- **CookieStorage**. Your token will be stored in cookie variable. You need to specify cookie name.\n- **SessionStorage**. Your token will be stored in session. You need to specify session variable name.\n\n**Important:** If you want to use session storage, you need setup aiohttp\\_session in your\napplication ([session storage example](demo/session_storage.py#L22))\n\nYou can implement your custom storages if needed. But make sure that your custom storage\nimplements `aiohttp_csrf.storage.AbstractStorage` interface.\n\n### Token generators\n\nYou can use different token generator in your application. By default storages\nusing `aiohttp_csrf.token_generator.SimpleTokenGenerator`\n\nBut if you need more secure token generator - you can use `aiohttp_csrf.token_generator.HashedTokenGenerator`\n\nAnd you can implement your custom token generators if needed. But make sure that your custom token generator\nimplements `aiohttp_csrf.token_generator.AbstractTokenGenerator` interface.\n\n### Invalid token behavior\n\nBy default, if token is invalid, `aiohttp_csrf` will raise `aiohttp.web.HTTPForbidden` exception.\n\nYou have ability to specify your custom error handler. It can be:\n\n- **callable instance. Input parameter - aiohttp request.**\n\n``` {.sourceCode .python}\ndef custom_error_handler(request):\n # do something\n return aiohttp.web.Response(status=403)\n\n# or\n\nasync def custom_async_error_handler(request):\n # await do something\n return aiohttp.web.Response(status=403)\n```\n\nIt will be called instead of protected handler.\n\n- **sub class of Exception**. In this case this Exception will be raised.\n\n``` {.sourceCode .python}\nclass CustomException(Exception):\n pass\n```\n\nYou can specify custom error handler globally, when initialize `aiohttp_csrf` in your application:\n\n``` {.sourceCode .python}\n...\nclass CustomException(Exception):\n pass\n\n...\naiohttp_csrf.setup(app, policy=csrf_policy, storage=csrf_storage, error_renderer=CustomException)\n...\n```\n\nIn this case custom error handler will be applied to all protected handlers.\n\nOr you can specify custom error handler locally, for specific handler:\n\n``` {.sourceCode .python}\n...\nclass CustomException(Exception):\n pass\n\n...\n@aiohttp_csrf.csrf_protect(error_renderer=CustomException)\ndef handler_with_custom_csrf_error(request):\n ...\n```\n\nIn this case custom error handler will be applied to this handler only. For all other handlers will be applied global\nerror handler.\n', + 'long_description': 'aiohttp_csrf\n=============\n\nThe library provides csrf (xsrf) protection for [aiohttp.web](https://docs.aiohttp.org/en/latest/web.html).\n\n**note:** The package [aiohttp-csrf-fixed](https://pypi.org/project/aiohttp-csrf-fixed) is aiohttp_csrf 0.0.2 +\n[this commit](https://github.com/oplik0/aiohttp-csrf/commit/b1bd9207f43a2abf30e32e72ecdb10983a251823). The maintainer\ndidn\'t submit a PR so I just saw it by chance. I haven\'t had time to closely examine it but I think it\'s just removing\nthe HTTP security error that happens if no CSRF is provided. Why do that? An HTTP error is good because it tells the\nclient what happened and lets you handle it by middleware.\n\n**Breaking Change:** New in 0.1.0 is Blake3 hashes are used by default. This means you must pass `secret_phrase` to\n`aiohttp_csrf.storage.SessionStorage`\n\n![image](https://img.shields.io/travis/wikibusiness/aiohttp-csrf.svg%0A%20:target:%20https://travis-ci.org/wikibusiness/aiohttp-csrf)\n\nBasic usage\n-----------\n\nThe library allows you to implement csrf (xsrf) protection for requests\n\nBasic usage example:\n\n``` {.sourceCode .python}\nimport aiohttp_csrf\nfrom aiohttp import web\n\nFORM_FIELD_NAME = \'_csrf_token\'\nCOOKIE_NAME = \'csrf_token\'\n\n\ndef make_app():\n csrf_policy = aiohttp_csrf.policy.FormPolicy(FORM_FIELD_NAME)\n\n csrf_storage = aiohttp_csrf.storage.CookieStorage(COOKIE_NAME)\n\n app = web.Application()\n\n aiohttp_csrf.setup(app, policy=csrf_policy, storage=csrf_storage)\n\n app.middlewares.append(aiohttp_csrf.csrf_middleware)\n\n async def handler_get_form_with_token(request):\n token = await aiohttp_csrf.generate_token(request)\n\n\n body = \'\'\'\n \n Form with csrf protection\n \n
\n \n \n \n
\n \n \n \'\'\' # noqa\n\n body = body.format(field_name=FORM_FIELD_NAME, token=token)\n\n return web.Response(\n body=body.encode(\'utf-8\'),\n content_type=\'text/html\',\n )\n\n async def handler_post_check(request):\n post = await request.post()\n\n body = \'Hello, {name}\'.format(name=post[\'name\'])\n\n return web.Response(\n body=body.encode(\'utf-8\'),\n content_type=\'text/html\',\n )\n\n app.router.add_route(\n \'GET\',\n \'/\',\n handler_get_form_with_token,\n )\n\n app.router.add_route(\n \'POST\',\n \'/\',\n handler_post_check,\n )\n\n return app\n\n\nweb.run_app(make_app())\n```\n\n### Initialize\n\nFirst of all, you need to initialize `aiohttp_csrf` in your application:\n\n``` {.sourceCode .python}\napp = web.Application()\n\ncsrf_policy = aiohttp_csrf.policy.FormPolicy(FORM_FIELD_NAME)\n\ncsrf_storage = aiohttp_csrf.storage.CookieStorage(COOKIE_NAME)\n\naiohttp_csrf.setup(app, policy=csrf_policy, storage=csrf_storage)\n```\n\n### Middleware and decorators\n\nAfter initialize you can use `@aiohttp_csrf.csrf_protect` for handlers, that you want to protect. Or you can\ninitialize `aiohttp_csrf.csrf_middleware` and do not disturb about using\ndecorator ([full middleware example here](demo/middleware.py)):\n\n``` {.sourceCode .python}\n...\napp.middlewares.append(aiohttp_csrf.csrf_middleware)\n...\n```\n\nIn this case all your handlers will be protected.\n\n**Note:** we strongly recommend to use `aiohttp_csrf.csrf_middleware` and `@aiohttp_csrf.csrf_exempt` instead of\nmanually managing with `@aiohttp_csrf.csrf_protect`. But if you prefer to use `@aiohttp_csrf.csrf_protect`, don\'t forget\nto use `@aiohttp_csrf.csrf_protect` for both methods: GET and\nPOST ([manual protection example](demo/manual_protection.py))\n\nIf you want to use middleware, but need handlers without protection, you can use `@aiohttp_csrf.csrf_exempt`. Mark you\nhandler with this decorator and this handler will not check the token:\n\n``` {.sourceCode .python}\n@aiohttp_csrf.csrf_exempt\nasync def handler_post_not_check(request):\n ...\n```\n\n### Generate token\n\nFor generate token you need to call `aiohttp_csrf.generate_token` in your handler:\n\n``` {.sourceCode .python}\n@aiohttp_csrf.csrf_protect\nasync def handler_get(request):\n token = await aiohttp_csrf.generate_token(request)\n ...\n```\n\nAdvanced usage\n--------------\n\n### Policies\n\nYou can use different policies for check tokens. Library provides 3 types of policy:\n\n- **FormPolicy**. This policy will search token in the body of your POST request (Usually use for forms) or as a GET\n variable of the same name. You need to specify name of field that will be checked.\n- **HeaderPolicy**. This policy will search token in headers of your POST request (Usually use for AJAX requests). You\n need to specify name of header that will be checked.\n- **FormAndHeaderPolicy**. This policy combines behavior of **FormPolicy** and **HeaderPolicy**.\n\nYou can implement your custom policies if needed. But make sure that your custom policy\nimplements `aiohttp_csrf.policy.AbstractPolicy` interface.\n\n### Storages\n\nYou can use different types of storages for storing token. Library provides 2 types of storage:\n\n- **CookieStorage**. Your token will be stored in cookie variable. You need to specify cookie name.\n- **SessionStorage**. Your token will be stored in session. You need to specify session variable name.\n\n**Important:** If you want to use session storage, you need setup aiohttp\\_session in your\napplication ([session storage example](demo/session_storage.py#L22))\n\nYou can implement your custom storages if needed. But make sure that your custom storage\nimplements `aiohttp_csrf.storage.AbstractStorage` interface.\n\n### Token generators\n\nYou can use different token generator in your application. By default storages\nusing `aiohttp_csrf.token_generator.SimpleTokenGenerator`\n\nBut if you need more secure token generator - you can use `aiohttp_csrf.token_generator.HashedTokenGenerator`\n\nAnd you can implement your custom token generators if needed. But make sure that your custom token generator\nimplements `aiohttp_csrf.token_generator.AbstractTokenGenerator` interface.\n\n### Invalid token behavior\n\nBy default, if token is invalid, `aiohttp_csrf` will raise `aiohttp.web.HTTPForbidden` exception.\n\nYou have ability to specify your custom error handler. It can be:\n\n- **callable instance. Input parameter - aiohttp request.**\n\n``` {.sourceCode .python}\ndef custom_error_handler(request):\n # do something\n return aiohttp.web.Response(status=403)\n\n# or\n\nasync def custom_async_error_handler(request):\n # await do something\n return aiohttp.web.Response(status=403)\n```\n\nIt will be called instead of protected handler.\n\n- **sub class of Exception**. In this case this Exception will be raised.\n\n``` {.sourceCode .python}\nclass CustomException(Exception):\n pass\n```\n\nYou can specify custom error handler globally, when initialize `aiohttp_csrf` in your application:\n\n``` {.sourceCode .python}\n...\nclass CustomException(Exception):\n pass\n\n...\naiohttp_csrf.setup(app, policy=csrf_policy, storage=csrf_storage, error_renderer=CustomException)\n...\n```\n\nIn this case custom error handler will be applied to all protected handlers.\n\nOr you can specify custom error handler locally, for specific handler:\n\n``` {.sourceCode .python}\n...\nclass CustomException(Exception):\n pass\n\n...\n@aiohttp_csrf.csrf_protect(error_renderer=CustomException)\ndef handler_with_custom_csrf_error(request):\n ...\n```\n\nIn this case custom error handler will be applied to this handler only. For all other handlers will be applied global\nerror handler.\n', 'author': 'TensorTom', 'author_email': None, 'maintainer': None, From c6a8e1aee2eb9f63c2f3b0d7b2cc4639dfc1b3cd Mon Sep 17 00:00:00 2001 From: TensorTom <14287229+TensorTom@users.noreply.github.com> Date: Tue, 22 Dec 2020 02:01:23 -0500 Subject: [PATCH 19/23] Fix aiohttp-session pin --- poetry.lock | 82 +++----------------------------------------------- pyproject.toml | 2 +- setup.py | 5 +-- 3 files changed, 6 insertions(+), 83 deletions(-) diff --git a/poetry.lock b/poetry.lock index 8ba51a3..ab841c0 100644 --- a/poetry.lock +++ b/poetry.lock @@ -35,23 +35,6 @@ secure = ["cryptography"] [package.dependencies] aiohttp = ">=3.0.1" -[package.source] -url = "https://github.com/TheDoctorAI/aiohttp-session" -reference = "f8062f5d89ed0989e7df9f22aca0c83168245416" -type = "git" - -[[package]] -name = "aioredis" -version = "1.3.1" -description = "asyncio (PEP 3156) Redis support" -category = "main" -optional = false -python-versions = "*" - -[package.dependencies] -async-timeout = "*" -hiredis = "*" - [[package]] name = "appdirs" version = "1.4.4" @@ -183,14 +166,6 @@ mccabe = ">=0.6.0,<0.7.0" pycodestyle = ">=2.6.0a1,<2.7.0" pyflakes = ">=2.2.0,<2.3.0" -[[package]] -name = "hiredis" -version = "1.1.0" -description = "Python wrapper for hiredis" -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - [[package]] name = "idna" version = "2.10" @@ -577,7 +552,7 @@ multidict = ">=4.0" [metadata] python-versions = ">=3.8.3, <4" -content-hash = "292cc9903afc09a3f816bbbef2b9f4d1cfc37577470aaa4eded47442d7064907" +content-hash = "522be7b583f6cc06dbbf720812ae29f1a0c6b7049ae5113fc764734662e59b61" [metadata.files] aiohttp = [ @@ -619,10 +594,9 @@ aiohttp = [ {file = "aiohttp-3.7.3-cp39-cp39-win_amd64.whl", hash = "sha256:e1b95972a0ae3f248a899cdbac92ba2e01d731225f566569311043ce2226f5e7"}, {file = "aiohttp-3.7.3.tar.gz", hash = "sha256:9c1a81af067e72261c9cbe33ea792893e83bc6aa987bfbd6fdc1e5e7b22777c4"}, ] -aiohttp-session = [] -aioredis = [ - {file = "aioredis-1.3.1-py3-none-any.whl", hash = "sha256:b61808d7e97b7cd5a92ed574937a079c9387fdadd22bfbfa7ad2fd319ecc26e3"}, - {file = "aioredis-1.3.1.tar.gz", hash = "sha256:15f8af30b044c771aee6787e5ec24694c048184c7b9e54c3b60c750a4b93273a"}, +aiohttp-session = [ + {file = "aiohttp-session-2.9.0.tar.gz", hash = "sha256:959413468b84e30e7ca09719617cfb0000066a2e0f6c20062d043433e82aeb74"}, + {file = "aiohttp_session-2.9.0-py3-none-any.whl", hash = "sha256:74853d1177541cccfefb436409f9ea5d67a62f84e13946a3e115a765d9a0349c"}, ] appdirs = [ {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, @@ -745,54 +719,6 @@ flake8 = [ {file = "flake8-3.8.4-py2.py3-none-any.whl", hash = "sha256:749dbbd6bfd0cf1318af27bf97a14e28e5ff548ef8e5b1566ccfb25a11e7c839"}, {file = "flake8-3.8.4.tar.gz", hash = "sha256:aadae8761ec651813c24be05c6f7b4680857ef6afaae4651a4eccaef97ce6c3b"}, ] -hiredis = [ - {file = "hiredis-1.1.0-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:289b31885b4996ce04cadfd5fc03d034dce8e2a8234479f7c9e23b9e245db06b"}, - {file = "hiredis-1.1.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:7b0f63f10a166583ab744a58baad04e0f52cfea1ac27bfa1b0c21a48d1003c23"}, - {file = "hiredis-1.1.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:6996883a8a6ff9117cbb3d6f5b0dcbbae6fb9e31e1a3e4e2f95e0214d9a1c655"}, - {file = "hiredis-1.1.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:b33aea449e7f46738811fbc6f0b3177c6777a572207412bbbf6f525ffed001ae"}, - {file = "hiredis-1.1.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:8daecd778c1da45b8bd54fd41ffcd471a86beed3d8e57a43acf7a8d63bba4058"}, - {file = "hiredis-1.1.0-cp27-cp27m-win32.whl", hash = "sha256:e82d6b930e02e80e5109b678c663a9ed210680ded81c1abaf54635d88d1da298"}, - {file = "hiredis-1.1.0-cp27-cp27m-win_amd64.whl", hash = "sha256:d2c0caffa47606d6d7c8af94ba42547bd2a441f06c74fd90a1ffe328524a6c64"}, - {file = "hiredis-1.1.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:47bcf3c5e6c1e87ceb86cdda2ee983fa0fe56a999e6185099b3c93a223f2fa9b"}, - {file = "hiredis-1.1.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:dcb2db95e629962db5a355047fb8aefb012df6c8ae608930d391619dbd96fd86"}, - {file = "hiredis-1.1.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:7332d5c3e35154cd234fd79573736ddcf7a0ade7a986db35b6196b9171493e75"}, - {file = "hiredis-1.1.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:6c96f64a54f030366657a54bb90b3093afc9c16c8e0dfa29fc0d6dbe169103a5"}, - {file = "hiredis-1.1.0-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:b44f9421c4505c548435244d74037618f452844c5d3c67719d8a55e2613549da"}, - {file = "hiredis-1.1.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:abfb15a6a7822f0fae681785cb38860e7a2cb1616a708d53df557b3d76c5bfd4"}, - {file = "hiredis-1.1.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:89ebf69cb19a33d625db72d2ac589d26e936b8f7628531269accf4a3196e7872"}, - {file = "hiredis-1.1.0-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:5b1451727f02e7acbdf6aae4e06d75f66ee82966ff9114550381c3271a90f56c"}, - {file = "hiredis-1.1.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:7885b6f32c4a898e825bb7f56f36a02781ac4a951c63e4169f0afcf9c8c30dfb"}, - {file = "hiredis-1.1.0-cp35-cp35m-win32.whl", hash = "sha256:a04901757cb0fb0f5602ac11dda48f5510f94372144d06c2563ba56c480b467c"}, - {file = "hiredis-1.1.0-cp35-cp35m-win_amd64.whl", hash = "sha256:3bb9b63d319402cead8bbd9dd55dca3b667d2997e9a0d8a1f9b6cc274db4baee"}, - {file = "hiredis-1.1.0-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:e0eeb9c112fec2031927a1745788a181d0eecbacbed941fc5c4f7bc3f7b273bf"}, - {file = "hiredis-1.1.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:18402d9e54fb278cb9a8c638df6f1550aca36a009d47ecf5aa263a38600f35b0"}, - {file = "hiredis-1.1.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:cdfd501c7ac5b198c15df800a3a34c38345f5182e5f80770caf362bccca65628"}, - {file = "hiredis-1.1.0-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:43b8ed3dbfd9171e44c554cb4acf4ee4505caa84c5e341858b50ea27dd2b6e12"}, - {file = "hiredis-1.1.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:c2851deeabd96d3f6283e9c6b26e0bfed4de2dc6fb15edf913e78b79fc5909ed"}, - {file = "hiredis-1.1.0-cp36-cp36m-win32.whl", hash = "sha256:955ba8ea73cf3ed8bd2f963b4cb9f8f0dcb27becd2f4b3dd536fd24c45533454"}, - {file = "hiredis-1.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:5263db1e2e1e8ae30500cdd75a979ff99dcc184201e6b4b820d0de74834d2323"}, - {file = "hiredis-1.1.0-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:e154891263306200260d7f3051982774d7b9ef35af3509d5adbbe539afd2610c"}, - {file = "hiredis-1.1.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:964f18a59f5a64c0170f684c417f4fe3e695a536612e13074c4dd5d1c6d7c882"}, - {file = "hiredis-1.1.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:23344e3c2177baf6975fbfa361ed92eb7d36d08f454636e5054b3faa7c2aff8a"}, - {file = "hiredis-1.1.0-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:b27f082f47d23cffc4cf1388b84fdc45c4ef6015f906cd7e0d988d9e35d36349"}, - {file = "hiredis-1.1.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:aa0af2deb166a5e26e0d554b824605e660039b161e37ed4f01b8d04beec184f3"}, - {file = "hiredis-1.1.0-cp37-cp37m-win32.whl", hash = "sha256:819f95d4eba3f9e484dd115ab7ab72845cf766b84286a00d4ecf76d33f1edca1"}, - {file = "hiredis-1.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2c1c570ae7bf1bab304f29427e2475fe1856814312c4a1cf1cd0ee133f07a3c6"}, - {file = "hiredis-1.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9e9c9078a7ce07e6fce366bd818be89365a35d2e4b163268f0ca9ba7e13bb2f6"}, - {file = "hiredis-1.1.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:2c227c0ed371771ffda256034427320870e8ea2e4fd0c0a618c766e7c49aad73"}, - {file = "hiredis-1.1.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:0a909bf501459062aa1552be1461456518f367379fdc9fdb1f2ca5e4a1fdd7c0"}, - {file = "hiredis-1.1.0-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:1e4cbbc3858ec7e680006e5ca590d89a5e083235988f26a004acf7244389ac01"}, - {file = "hiredis-1.1.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:a7bf1492429f18d205f3a818da3ff1f242f60aa59006e53dee00b4ef592a3363"}, - {file = "hiredis-1.1.0-cp38-cp38-win32.whl", hash = "sha256:bcc371151d1512201d0214c36c0c150b1dc64f19c2b1a8c9cb1d7c7c15ebd93f"}, - {file = "hiredis-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:e64be68255234bb489a574c4f2f8df7029c98c81ec4d160d6cd836e7f0679390"}, - {file = "hiredis-1.1.0-pp27-pypy_73-manylinux1_x86_64.whl", hash = "sha256:8968eeaa4d37a38f8ca1f9dbe53526b69628edc9c42229a5b2f56d98bb828c1f"}, - {file = "hiredis-1.1.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:b253fe4df2afea4dfa6b1fa8c5fef212aff8bcaaeb4207e81eed05cb5e4a7919"}, - {file = "hiredis-1.1.0-pp27-pypy_73-win32.whl", hash = "sha256:969843fbdfbf56cdb71da6f0bdf50f9985b8b8aeb630102945306cf10a9c6af2"}, - {file = "hiredis-1.1.0-pp36-pypy36_pp73-manylinux1_x86_64.whl", hash = "sha256:e2e023a42dcbab8ed31f97c2bcdb980b7fbe0ada34037d87ba9d799664b58ded"}, - {file = "hiredis-1.1.0-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:06a039208f83744a702279b894c8cf24c14fd63c59cd917dcde168b79eef0680"}, - {file = "hiredis-1.1.0-pp36-pypy36_pp73-win32.whl", hash = "sha256:3ef2183de67b59930d2db8b8e8d4d58e00a50fcc5e92f4f678f6eed7a1c72d55"}, - {file = "hiredis-1.1.0.tar.gz", hash = "sha256:996021ef33e0f50b97ff2d6b5f422a0fe5577de21a8873b58a779a5ddd1c3132"}, -] idna = [ {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, diff --git a/pyproject.toml b/pyproject.toml index 3a0bb57..2948571 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ readme = "README.md" [tool.poetry.dependencies] python = ">=3.8.3, <4" aiohttp = ">=3.6.2, <3.8" -aiohttp-session = {extras = ["aioredis"], git = "https://github.com/TheDoctorAI/aiohttp-session", branch = "master"} +aiohttp-session = "^2" blake3 = "^0.1.8" [tool.poetry.dev-dependencies] diff --git a/setup.py b/setup.py index 210a693..446f42c 100644 --- a/setup.py +++ b/setup.py @@ -8,10 +8,7 @@ {'': ['*']} install_requires = \ -['aiohttp-session[aioredis] @ ' - 'git+https://github.com/TheDoctorAI/aiohttp-session@master', - 'aiohttp>=3.6.2,<3.8', - 'blake3>=0.1.8,<0.2.0'] +['aiohttp-session>=2,<3', 'aiohttp>=3.6.2,<3.8', 'blake3>=0.1.8,<0.2.0'] setup_kwargs = { 'name': 'aiohttp-csrf', From ac0d09d4d7f5e35be32b7ac206e543745a742fcb Mon Sep 17 00:00:00 2001 From: TensorTom <14287229+TensorTom@users.noreply.github.com> Date: Fri, 25 Dec 2020 01:11:50 -0500 Subject: [PATCH 20/23] Allow up to aiohttp version 4. Bump this version. --- aiohttp_csrf/__init__.py | 2 +- poetry.lock | 2 +- pyproject.toml | 4 ++-- setup.py | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/aiohttp_csrf/__init__.py b/aiohttp_csrf/__init__.py index f47baf7..68045c6 100644 --- a/aiohttp_csrf/__init__.py +++ b/aiohttp_csrf/__init__.py @@ -9,7 +9,7 @@ from .storage import AbstractStorage -__version__ = '0.1.0' +__version__ = '0.1.1' APP_POLICY_KEY = 'aiohttp_csrf_policy' APP_STORAGE_KEY = 'aiohttp_csrf_storage' diff --git a/poetry.lock b/poetry.lock index ab841c0..4aa5758 100644 --- a/poetry.lock +++ b/poetry.lock @@ -552,7 +552,7 @@ multidict = ">=4.0" [metadata] python-versions = ">=3.8.3, <4" -content-hash = "522be7b583f6cc06dbbf720812ae29f1a0c6b7049ae5113fc764734662e59b61" +content-hash = "8fab42c7fc25f0f2db49dfcf2ec0da6e7ee9fc3c84db3e7649d8251c7190c5cc" [metadata.files] aiohttp = [ diff --git a/pyproject.toml b/pyproject.toml index 2948571..ce55955 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [tool] [tool.poetry] name = "aiohttp_csrf" -version = "0.1.0" +version = "0.1.1" description = "CSRF protection for aiohttp-server" authors = ["TensorTom"] license = "MIT" @@ -9,7 +9,7 @@ readme = "README.md" [tool.poetry.dependencies] python = ">=3.8.3, <4" -aiohttp = ">=3.6.2, <3.8" +aiohttp = ">=3.6.2, <4.1" aiohttp-session = "^2" blake3 = "^0.1.8" diff --git a/setup.py b/setup.py index 446f42c..3e06de5 100644 --- a/setup.py +++ b/setup.py @@ -8,11 +8,11 @@ {'': ['*']} install_requires = \ -['aiohttp-session>=2,<3', 'aiohttp>=3.6.2,<3.8', 'blake3>=0.1.8,<0.2.0'] +['aiohttp-session>=2,<3', 'aiohttp>=3.6.2,<4.1', 'blake3>=0.1.8,<0.2.0'] setup_kwargs = { 'name': 'aiohttp-csrf', - 'version': '0.1.0', + 'version': '0.1.1', 'description': 'CSRF protection for aiohttp-server', 'long_description': 'aiohttp_csrf\n=============\n\nThe library provides csrf (xsrf) protection for [aiohttp.web](https://docs.aiohttp.org/en/latest/web.html).\n\n**note:** The package [aiohttp-csrf-fixed](https://pypi.org/project/aiohttp-csrf-fixed) is aiohttp_csrf 0.0.2 +\n[this commit](https://github.com/oplik0/aiohttp-csrf/commit/b1bd9207f43a2abf30e32e72ecdb10983a251823). The maintainer\ndidn\'t submit a PR so I just saw it by chance. I haven\'t had time to closely examine it but I think it\'s just removing\nthe HTTP security error that happens if no CSRF is provided. Why do that? An HTTP error is good because it tells the\nclient what happened and lets you handle it by middleware.\n\n**Breaking Change:** New in 0.1.0 is Blake3 hashes are used by default. This means you must pass `secret_phrase` to\n`aiohttp_csrf.storage.SessionStorage`\n\n![image](https://img.shields.io/travis/wikibusiness/aiohttp-csrf.svg%0A%20:target:%20https://travis-ci.org/wikibusiness/aiohttp-csrf)\n\nBasic usage\n-----------\n\nThe library allows you to implement csrf (xsrf) protection for requests\n\nBasic usage example:\n\n``` {.sourceCode .python}\nimport aiohttp_csrf\nfrom aiohttp import web\n\nFORM_FIELD_NAME = \'_csrf_token\'\nCOOKIE_NAME = \'csrf_token\'\n\n\ndef make_app():\n csrf_policy = aiohttp_csrf.policy.FormPolicy(FORM_FIELD_NAME)\n\n csrf_storage = aiohttp_csrf.storage.CookieStorage(COOKIE_NAME)\n\n app = web.Application()\n\n aiohttp_csrf.setup(app, policy=csrf_policy, storage=csrf_storage)\n\n app.middlewares.append(aiohttp_csrf.csrf_middleware)\n\n async def handler_get_form_with_token(request):\n token = await aiohttp_csrf.generate_token(request)\n\n\n body = \'\'\'\n \n Form with csrf protection\n \n
\n \n \n \n
\n \n \n \'\'\' # noqa\n\n body = body.format(field_name=FORM_FIELD_NAME, token=token)\n\n return web.Response(\n body=body.encode(\'utf-8\'),\n content_type=\'text/html\',\n )\n\n async def handler_post_check(request):\n post = await request.post()\n\n body = \'Hello, {name}\'.format(name=post[\'name\'])\n\n return web.Response(\n body=body.encode(\'utf-8\'),\n content_type=\'text/html\',\n )\n\n app.router.add_route(\n \'GET\',\n \'/\',\n handler_get_form_with_token,\n )\n\n app.router.add_route(\n \'POST\',\n \'/\',\n handler_post_check,\n )\n\n return app\n\n\nweb.run_app(make_app())\n```\n\n### Initialize\n\nFirst of all, you need to initialize `aiohttp_csrf` in your application:\n\n``` {.sourceCode .python}\napp = web.Application()\n\ncsrf_policy = aiohttp_csrf.policy.FormPolicy(FORM_FIELD_NAME)\n\ncsrf_storage = aiohttp_csrf.storage.CookieStorage(COOKIE_NAME)\n\naiohttp_csrf.setup(app, policy=csrf_policy, storage=csrf_storage)\n```\n\n### Middleware and decorators\n\nAfter initialize you can use `@aiohttp_csrf.csrf_protect` for handlers, that you want to protect. Or you can\ninitialize `aiohttp_csrf.csrf_middleware` and do not disturb about using\ndecorator ([full middleware example here](demo/middleware.py)):\n\n``` {.sourceCode .python}\n...\napp.middlewares.append(aiohttp_csrf.csrf_middleware)\n...\n```\n\nIn this case all your handlers will be protected.\n\n**Note:** we strongly recommend to use `aiohttp_csrf.csrf_middleware` and `@aiohttp_csrf.csrf_exempt` instead of\nmanually managing with `@aiohttp_csrf.csrf_protect`. But if you prefer to use `@aiohttp_csrf.csrf_protect`, don\'t forget\nto use `@aiohttp_csrf.csrf_protect` for both methods: GET and\nPOST ([manual protection example](demo/manual_protection.py))\n\nIf you want to use middleware, but need handlers without protection, you can use `@aiohttp_csrf.csrf_exempt`. Mark you\nhandler with this decorator and this handler will not check the token:\n\n``` {.sourceCode .python}\n@aiohttp_csrf.csrf_exempt\nasync def handler_post_not_check(request):\n ...\n```\n\n### Generate token\n\nFor generate token you need to call `aiohttp_csrf.generate_token` in your handler:\n\n``` {.sourceCode .python}\n@aiohttp_csrf.csrf_protect\nasync def handler_get(request):\n token = await aiohttp_csrf.generate_token(request)\n ...\n```\n\nAdvanced usage\n--------------\n\n### Policies\n\nYou can use different policies for check tokens. Library provides 3 types of policy:\n\n- **FormPolicy**. This policy will search token in the body of your POST request (Usually use for forms) or as a GET\n variable of the same name. You need to specify name of field that will be checked.\n- **HeaderPolicy**. This policy will search token in headers of your POST request (Usually use for AJAX requests). You\n need to specify name of header that will be checked.\n- **FormAndHeaderPolicy**. This policy combines behavior of **FormPolicy** and **HeaderPolicy**.\n\nYou can implement your custom policies if needed. But make sure that your custom policy\nimplements `aiohttp_csrf.policy.AbstractPolicy` interface.\n\n### Storages\n\nYou can use different types of storages for storing token. Library provides 2 types of storage:\n\n- **CookieStorage**. Your token will be stored in cookie variable. You need to specify cookie name.\n- **SessionStorage**. Your token will be stored in session. You need to specify session variable name.\n\n**Important:** If you want to use session storage, you need setup aiohttp\\_session in your\napplication ([session storage example](demo/session_storage.py#L22))\n\nYou can implement your custom storages if needed. But make sure that your custom storage\nimplements `aiohttp_csrf.storage.AbstractStorage` interface.\n\n### Token generators\n\nYou can use different token generator in your application. By default storages\nusing `aiohttp_csrf.token_generator.SimpleTokenGenerator`\n\nBut if you need more secure token generator - you can use `aiohttp_csrf.token_generator.HashedTokenGenerator`\n\nAnd you can implement your custom token generators if needed. But make sure that your custom token generator\nimplements `aiohttp_csrf.token_generator.AbstractTokenGenerator` interface.\n\n### Invalid token behavior\n\nBy default, if token is invalid, `aiohttp_csrf` will raise `aiohttp.web.HTTPForbidden` exception.\n\nYou have ability to specify your custom error handler. It can be:\n\n- **callable instance. Input parameter - aiohttp request.**\n\n``` {.sourceCode .python}\ndef custom_error_handler(request):\n # do something\n return aiohttp.web.Response(status=403)\n\n# or\n\nasync def custom_async_error_handler(request):\n # await do something\n return aiohttp.web.Response(status=403)\n```\n\nIt will be called instead of protected handler.\n\n- **sub class of Exception**. In this case this Exception will be raised.\n\n``` {.sourceCode .python}\nclass CustomException(Exception):\n pass\n```\n\nYou can specify custom error handler globally, when initialize `aiohttp_csrf` in your application:\n\n``` {.sourceCode .python}\n...\nclass CustomException(Exception):\n pass\n\n...\naiohttp_csrf.setup(app, policy=csrf_policy, storage=csrf_storage, error_renderer=CustomException)\n...\n```\n\nIn this case custom error handler will be applied to all protected handlers.\n\nOr you can specify custom error handler locally, for specific handler:\n\n``` {.sourceCode .python}\n...\nclass CustomException(Exception):\n pass\n\n...\n@aiohttp_csrf.csrf_protect(error_renderer=CustomException)\ndef handler_with_custom_csrf_error(request):\n ...\n```\n\nIn this case custom error handler will be applied to this handler only. For all other handlers will be applied global\nerror handler.\n', 'author': 'TensorTom', From ad7fbfe57e1274e96b33fda0e9a2baeea9739c0a Mon Sep 17 00:00:00 2001 From: TensorTom <14287229+TensorTom@users.noreply.github.com> Date: Thu, 31 Dec 2020 05:39:05 -0500 Subject: [PATCH 21/23] Make csrf_exempt() a coroutine --- README.md | 22 +++++++++++----------- aiohttp_csrf/__init__.py | 2 +- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 04eac80..fbd1df3 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ The library allows you to implement csrf (xsrf) protection for requests Basic usage example: -``` {.sourceCode .python} +```python import aiohttp_csrf from aiohttp import web @@ -96,7 +96,7 @@ web.run_app(make_app()) First of all, you need to initialize `aiohttp_csrf` in your application: -``` {.sourceCode .python} +```python app = web.Application() csrf_policy = aiohttp_csrf.policy.FormPolicy(FORM_FIELD_NAME) @@ -112,10 +112,10 @@ After initialize you can use `@aiohttp_csrf.csrf_protect` for handlers, that you initialize `aiohttp_csrf.csrf_middleware` and do not disturb about using decorator ([full middleware example here](demo/middleware.py)): -``` {.sourceCode .python} -... +```python +# ... app.middlewares.append(aiohttp_csrf.csrf_middleware) -... +# ... ``` In this case all your handlers will be protected. @@ -128,7 +128,7 @@ POST ([manual protection example](demo/manual_protection.py)) If you want to use middleware, but need handlers without protection, you can use `@aiohttp_csrf.csrf_exempt`. Mark you handler with this decorator and this handler will not check the token: -``` {.sourceCode .python} +```python @aiohttp_csrf.csrf_exempt async def handler_post_not_check(request): ... @@ -138,7 +138,7 @@ async def handler_post_not_check(request): For generate token you need to call `aiohttp_csrf.generate_token` in your handler: -``` {.sourceCode .python} +```python @aiohttp_csrf.csrf_protect async def handler_get(request): token = await aiohttp_csrf.generate_token(request) @@ -192,7 +192,7 @@ You have ability to specify your custom error handler. It can be: - **callable instance. Input parameter - aiohttp request.** -``` {.sourceCode .python} +```python def custom_error_handler(request): # do something return aiohttp.web.Response(status=403) @@ -208,14 +208,14 @@ It will be called instead of protected handler. - **sub class of Exception**. In this case this Exception will be raised. -``` {.sourceCode .python} +```python class CustomException(Exception): pass ``` You can specify custom error handler globally, when initialize `aiohttp_csrf` in your application: -``` {.sourceCode .python} +```python ... class CustomException(Exception): pass @@ -229,7 +229,7 @@ In this case custom error handler will be applied to all protected handlers. Or you can specify custom error handler locally, for specific handler: -``` {.sourceCode .python} +```python ... class CustomException(Exception): pass diff --git a/aiohttp_csrf/__init__.py b/aiohttp_csrf/__init__.py index 68045c6..e83eedc 100644 --- a/aiohttp_csrf/__init__.py +++ b/aiohttp_csrf/__init__.py @@ -96,7 +96,7 @@ async def save_token(request, response): await storage.save_token(request, response) -def csrf_exempt(handler): +async def csrf_exempt(handler): @wraps(handler) def wrapped_handler(*args, **kwargs): return handler(*args, **kwargs) From 5d5053f1fbcc0da0d69f9b25a1b35085eda80516 Mon Sep 17 00:00:00 2001 From: TensorTom <14287229+TensorTom@users.noreply.github.com> Date: Thu, 31 Dec 2020 07:17:30 -0500 Subject: [PATCH 22/23] async decorator wrapper. Revert csrf_exempt to non-async --- aiohttp_csrf/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/aiohttp_csrf/__init__.py b/aiohttp_csrf/__init__.py index e83eedc..38d10ab 100644 --- a/aiohttp_csrf/__init__.py +++ b/aiohttp_csrf/__init__.py @@ -96,10 +96,10 @@ async def save_token(request, response): await storage.save_token(request, response) -async def csrf_exempt(handler): +def csrf_exempt(handler): @wraps(handler) - def wrapped_handler(*args, **kwargs): - return handler(*args, **kwargs) + async def wrapped_handler(*args, **kwargs): + return await handler(*args, **kwargs) setattr(wrapped_handler, MIDDLEWARE_SKIP_PROPERTY, True) From d5ecf46f8c15b5e957be7e36c36b3a75957ba3a3 Mon Sep 17 00:00:00 2001 From: TensorTom <14287229+TensorTom@users.noreply.github.com> Date: Thu, 31 Dec 2020 07:50:43 -0500 Subject: [PATCH 23/23] Improved README.md & Built 0.1.1 for release. --- README.md | 6 ++++-- poetry.lock | 31 ++++++++++++++++--------------- pyproject.toml | 1 + setup.py | 4 ++-- 4 files changed, 23 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index fbd1df3..eab2f4f 100644 --- a/README.md +++ b/README.md @@ -3,14 +3,16 @@ aiohttp_csrf The library provides csrf (xsrf) protection for [aiohttp.web](https://docs.aiohttp.org/en/latest/web.html). +**Breaking Change:** New in 0.1.0 is Blake3 hashes are used by default. This means you must pass `secret_phrase` to +`aiohttp_csrf.storage.SessionStorage` + **note:** The package [aiohttp-csrf-fixed](https://pypi.org/project/aiohttp-csrf-fixed) is aiohttp_csrf 0.0.2 + [this commit](https://github.com/oplik0/aiohttp-csrf/commit/b1bd9207f43a2abf30e32e72ecdb10983a251823). The maintainer didn't submit a PR so I just saw it by chance. I haven't had time to closely examine it but I think it's just removing the HTTP security error that happens if no CSRF is provided. Why do that? An HTTP error is good because it tells the client what happened and lets you handle it by middleware. -**Breaking Change:** New in 0.1.0 is Blake3 hashes are used by default. This means you must pass `secret_phrase` to -`aiohttp_csrf.storage.SessionStorage` +__0.1.1:__ Converted `@aiohttp_csrf.csrf_exempt` decorator to a co-routine to make it compatible with latest aiohttp. ![image](https://img.shields.io/travis/wikibusiness/aiohttp-csrf.svg%0A%20:target:%20https://travis-ci.org/wikibusiness/aiohttp-csrf) diff --git a/poetry.lock b/poetry.lock index 4aa5758..d3c9e72 100644 --- a/poetry.lock +++ b/poetry.lock @@ -255,19 +255,19 @@ xdg_home = ["appdirs (>=1.4.0)"] [[package]] name = "jedi" -version = "0.17.2" +version = "0.18.0" description = "An autocompletion tool for Python that can be used for text editors." category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=3.6" marker = "python_version >= \"3.4\"" [package.extras] -qa = ["flake8 (3.7.9)"] -testing = ["Django (<3.1)", "colorama", "docopt", "pytest (>=3.9.0,<5.0.0)"] +qa = ["flake8 (3.8.3)", "mypy (0.782)"] +testing = ["Django (<3.1)", "colorama", "docopt", "pytest (<6.0.0)"] [package.dependencies] -parso = ">=0.7.0,<0.8.0" +parso = ">=0.8.0,<0.9.0" [[package]] name = "mccabe" @@ -298,15 +298,16 @@ pyparsing = ">=2.0.2" [[package]] name = "parso" -version = "0.7.1" +version = "0.8.1" description = "A Python Parser" category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.6" marker = "python_version >= \"3.4\"" [package.extras] -testing = ["docopt", "pytest (>=3.0.7)"] +qa = ["flake8 (3.8.3)", "mypy (0.782)"] +testing = ["docopt", "pytest (<6.0.0)"] [[package]] name = "pexpect" @@ -354,7 +355,7 @@ wcwidth = "*" [[package]] name = "ptyprocess" -version = "0.6.0" +version = "0.7.0" description = "Run a subprocess in a pseudo terminal" category = "dev" optional = false @@ -743,8 +744,8 @@ isort = [ {file = "isort-4.3.21.tar.gz", hash = "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1"}, ] jedi = [ - {file = "jedi-0.17.2-py2.py3-none-any.whl", hash = "sha256:98cc583fa0f2f8304968199b01b6b4b94f469a1f4a74c1560506ca2a211378b5"}, - {file = "jedi-0.17.2.tar.gz", hash = "sha256:86ed7d9b750603e4ba582ea8edc678657fb4007894a12bcf6f4bb97892f31d20"}, + {file = "jedi-0.18.0-py2.py3-none-any.whl", hash = "sha256:18456d83f65f400ab0c2d3319e48520420ef43b23a086fdc05dff34132f0fb93"}, + {file = "jedi-0.18.0.tar.gz", hash = "sha256:92550a404bad8afed881a137ec9a461fed49eca661414be45059329614ed0707"}, ] mccabe = [ {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, @@ -794,8 +795,8 @@ packaging = [ {file = "packaging-20.8.tar.gz", hash = "sha256:78598185a7008a470d64526a8059de9aaa449238f280fc9eb6b13ba6c4109093"}, ] parso = [ - {file = "parso-0.7.1-py2.py3-none-any.whl", hash = "sha256:97218d9159b2520ff45eb78028ba8b50d2bc61dcc062a9682666f2dc4bd331ea"}, - {file = "parso-0.7.1.tar.gz", hash = "sha256:caba44724b994a8a5e086460bb212abc5a8bc46951bf4a9a1210745953622eb9"}, + {file = "parso-0.8.1-py2.py3-none-any.whl", hash = "sha256:15b00182f472319383252c18d5913b69269590616c947747bc50bf4ac768f410"}, + {file = "parso-0.8.1.tar.gz", hash = "sha256:8519430ad07087d4c997fda3a7918f7cfa27cb58972a8c89c2a0295a1c940e9e"}, ] pexpect = [ {file = "pexpect-4.8.0-py2.py3-none-any.whl", hash = "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"}, @@ -814,8 +815,8 @@ prompt-toolkit = [ {file = "prompt_toolkit-3.0.8.tar.gz", hash = "sha256:25c95d2ac813909f813c93fde734b6e44406d1477a9faef7c915ff37d39c0a8c"}, ] ptyprocess = [ - {file = "ptyprocess-0.6.0-py2.py3-none-any.whl", hash = "sha256:d7cc528d76e76342423ca640335bd3633420dc1366f258cb31d05e865ef5ca1f"}, - {file = "ptyprocess-0.6.0.tar.gz", hash = "sha256:923f299cc5ad920c68f2bc0bc98b75b9f838b93b599941a6b63ddbc2476394c0"}, + {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, + {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, ] py = [ {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, diff --git a/pyproject.toml b/pyproject.toml index ce55955..ea2da55 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,6 +6,7 @@ description = "CSRF protection for aiohttp-server" authors = ["TensorTom"] license = "MIT" readme = "README.md" +homepage = "https://github.com/TensorTom/aiohttp-csrf" [tool.poetry.dependencies] python = ">=3.8.3, <4" diff --git a/setup.py b/setup.py index 3e06de5..f57cec6 100644 --- a/setup.py +++ b/setup.py @@ -14,12 +14,12 @@ 'name': 'aiohttp-csrf', 'version': '0.1.1', 'description': 'CSRF protection for aiohttp-server', - 'long_description': 'aiohttp_csrf\n=============\n\nThe library provides csrf (xsrf) protection for [aiohttp.web](https://docs.aiohttp.org/en/latest/web.html).\n\n**note:** The package [aiohttp-csrf-fixed](https://pypi.org/project/aiohttp-csrf-fixed) is aiohttp_csrf 0.0.2 +\n[this commit](https://github.com/oplik0/aiohttp-csrf/commit/b1bd9207f43a2abf30e32e72ecdb10983a251823). The maintainer\ndidn\'t submit a PR so I just saw it by chance. I haven\'t had time to closely examine it but I think it\'s just removing\nthe HTTP security error that happens if no CSRF is provided. Why do that? An HTTP error is good because it tells the\nclient what happened and lets you handle it by middleware.\n\n**Breaking Change:** New in 0.1.0 is Blake3 hashes are used by default. This means you must pass `secret_phrase` to\n`aiohttp_csrf.storage.SessionStorage`\n\n![image](https://img.shields.io/travis/wikibusiness/aiohttp-csrf.svg%0A%20:target:%20https://travis-ci.org/wikibusiness/aiohttp-csrf)\n\nBasic usage\n-----------\n\nThe library allows you to implement csrf (xsrf) protection for requests\n\nBasic usage example:\n\n``` {.sourceCode .python}\nimport aiohttp_csrf\nfrom aiohttp import web\n\nFORM_FIELD_NAME = \'_csrf_token\'\nCOOKIE_NAME = \'csrf_token\'\n\n\ndef make_app():\n csrf_policy = aiohttp_csrf.policy.FormPolicy(FORM_FIELD_NAME)\n\n csrf_storage = aiohttp_csrf.storage.CookieStorage(COOKIE_NAME)\n\n app = web.Application()\n\n aiohttp_csrf.setup(app, policy=csrf_policy, storage=csrf_storage)\n\n app.middlewares.append(aiohttp_csrf.csrf_middleware)\n\n async def handler_get_form_with_token(request):\n token = await aiohttp_csrf.generate_token(request)\n\n\n body = \'\'\'\n \n Form with csrf protection\n \n
\n \n \n \n
\n \n \n \'\'\' # noqa\n\n body = body.format(field_name=FORM_FIELD_NAME, token=token)\n\n return web.Response(\n body=body.encode(\'utf-8\'),\n content_type=\'text/html\',\n )\n\n async def handler_post_check(request):\n post = await request.post()\n\n body = \'Hello, {name}\'.format(name=post[\'name\'])\n\n return web.Response(\n body=body.encode(\'utf-8\'),\n content_type=\'text/html\',\n )\n\n app.router.add_route(\n \'GET\',\n \'/\',\n handler_get_form_with_token,\n )\n\n app.router.add_route(\n \'POST\',\n \'/\',\n handler_post_check,\n )\n\n return app\n\n\nweb.run_app(make_app())\n```\n\n### Initialize\n\nFirst of all, you need to initialize `aiohttp_csrf` in your application:\n\n``` {.sourceCode .python}\napp = web.Application()\n\ncsrf_policy = aiohttp_csrf.policy.FormPolicy(FORM_FIELD_NAME)\n\ncsrf_storage = aiohttp_csrf.storage.CookieStorage(COOKIE_NAME)\n\naiohttp_csrf.setup(app, policy=csrf_policy, storage=csrf_storage)\n```\n\n### Middleware and decorators\n\nAfter initialize you can use `@aiohttp_csrf.csrf_protect` for handlers, that you want to protect. Or you can\ninitialize `aiohttp_csrf.csrf_middleware` and do not disturb about using\ndecorator ([full middleware example here](demo/middleware.py)):\n\n``` {.sourceCode .python}\n...\napp.middlewares.append(aiohttp_csrf.csrf_middleware)\n...\n```\n\nIn this case all your handlers will be protected.\n\n**Note:** we strongly recommend to use `aiohttp_csrf.csrf_middleware` and `@aiohttp_csrf.csrf_exempt` instead of\nmanually managing with `@aiohttp_csrf.csrf_protect`. But if you prefer to use `@aiohttp_csrf.csrf_protect`, don\'t forget\nto use `@aiohttp_csrf.csrf_protect` for both methods: GET and\nPOST ([manual protection example](demo/manual_protection.py))\n\nIf you want to use middleware, but need handlers without protection, you can use `@aiohttp_csrf.csrf_exempt`. Mark you\nhandler with this decorator and this handler will not check the token:\n\n``` {.sourceCode .python}\n@aiohttp_csrf.csrf_exempt\nasync def handler_post_not_check(request):\n ...\n```\n\n### Generate token\n\nFor generate token you need to call `aiohttp_csrf.generate_token` in your handler:\n\n``` {.sourceCode .python}\n@aiohttp_csrf.csrf_protect\nasync def handler_get(request):\n token = await aiohttp_csrf.generate_token(request)\n ...\n```\n\nAdvanced usage\n--------------\n\n### Policies\n\nYou can use different policies for check tokens. Library provides 3 types of policy:\n\n- **FormPolicy**. This policy will search token in the body of your POST request (Usually use for forms) or as a GET\n variable of the same name. You need to specify name of field that will be checked.\n- **HeaderPolicy**. This policy will search token in headers of your POST request (Usually use for AJAX requests). You\n need to specify name of header that will be checked.\n- **FormAndHeaderPolicy**. This policy combines behavior of **FormPolicy** and **HeaderPolicy**.\n\nYou can implement your custom policies if needed. But make sure that your custom policy\nimplements `aiohttp_csrf.policy.AbstractPolicy` interface.\n\n### Storages\n\nYou can use different types of storages for storing token. Library provides 2 types of storage:\n\n- **CookieStorage**. Your token will be stored in cookie variable. You need to specify cookie name.\n- **SessionStorage**. Your token will be stored in session. You need to specify session variable name.\n\n**Important:** If you want to use session storage, you need setup aiohttp\\_session in your\napplication ([session storage example](demo/session_storage.py#L22))\n\nYou can implement your custom storages if needed. But make sure that your custom storage\nimplements `aiohttp_csrf.storage.AbstractStorage` interface.\n\n### Token generators\n\nYou can use different token generator in your application. By default storages\nusing `aiohttp_csrf.token_generator.SimpleTokenGenerator`\n\nBut if you need more secure token generator - you can use `aiohttp_csrf.token_generator.HashedTokenGenerator`\n\nAnd you can implement your custom token generators if needed. But make sure that your custom token generator\nimplements `aiohttp_csrf.token_generator.AbstractTokenGenerator` interface.\n\n### Invalid token behavior\n\nBy default, if token is invalid, `aiohttp_csrf` will raise `aiohttp.web.HTTPForbidden` exception.\n\nYou have ability to specify your custom error handler. It can be:\n\n- **callable instance. Input parameter - aiohttp request.**\n\n``` {.sourceCode .python}\ndef custom_error_handler(request):\n # do something\n return aiohttp.web.Response(status=403)\n\n# or\n\nasync def custom_async_error_handler(request):\n # await do something\n return aiohttp.web.Response(status=403)\n```\n\nIt will be called instead of protected handler.\n\n- **sub class of Exception**. In this case this Exception will be raised.\n\n``` {.sourceCode .python}\nclass CustomException(Exception):\n pass\n```\n\nYou can specify custom error handler globally, when initialize `aiohttp_csrf` in your application:\n\n``` {.sourceCode .python}\n...\nclass CustomException(Exception):\n pass\n\n...\naiohttp_csrf.setup(app, policy=csrf_policy, storage=csrf_storage, error_renderer=CustomException)\n...\n```\n\nIn this case custom error handler will be applied to all protected handlers.\n\nOr you can specify custom error handler locally, for specific handler:\n\n``` {.sourceCode .python}\n...\nclass CustomException(Exception):\n pass\n\n...\n@aiohttp_csrf.csrf_protect(error_renderer=CustomException)\ndef handler_with_custom_csrf_error(request):\n ...\n```\n\nIn this case custom error handler will be applied to this handler only. For all other handlers will be applied global\nerror handler.\n', + 'long_description': 'aiohttp_csrf\n=============\n\nThe library provides csrf (xsrf) protection for [aiohttp.web](https://docs.aiohttp.org/en/latest/web.html).\n\n**Breaking Change:** New in 0.1.0 is Blake3 hashes are used by default. This means you must pass `secret_phrase` to\n`aiohttp_csrf.storage.SessionStorage`\n\n**note:** The package [aiohttp-csrf-fixed](https://pypi.org/project/aiohttp-csrf-fixed) is aiohttp_csrf 0.0.2 +\n[this commit](https://github.com/oplik0/aiohttp-csrf/commit/b1bd9207f43a2abf30e32e72ecdb10983a251823). The maintainer\ndidn\'t submit a PR so I just saw it by chance. I haven\'t had time to closely examine it but I think it\'s just removing\nthe HTTP security error that happens if no CSRF is provided. Why do that? An HTTP error is good because it tells the\nclient what happened and lets you handle it by middleware.\n\n__0.1.1:__ Converted `@aiohttp_csrf.csrf_exempt` decorator to a co-routine to make it compatible with latest aiohttp.\n\n![image](https://img.shields.io/travis/wikibusiness/aiohttp-csrf.svg%0A%20:target:%20https://travis-ci.org/wikibusiness/aiohttp-csrf)\n\nBasic usage\n-----------\n\nThe library allows you to implement csrf (xsrf) protection for requests\n\nBasic usage example:\n\n```python\nimport aiohttp_csrf\nfrom aiohttp import web\n\nFORM_FIELD_NAME = \'_csrf_token\'\nCOOKIE_NAME = \'csrf_token\'\n\n\ndef make_app():\n csrf_policy = aiohttp_csrf.policy.FormPolicy(FORM_FIELD_NAME)\n\n csrf_storage = aiohttp_csrf.storage.CookieStorage(COOKIE_NAME)\n\n app = web.Application()\n\n aiohttp_csrf.setup(app, policy=csrf_policy, storage=csrf_storage)\n\n app.middlewares.append(aiohttp_csrf.csrf_middleware)\n\n async def handler_get_form_with_token(request):\n token = await aiohttp_csrf.generate_token(request)\n\n\n body = \'\'\'\n \n Form with csrf protection\n \n
\n \n \n \n
\n \n \n \'\'\' # noqa\n\n body = body.format(field_name=FORM_FIELD_NAME, token=token)\n\n return web.Response(\n body=body.encode(\'utf-8\'),\n content_type=\'text/html\',\n )\n\n async def handler_post_check(request):\n post = await request.post()\n\n body = \'Hello, {name}\'.format(name=post[\'name\'])\n\n return web.Response(\n body=body.encode(\'utf-8\'),\n content_type=\'text/html\',\n )\n\n app.router.add_route(\n \'GET\',\n \'/\',\n handler_get_form_with_token,\n )\n\n app.router.add_route(\n \'POST\',\n \'/\',\n handler_post_check,\n )\n\n return app\n\n\nweb.run_app(make_app())\n```\n\n### Initialize\n\nFirst of all, you need to initialize `aiohttp_csrf` in your application:\n\n```python\napp = web.Application()\n\ncsrf_policy = aiohttp_csrf.policy.FormPolicy(FORM_FIELD_NAME)\n\ncsrf_storage = aiohttp_csrf.storage.CookieStorage(COOKIE_NAME)\n\naiohttp_csrf.setup(app, policy=csrf_policy, storage=csrf_storage)\n```\n\n### Middleware and decorators\n\nAfter initialize you can use `@aiohttp_csrf.csrf_protect` for handlers, that you want to protect. Or you can\ninitialize `aiohttp_csrf.csrf_middleware` and do not disturb about using\ndecorator ([full middleware example here](demo/middleware.py)):\n\n```python\n# ...\napp.middlewares.append(aiohttp_csrf.csrf_middleware)\n# ...\n```\n\nIn this case all your handlers will be protected.\n\n**Note:** we strongly recommend to use `aiohttp_csrf.csrf_middleware` and `@aiohttp_csrf.csrf_exempt` instead of\nmanually managing with `@aiohttp_csrf.csrf_protect`. But if you prefer to use `@aiohttp_csrf.csrf_protect`, don\'t forget\nto use `@aiohttp_csrf.csrf_protect` for both methods: GET and\nPOST ([manual protection example](demo/manual_protection.py))\n\nIf you want to use middleware, but need handlers without protection, you can use `@aiohttp_csrf.csrf_exempt`. Mark you\nhandler with this decorator and this handler will not check the token:\n\n```python\n@aiohttp_csrf.csrf_exempt\nasync def handler_post_not_check(request):\n ...\n```\n\n### Generate token\n\nFor generate token you need to call `aiohttp_csrf.generate_token` in your handler:\n\n```python\n@aiohttp_csrf.csrf_protect\nasync def handler_get(request):\n token = await aiohttp_csrf.generate_token(request)\n ...\n```\n\nAdvanced usage\n--------------\n\n### Policies\n\nYou can use different policies for check tokens. Library provides 3 types of policy:\n\n- **FormPolicy**. This policy will search token in the body of your POST request (Usually use for forms) or as a GET\n variable of the same name. You need to specify name of field that will be checked.\n- **HeaderPolicy**. This policy will search token in headers of your POST request (Usually use for AJAX requests). You\n need to specify name of header that will be checked.\n- **FormAndHeaderPolicy**. This policy combines behavior of **FormPolicy** and **HeaderPolicy**.\n\nYou can implement your custom policies if needed. But make sure that your custom policy\nimplements `aiohttp_csrf.policy.AbstractPolicy` interface.\n\n### Storages\n\nYou can use different types of storages for storing token. Library provides 2 types of storage:\n\n- **CookieStorage**. Your token will be stored in cookie variable. You need to specify cookie name.\n- **SessionStorage**. Your token will be stored in session. You need to specify session variable name.\n\n**Important:** If you want to use session storage, you need setup aiohttp\\_session in your\napplication ([session storage example](demo/session_storage.py#L22))\n\nYou can implement your custom storages if needed. But make sure that your custom storage\nimplements `aiohttp_csrf.storage.AbstractStorage` interface.\n\n### Token generators\n\nYou can use different token generator in your application. By default storages\nusing `aiohttp_csrf.token_generator.SimpleTokenGenerator`\n\nBut if you need more secure token generator - you can use `aiohttp_csrf.token_generator.HashedTokenGenerator`\n\nAnd you can implement your custom token generators if needed. But make sure that your custom token generator\nimplements `aiohttp_csrf.token_generator.AbstractTokenGenerator` interface.\n\n### Invalid token behavior\n\nBy default, if token is invalid, `aiohttp_csrf` will raise `aiohttp.web.HTTPForbidden` exception.\n\nYou have ability to specify your custom error handler. It can be:\n\n- **callable instance. Input parameter - aiohttp request.**\n\n```python\ndef custom_error_handler(request):\n # do something\n return aiohttp.web.Response(status=403)\n\n# or\n\nasync def custom_async_error_handler(request):\n # await do something\n return aiohttp.web.Response(status=403)\n```\n\nIt will be called instead of protected handler.\n\n- **sub class of Exception**. In this case this Exception will be raised.\n\n```python\nclass CustomException(Exception):\n pass\n```\n\nYou can specify custom error handler globally, when initialize `aiohttp_csrf` in your application:\n\n```python\n...\nclass CustomException(Exception):\n pass\n\n...\naiohttp_csrf.setup(app, policy=csrf_policy, storage=csrf_storage, error_renderer=CustomException)\n...\n```\n\nIn this case custom error handler will be applied to all protected handlers.\n\nOr you can specify custom error handler locally, for specific handler:\n\n```python\n...\nclass CustomException(Exception):\n pass\n\n...\n@aiohttp_csrf.csrf_protect(error_renderer=CustomException)\ndef handler_with_custom_csrf_error(request):\n ...\n```\n\nIn this case custom error handler will be applied to this handler only. For all other handlers will be applied global\nerror handler.\n', 'author': 'TensorTom', 'author_email': None, 'maintainer': None, 'maintainer_email': None, - 'url': None, + 'url': 'https://github.com/TensorTom/aiohttp-csrf', 'packages': packages, 'package_data': package_data, 'install_requires': install_requires,