Initial commit
This commit is contained in:
+54
@@ -0,0 +1,54 @@
|
|||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
env/
|
||||||
|
bin/
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.coverage
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
|
||||||
|
# Mr Developer
|
||||||
|
.mr.developer.cfg
|
||||||
|
.project
|
||||||
|
.pydevproject
|
||||||
|
|
||||||
|
# Rope
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
Copyright (c) 2014 by Shipeng Feng.
|
||||||
|
Copyright (c) 2020 by Sander.
|
||||||
|
|
||||||
|
Some rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are
|
||||||
|
met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
* Redistributions in binary form must reproduce the above
|
||||||
|
copyright notice, this list of conditions and the following
|
||||||
|
disclaimer in the documentation and/or other materials provided
|
||||||
|
with the distribution.
|
||||||
|
|
||||||
|
* The names of the contributors may not be used to endorse or
|
||||||
|
promote products derived from this software without specific
|
||||||
|
prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||||
|
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||||
|
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
# Quart-session
|
||||||
|
|
||||||
|
Quart-Session is an extension for Quart that adds support for
|
||||||
|
server-side sessions to your application.
|
||||||
|
|
||||||
|
Based on [flask-session](https://pypi.org/project/Flask-Session/).
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
|
||||||
|
Quart-Session can be installed via pipenv or
|
||||||
|
pip,
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ pipenv install quart-session
|
||||||
|
$ pip install quart-session
|
||||||
|
```
|
||||||
|
|
||||||
|
and requires Python 3.7.0 or higher. A fairly minimal Quart-Session example is,
|
||||||
|
|
||||||
|
```python3
|
||||||
|
from quart import Quart, session
|
||||||
|
from quart_session import Session
|
||||||
|
|
||||||
|
app = Quart(__name__)
|
||||||
|
app.config['SESSION_TYPE'] = 'redis'
|
||||||
|
Session(app)
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
async def hello():
|
||||||
|
session["foo"] = "bar"
|
||||||
|
return 'hello'
|
||||||
|
|
||||||
|
app.run()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
|
||||||
|
### Redis support
|
||||||
|
|
||||||
|
via `aioredis` or `trio-redis` (when using [Trio](https://trio.readthedocs.io/en/stable/)).
|
||||||
|
|
||||||
|
```python3
|
||||||
|
app = Quart(__name__)
|
||||||
|
app.config['SESSION_TYPE'] = 'redis'
|
||||||
|
Session(app)
|
||||||
|
```
|
||||||
|
|
||||||
|
If you already have a `aioredis.Client` instance and you'd like to share
|
||||||
|
it with the session interface,
|
||||||
|
|
||||||
|
```python3
|
||||||
|
app = Quart(__name__)
|
||||||
|
app.config['SESSION_TYPE'] = 'redis'
|
||||||
|
|
||||||
|
@app.before_serving
|
||||||
|
async def setup():
|
||||||
|
cache = await aioredis.create_redis_pool({"address": "..."})
|
||||||
|
app.config['SESSION_REDIS'] = cache
|
||||||
|
Session(app)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Trio
|
||||||
|
|
||||||
|
Quart-Session comes with a Redis client for use with the [Trio](https://trio.readthedocs.io/en/stable/) eventloop.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Memcached support
|
||||||
|
|
||||||
|
via `aiomcache`.
|
||||||
|
|
||||||
|
```python3
|
||||||
|
app = Quart(__name__)
|
||||||
|
app.config['SESSION_TYPE'] = 'memcached'
|
||||||
|
Session(app)
|
||||||
|
```
|
||||||
|
|
||||||
|
### JSON serializer
|
||||||
|
|
||||||
|
[flask-session](https://pypi.org/project/Flask-Session/) uses `pickle`
|
||||||
|
for session data, Quart-Session opts for a JSON serializer capable of
|
||||||
|
(de)serializing the usual JSON types, as well as: `Tuple`, `Bytes`,
|
||||||
|
`Markup`, `UUID`, and `DateTime`.
|
||||||
|
|
||||||
|
JSON as session data allows for greater interoperability with other
|
||||||
|
programs/languages that might want to read session data straight
|
||||||
|
from a back-end. In addition, it is more secure.
|
||||||
|
|
||||||
|
If, for some unholy reason, you prefer `pickle` or your own serializer,
|
||||||
|
|
||||||
|
```python3
|
||||||
|
app = Quart(__name__)
|
||||||
|
app.config['SESSION_TYPE'] = 'redis'
|
||||||
|
Session(app)
|
||||||
|
|
||||||
|
try:
|
||||||
|
import cPickle as pickle
|
||||||
|
except ImportError:
|
||||||
|
import pickle
|
||||||
|
|
||||||
|
app.session_interface.serialize = pickle
|
||||||
|
```
|
||||||
|
|
||||||
|
### Session control
|
||||||
|
|
||||||
|
By default, [flask-session](https://pypi.org/project/Flask-Session/) sets a
|
||||||
|
session for each incoming request, including static files. From experience,
|
||||||
|
this approach can put unneeded load on underlying session infrastructure,
|
||||||
|
especially in high-traffic environments.
|
||||||
|
|
||||||
|
Quart-Session offers control over the session creation. For example, often you'll only need to create a session when
|
||||||
|
a user successfully logs in.
|
||||||
|
|
||||||
|
To enable this behaviour, set `SESSION_EXPLICIT` to `True`.
|
||||||
|
|
||||||
|
```python3
|
||||||
|
app = Quart(__name__)
|
||||||
|
app.config['SESSION_TYPE'] = 'redis'
|
||||||
|
app.config['SESSION_EXPLICIT'] = True
|
||||||
|
Session(app)
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
async def root():
|
||||||
|
if session.get('authenticated'):
|
||||||
|
return "Welcome back!"
|
||||||
|
return "Welcome anonymous!"
|
||||||
|
|
||||||
|
@app.route('/login')
|
||||||
|
async def login():
|
||||||
|
session["authenticated"] = True
|
||||||
|
session.dirty() # mark session for saving
|
||||||
|
return 'Logged in!'
|
||||||
|
|
||||||
|
app.run()
|
||||||
|
```
|
||||||
|
|
||||||
|
To re-gain the old behaviour of always emitting a `Set-Cookie` header on static file serves,
|
||||||
|
set `SESSION_STATIC_FILE` to `True`.
|
||||||
|
|
||||||
|
|
||||||
|
### Session hijack prevention
|
||||||
|
|
||||||
|
(Optionally) pins an user's session to his/her IP address. This mitigates cookie stealing via XSS etc, and is handy
|
||||||
|
for paranoid web applications.
|
||||||
|
|
||||||
|
```python3
|
||||||
|
app = Quart(__name__)
|
||||||
|
app.config['SESSION_TYPE'] = 'redis'
|
||||||
|
app.config['SESSION_HIJACK_PROTECTION'] = True
|
||||||
|
Session(app)
|
||||||
|
```
|
||||||
|
|
||||||
|
With this option, session reuse from a different IP will result in the
|
||||||
|
creation of a new session, and the deletion of the old.
|
||||||
|
|
||||||
|
**Important:** If your application is behind a reverse proxy, it most
|
||||||
|
likely provides the `X-Forwarded-For` header which you **must** make use of
|
||||||
|
by explicitly setting `SESSION_HIJACK_REVERSE_PROXY` to `True`.
|
||||||
|
|
||||||
|
## Future development
|
||||||
|
|
||||||
|
The following session interfaces would be nice to have:
|
||||||
|
|
||||||
|
- `MongoDBSessionInterface`
|
||||||
|
- `FileSystemSessionInterface`
|
||||||
|
- `GoogleCloudDatastoreSessionInterface`
|
||||||
|
|
||||||
|
Other to-do's:
|
||||||
|
|
||||||
|
- Unit testing
|
||||||
|
- Documentation (Sphinx)
|
||||||
|
|
||||||
|
## Migrating from Flask
|
||||||
|
|
||||||
|
This library works very similarly to [flask-session](https://pypi.org/project/Flask-Session/).
|
||||||
|
The `quart_session.sessions` APIs are not 100% the same, but unless you
|
||||||
|
are embedded in Flask-Session's internals, a migration should be fairly
|
||||||
|
straightforward. The distinct changes are specified below:
|
||||||
|
|
||||||
|
- Quart-Session does not `Set-Cookie` on (static) files by default.
|
||||||
|
- Quart-Session might not have all the back-end interfaces implemented (yet), such as "filesystem".
|
||||||
|
- Quart-Session uses a different serializer: `quart.json.tag.TaggedJSONSerializer` instead of `pickle`.
|
||||||
|
- Quart-Session uses asyncio ;-)
|
||||||
|
|
||||||
|
## Help
|
||||||
|
|
||||||
|
Find the Quart folk on [gitter](https://gitter.im/python-quart/lobby) or open an issue.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
BSD
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Hello
|
||||||
|
~~~~~
|
||||||
|
|
||||||
|
Quart-Session demo.
|
||||||
|
|
||||||
|
:copyright: (c) 2020 by Sander.
|
||||||
|
:license: BSD, see LICENSE for more details.
|
||||||
|
"""
|
||||||
|
from quart import Quart, session
|
||||||
|
from quart_session import Session
|
||||||
|
|
||||||
|
|
||||||
|
SESSION_TYPE = 'redis'
|
||||||
|
|
||||||
|
|
||||||
|
app = Quart(__name__)
|
||||||
|
app.config.from_object(__name__)
|
||||||
|
Session(app)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/set/')
|
||||||
|
def set():
|
||||||
|
session['key'] = 'value'
|
||||||
|
return 'ok'
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/get/')
|
||||||
|
def get():
|
||||||
|
return session.get('key', 'not set')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app.run(debug=True)
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
quart_session
|
||||||
|
~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Adds server session support to your application.
|
||||||
|
|
||||||
|
:copyright: (c) 2014 by Shipeng Feng.
|
||||||
|
:copyright: (c) 2020 by Sander.
|
||||||
|
:license: BSD, see LICENSE for more details.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__version__ = '0.0.1'
|
||||||
|
|
||||||
|
import os
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from quart import Quart
|
||||||
|
|
||||||
|
from .sessions import RedisSessionInterface, MemcachedSessionInterface, NullSessionInterface
|
||||||
|
|
||||||
|
|
||||||
|
class Session(object):
|
||||||
|
"""This class is used to add Server-side Session to one or more Quart
|
||||||
|
applications.
|
||||||
|
|
||||||
|
There are two usage modes. One is initialize the instance with a very
|
||||||
|
specific Quart application::
|
||||||
|
|
||||||
|
app = Quart(__name__)
|
||||||
|
Session(app)
|
||||||
|
|
||||||
|
The second possibility is to create the object once and configure the
|
||||||
|
application later::
|
||||||
|
|
||||||
|
sess = Session()
|
||||||
|
|
||||||
|
def create_app():
|
||||||
|
app = Quart(__name__)
|
||||||
|
sess.init_app(app)
|
||||||
|
return app
|
||||||
|
|
||||||
|
By default Quart-Session will use :class:`NullSessionInterface`, you
|
||||||
|
really should configure your app to use a different SessionInterface.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
You can not use ``Session`` instance directly, what ``Session`` does
|
||||||
|
is just change the :attr:`~quart.Quart.session_interface` attribute on
|
||||||
|
your Quart applications.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, app: Quart = None) -> None:
|
||||||
|
self.app = app
|
||||||
|
if app is not None:
|
||||||
|
self.init_app(app)
|
||||||
|
|
||||||
|
def init_app(self, app: Quart) -> None:
|
||||||
|
"""This is used to set up session for your app object.
|
||||||
|
|
||||||
|
:param app: the Quart app object with proper configuration.
|
||||||
|
"""
|
||||||
|
app.session_interface = self._get_interface(app)
|
||||||
|
|
||||||
|
@app.before_serving
|
||||||
|
async def setup():
|
||||||
|
await app.session_interface.create(app)
|
||||||
|
|
||||||
|
def _get_interface(self, app: Quart):
|
||||||
|
config = app.config.copy()
|
||||||
|
config.setdefault('SESSION_TYPE', 'null')
|
||||||
|
config.setdefault('SESSION_PERMANENT', True)
|
||||||
|
config.setdefault('SESSION_USE_SIGNER', False)
|
||||||
|
config.setdefault('SESSION_KEY_PREFIX', 'session:')
|
||||||
|
config.setdefault('SESSION_HIJACK_PROTECTION', False)
|
||||||
|
config.setdefault('SESSION_HIJACK_REVERSE_PROXY', False)
|
||||||
|
config.setdefault('SESSION_STATIC_FILE', False)
|
||||||
|
config.setdefault('SESSION_EXPLICIT', False)
|
||||||
|
config.setdefault('SESSION_REDIS', None)
|
||||||
|
config.setdefault('SESSION_MEMCACHED', None)
|
||||||
|
config.setdefault('SESSION_FILE_DIR',
|
||||||
|
os.path.join(os.getcwd(), 'quart_session'))
|
||||||
|
config.setdefault('SESSION_FILE_THRESHOLD', 500)
|
||||||
|
config.setdefault('SESSION_FILE_MODE', 384)
|
||||||
|
config = {k: v for k, v in config.items() if k.startswith('SESSION_')}
|
||||||
|
|
||||||
|
if config['SESSION_TYPE'] == 'redis':
|
||||||
|
session_interface = RedisSessionInterface(
|
||||||
|
redis=config['SESSION_REDIS'],
|
||||||
|
key_prefix=config['SESSION_KEY_PREFIX'],
|
||||||
|
use_signer=config['SESSION_USE_SIGNER'],
|
||||||
|
permanent=config['SESSION_PERMANENT'],
|
||||||
|
**config)
|
||||||
|
elif config['SESSION_TYPE'] == 'memcached':
|
||||||
|
session_interface = MemcachedSessionInterface(
|
||||||
|
memcached=config['SESSION_MEMCACHED'],
|
||||||
|
key_prefix=config['SESSION_KEY_PREFIX'],
|
||||||
|
use_signer=config['SESSION_USE_SIGNER'],
|
||||||
|
permanent=config['SESSION_PERMANENT'],
|
||||||
|
**config)
|
||||||
|
else:
|
||||||
|
session_interface = NullSessionInterface()
|
||||||
|
|
||||||
|
return session_interface
|
||||||
@@ -0,0 +1,318 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
quart_session.sessions
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Server-side Sessions and SessionInterfaces.
|
||||||
|
|
||||||
|
:copyright: (c) 2014 by Shipeng Feng.
|
||||||
|
:copyright: (c) 2020 by Sander.
|
||||||
|
:license: BSD, see LICENSE for more details.
|
||||||
|
"""
|
||||||
|
import time
|
||||||
|
from typing import Any, Callable, Optional, TYPE_CHECKING
|
||||||
|
from uuid import uuid4
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
from quart import Quart
|
||||||
|
from quart.wrappers import BaseRequestWebsocket, Response
|
||||||
|
from quart.wrappers.response import FileBody
|
||||||
|
from quart.sessions import SessionInterface as QuartSessionInterface, SecureCookieSession
|
||||||
|
from quart.json.tag import TaggedJSONSerializer
|
||||||
|
from itsdangerous import Signer, BadSignature, want_bytes
|
||||||
|
|
||||||
|
|
||||||
|
def total_seconds(td):
|
||||||
|
return td.days * 60 * 60 * 24 + td.seconds
|
||||||
|
|
||||||
|
|
||||||
|
class ServerSideSession(SecureCookieSession):
|
||||||
|
"""Baseclass for server-side based sessions."""
|
||||||
|
|
||||||
|
def __init__(self, initial=None, sid=None, permanent=None, addr=None):
|
||||||
|
super(ServerSideSession, self).__init__(**initial or {})
|
||||||
|
self.sid = sid
|
||||||
|
if permanent:
|
||||||
|
self.permanent = permanent
|
||||||
|
if addr:
|
||||||
|
self.addr = addr
|
||||||
|
self._dirty = False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def addr(self) -> str:
|
||||||
|
return self.get('_addr', False) # type: ignore
|
||||||
|
|
||||||
|
@addr.setter
|
||||||
|
def addr(self, value: str) -> None:
|
||||||
|
self['_addr'] = value # type: ignore
|
||||||
|
|
||||||
|
def dirty(self):
|
||||||
|
"""Marks the session to be written/saved.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
This feature only works if you have set ``SESSION_EXPLICIT``
|
||||||
|
to ``True``, at which point you'll have to explicitly mark
|
||||||
|
each session before they'll get processed and saved.
|
||||||
|
|
||||||
|
Example::
|
||||||
|
|
||||||
|
app.config['SESSION_EXPLICIT'] = True
|
||||||
|
Session(app)
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
def root():
|
||||||
|
session['foo'] = 'bar'
|
||||||
|
session.dirty()
|
||||||
|
return "Hello World!"
|
||||||
|
"""
|
||||||
|
self._dirty = True
|
||||||
|
|
||||||
|
|
||||||
|
class RedisSession(ServerSideSession):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class MemcachedSession(ServerSideSession):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class NullSession(ServerSideSession):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class SessionInterface(QuartSessionInterface):
|
||||||
|
"""Baseclass for session interfaces"""
|
||||||
|
|
||||||
|
serializer = TaggedJSONSerializer()
|
||||||
|
session_class = None
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
key_prefix: str,
|
||||||
|
use_signer: bool = False,
|
||||||
|
permanent: bool = True,
|
||||||
|
**kwargs
|
||||||
|
) -> None:
|
||||||
|
self.key_prefix = key_prefix
|
||||||
|
self.use_signer = use_signer
|
||||||
|
self.permanent = permanent
|
||||||
|
self._config = kwargs
|
||||||
|
|
||||||
|
async def open_session(
|
||||||
|
self, app: Quart, request: BaseRequestWebsocket
|
||||||
|
) -> Optional[SecureCookieSession]:
|
||||||
|
sid = request.cookies.get(app.session_cookie_name)
|
||||||
|
addr = request.headers.get('X-Forwarded-For', request.remote_addr) if \
|
||||||
|
self._config['SESSION_HIJACK_PROTECTION'] else None
|
||||||
|
options = {"sid": sid, "permanent": self.permanent, "addr": addr}
|
||||||
|
|
||||||
|
if not sid:
|
||||||
|
options['sid'] = self._generate_sid()
|
||||||
|
return self.session_class(**options)
|
||||||
|
if self.use_signer:
|
||||||
|
signer = self._get_signer(app)
|
||||||
|
if signer is None:
|
||||||
|
app.logger.warning("Failed to obtain a valid signer.")
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
sid_as_bytes = signer.unsign(sid)
|
||||||
|
sid = sid_as_bytes.decode()
|
||||||
|
except BadSignature:
|
||||||
|
app.logger.warning(f"Bad signature for sid: {sid}.")
|
||||||
|
options['sid'] = self._generate_sid()
|
||||||
|
return self.session_class(**options)
|
||||||
|
|
||||||
|
val = await self._backend_get(app, self.key_prefix + sid)
|
||||||
|
if val is None:
|
||||||
|
options['sid'] = self._generate_sid()
|
||||||
|
return self.session_class(**options)
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = self.serializer.loads(val)
|
||||||
|
except:
|
||||||
|
app.logger.warning(f"Failed to deserialize session "
|
||||||
|
f"data for sid: {sid}. Generating new sid.")
|
||||||
|
options['sid'] = self._generate_sid()
|
||||||
|
return self.session_class(**options)
|
||||||
|
|
||||||
|
prevent_hijack = self._config['SESSION_HIJACK_PROTECTION']
|
||||||
|
if prevent_hijack is False:
|
||||||
|
pass
|
||||||
|
elif isinstance(prevent_hijack, bool) and \
|
||||||
|
prevent_hijack is True:
|
||||||
|
if self._config['SESSION_HIJACK_REVERSE_PROXY'] is True:
|
||||||
|
addr = request.headers.get('X-Forwarded-For', request.remote_addr)
|
||||||
|
else:
|
||||||
|
addr = request.remote_addr
|
||||||
|
if data.get('_addr', addr) != addr:
|
||||||
|
await self._backend_delete(app, self.key_prefix + sid)
|
||||||
|
options['sid'] = self._generate_sid()
|
||||||
|
return self.session_class(**options)
|
||||||
|
|
||||||
|
res = self.session_class(data, sid)
|
||||||
|
return res
|
||||||
|
|
||||||
|
async def save_session( # type: ignore
|
||||||
|
self,
|
||||||
|
app: "Quart",
|
||||||
|
session: SecureCookieSession,
|
||||||
|
response: Response
|
||||||
|
) -> None:
|
||||||
|
# prevent set-cookie
|
||||||
|
# motivation: https://github.com/fengsp/flask-session/pull/70
|
||||||
|
if self._config['SESSION_EXPLICIT'] is True and \
|
||||||
|
not session._dirty:
|
||||||
|
return
|
||||||
|
|
||||||
|
# prevent set-cookie on (static) file responses
|
||||||
|
if self._config['SESSION_STATIC_FILE'] is False and \
|
||||||
|
isinstance(response.response, FileBody):
|
||||||
|
return
|
||||||
|
session_key = self.key_prefix + session.sid
|
||||||
|
domain = self.get_cookie_domain(app)
|
||||||
|
path = self.get_cookie_path(app)
|
||||||
|
if not session:
|
||||||
|
if session.modified:
|
||||||
|
await self._backend_delete(app=app, key=session_key)
|
||||||
|
response.delete_cookie(app.session_cookie_name,
|
||||||
|
domain=domain, path=path)
|
||||||
|
return
|
||||||
|
httponly = self.get_cookie_httponly(app)
|
||||||
|
secure = self.get_cookie_secure(app)
|
||||||
|
expires = self.get_expiration_time(app, session)
|
||||||
|
|
||||||
|
val = self.serializer.dumps(dict(session))
|
||||||
|
await self._backend_set(app=app, key=session_key, value=val)
|
||||||
|
if self.use_signer:
|
||||||
|
session_id = self._get_signer(app).sign(want_bytes(session.sid))
|
||||||
|
else:
|
||||||
|
session_id = session.sid
|
||||||
|
response.set_cookie(app.session_cookie_name, session_id,
|
||||||
|
expires=expires, httponly=httponly,
|
||||||
|
domain=domain, path=path, secure=secure)
|
||||||
|
|
||||||
|
async def create(self, app: Quart):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
async def _backend_get(self, app: Quart, key: str):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
async def _backend_set(self, app: Quart, key: str, value):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
async def _backend_delete(self, app: Quart, key: str):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def _generate_sid(self) -> str:
|
||||||
|
return str(uuid4())
|
||||||
|
|
||||||
|
def _get_signer(self, app) -> Optional[Signer]:
|
||||||
|
if not app.secret_key:
|
||||||
|
return None
|
||||||
|
return Signer(app.secret_key, salt='quart-session',
|
||||||
|
key_derivation='hmac')
|
||||||
|
|
||||||
|
|
||||||
|
class RedisSessionInterface(SessionInterface):
|
||||||
|
"""Uses the Redis key-value store as a session backend.
|
||||||
|
|
||||||
|
:param redis: ``aioredis.Redis`` instance.
|
||||||
|
:param key_prefix: A prefix that is added to all Redis store keys.
|
||||||
|
:param use_signer: Whether to sign the session id cookie or not.
|
||||||
|
:param permanent: Whether to use permanent session or not.
|
||||||
|
:param kwargs: Quart-session config, used internally.
|
||||||
|
"""
|
||||||
|
|
||||||
|
session_class = RedisSession
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, redis, key_prefix: str, use_signer: bool = False,
|
||||||
|
permanent: bool = True, **kwargs):
|
||||||
|
super(RedisSessionInterface, self).__init__(
|
||||||
|
key_prefix=key_prefix, use_signer=use_signer,
|
||||||
|
permanent=permanent, **kwargs)
|
||||||
|
self.redis = redis
|
||||||
|
|
||||||
|
async def create(self, app: Quart) -> None:
|
||||||
|
if self.redis is None:
|
||||||
|
import aioredis
|
||||||
|
self.redis = await aioredis.create_redis("redis://localhost")
|
||||||
|
|
||||||
|
async def _backend_get(self, app: Quart, key: str):
|
||||||
|
return await self.redis.get(key)
|
||||||
|
|
||||||
|
async def _backend_set(self, app: Quart, key: str, value):
|
||||||
|
return await self.redis.setex(
|
||||||
|
key=key, value=value,
|
||||||
|
seconds=total_seconds(app.permanent_session_lifetime))
|
||||||
|
|
||||||
|
async def _backend_delete(self, app: Quart, key: str):
|
||||||
|
return await self.redis.delete(key)
|
||||||
|
|
||||||
|
|
||||||
|
class MemcachedSessionInterface(SessionInterface):
|
||||||
|
"""Uses the Memcached key-value store as a session backend.
|
||||||
|
|
||||||
|
:param client: ``aiomcache.Client`` instance.
|
||||||
|
:param key_prefix: A prefix that is added to all Redis store keys.
|
||||||
|
:param use_signer: Whether to sign the session id cookie or not.
|
||||||
|
:param permanent: Whether to use permanent session or not.
|
||||||
|
:param kwargs: Quart-session config, used internally.
|
||||||
|
"""
|
||||||
|
|
||||||
|
session_class = MemcachedSession
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, memcached, key_prefix: str, use_signer: bool = False,
|
||||||
|
permanent: bool = True, **kwargs):
|
||||||
|
super(MemcachedSessionInterface, self).__init__(
|
||||||
|
key_prefix=key_prefix, use_signer=use_signer,
|
||||||
|
permanent=permanent, **kwargs)
|
||||||
|
self.memcached = memcached
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def create(self, app: Quart) -> None:
|
||||||
|
if self.memcached is None:
|
||||||
|
import aiomcache
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
self.memcached = aiomcache.Client("127.0.0.1", 11211, loop=loop)
|
||||||
|
|
||||||
|
def _get_memcache_timeout(self, timeout):
|
||||||
|
"""
|
||||||
|
Memcached deals with long (> 30 days) timeouts in a special
|
||||||
|
way. Call this function to obtain a safe value for your timeout.
|
||||||
|
"""
|
||||||
|
if timeout > 2592000: # 60*60*24*30, 30 days
|
||||||
|
# See http://code.google.com/p/memcached/wiki/FAQ
|
||||||
|
# "You can set expire times up to 30 days in the future. After that
|
||||||
|
# memcached interprets it as a date, and will expire the item after
|
||||||
|
# said date. This is a simple (but obscure) mechanic."
|
||||||
|
#
|
||||||
|
# This means that we have to switch to absolute timestamps.
|
||||||
|
timeout += int(time.time())
|
||||||
|
return timeout
|
||||||
|
|
||||||
|
async def _backend_get(self, app: Quart, key: str):
|
||||||
|
key = key.encode()
|
||||||
|
return await self.memcached.get(key)
|
||||||
|
|
||||||
|
async def _backend_set(self, app: Quart, key: str, value):
|
||||||
|
key = key.encode()
|
||||||
|
value = value.encode()
|
||||||
|
expiry = self._get_memcache_timeout(total_seconds(
|
||||||
|
app.permanent_session_lifetime))
|
||||||
|
return await self.memcached.set(key=key, value=value,
|
||||||
|
exptime=expiry)
|
||||||
|
|
||||||
|
async def _backend_delete(self, app: Quart, key: str):
|
||||||
|
key = key.encode()
|
||||||
|
return await self.memcached.delete(key)
|
||||||
|
|
||||||
|
|
||||||
|
class NullSessionInterface(SessionInterface):
|
||||||
|
"""Used to open a :class:`quart.sessions.NullSession` instance.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def open_session(self, app: Quart, request: BaseRequestWebsocket):
|
||||||
|
return None
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
"""
|
||||||
|
Quart-Session
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Quart-Session is an extension for Quart that adds support for
|
||||||
|
Server-side Session to your application.
|
||||||
|
|
||||||
|
Links
|
||||||
|
`````
|
||||||
|
|
||||||
|
* `Github
|
||||||
|
<https://github.com/sferdi0/quart-session>`_
|
||||||
|
|
||||||
|
"""
|
||||||
|
from setuptools import setup
|
||||||
|
|
||||||
|
|
||||||
|
INSTALL_REQUIRES = [
|
||||||
|
"Quart>=0.10.0"
|
||||||
|
]
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name='Quart-Session',
|
||||||
|
version='0.0.1',
|
||||||
|
url='https://github.com/sferdi0/quart-session',
|
||||||
|
license='BSD',
|
||||||
|
author='Sander',
|
||||||
|
author_email='sander@sanderf.nl',
|
||||||
|
description='Adds server-side session support to your Quart application',
|
||||||
|
long_description=__doc__,
|
||||||
|
packages=['quart_session'],
|
||||||
|
zip_safe=False,
|
||||||
|
include_package_data=True,
|
||||||
|
platforms='any',
|
||||||
|
install_requires=INSTALL_REQUIRES,
|
||||||
|
tests_require=INSTALL_REQUIRES + ["asynctest", "hypothesis", "pytest", "pytest-asyncio"],
|
||||||
|
extras_require={"dotenv": ["python-dotenv"]},
|
||||||
|
classifiers=[
|
||||||
|
'Environment :: Web Environment',
|
||||||
|
'Intended Audience :: Developers',
|
||||||
|
'License :: OSI Approved :: BSD License',
|
||||||
|
'Operating System :: OS Independent',
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"Programming Language :: Python :: 3.7",
|
||||||
|
"Programming Language :: Python :: 3.8",
|
||||||
|
'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
|
||||||
|
'Topic :: Software Development :: Libraries :: Python Modules'
|
||||||
|
]
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user