43 Commits

Author SHA1 Message Date
daniel156161 b3105423f8 fix that it can set the responce cookie correctly for newer werkzeug version 2025-09-18 18:16:18 +02:00
Kroket Ltd 249a7abd89 Bump to 3.0.0 2023-11-24 04:12:03 +02:00
Kroket Ltd 093d28fe62 Merge pull request #19 from jonathonfletcher/master
do not unpack initial in call to ServerSideSession's super.
2023-11-24 04:07:24 +02:00
Jonathon Fletcher 7e43c76ad0 do not unpack initial in call to ServerSideSession's super. 2023-11-04 11:50:46 -07:00
Kroket Ltd c25a62412d Merge pull request #16 from kroketio/session_cookie_name_fix
Session cookie name fix
2023-10-17 01:42:31 +03:00
Kroket Ltd 9186fae9e7 bump version to 2.1.0 2023-10-17 01:41:24 +03:00
Kroket Ltd 31a56ddd47 session_cookie_name fix due to https://github.com/pallets/flask/pull/4995/files 2023-10-17 01:40:11 +03:00
Sander 6ad2a3789d Update CHANGELOG, bump version 2023-01-16 19:31:09 +02:00
Sander d08156f193 Merge pull request #12 from degenhard/master
Change for python 3.11 aioredis to redis.
2023-01-16 12:17:46 +02:00
Peter Heise 39e5fc5c13 Updated setup.py redis requirement. 2023-01-16 10:16:33 +01:00
Peter Heise 237f2354ad Changed redis package in readme. 2023-01-15 21:42:18 +01:00
Peter Heise 5119f60434 Changed aioredis import for python 3.11 2023-01-15 21:38:47 +01:00
Kroket Ltd 2f53caa654 Bump version 2022-11-24 17:14:14 +01:00
Sander cc99a72901 Merge pull request #11 from adrienyhuel/master
Fix MemcachedSession
2022-11-24 17:09:45 +01:00
Adrien YHUEL 5301b4418f Fix MemcachedSession
Remove asyncio loop argument
Remove asyncio.coroutine annotation as it is removed in Python 3.11
2022-11-24 15:10:48 +01:00
Kroket Ltd 3e6b102b34 Transfer copyright, bump version 2022-11-13 03:36:15 +02:00
Sander 65b44db7df Merge pull request #10 from smithk86/mongodb
add session handler for mongodb: MongoDBSessionInterface
2022-03-20 12:55:20 +02:00
Kyle Smith b9f2dc0067 add session handler for mongodb: MongoDBSessionInterface 2022-03-17 15:51:45 -04:00
Sander a19f227d88 Merge pull request #9 from sanderfoobar/self-cfg-update-version
dont use app.config, bump version
2022-03-10 15:01:28 +02:00
Sander 53a82fda3f Bump version 2022-03-10 15:00:32 +02:00
Sander a82799d358 use self._config instead of app.config 2022-03-10 14:59:32 +02:00
Sander 80f39ec79b Merge pull request #8 from sanderfoobar/session-uri-docs
Change README to support new config option `SESSION_URI`
2022-03-10 14:55:03 +02:00
Sander 9d02429aee Change README to support new config option SESSION_URI 2022-03-10 14:54:41 +02:00
Sander 13fcc653c6 Merge pull request #7 from rubikscuber/patch-1
respect custom redis uri
2022-03-10 14:52:40 +02:00
rubikscuber 16fb57d62a add SESSION_URI example 2022-03-10 13:51:13 +01:00
rubikscuber 43956f045f Update sessions.py 2022-03-10 13:45:28 +01:00
rubikscuber cdd237b5f9 respect custom redis uri
enable usage of app.config["REDIS_URI"]
2022-03-10 11:28:03 +01:00
sander 38bf10d3d2 Bump version to 1.0.3 2021-08-31 22:41:47 +02:00
Sander cb9c6339e5 Merge pull request #6 from adrienyhuel/master
Make quart-session compatible with aioredis 2.0
2021-08-31 21:22:06 +02:00
Adrien YHUEL 264a9ad369 Make quart-session compatible with aioredis 2.0 2021-08-31 04:33:41 +02:00
sferdi0 ff41a72222 Merge pull request #5 from filak/master
SameSite support and Memcached config
2021-05-19 22:44:21 +02:00
filak 8daae3a673 SameSite support 2021-05-12 19:22:17 +02:00
filakx 004871c495 Memcached config 2021-04-20 23:13:20 +02:00
Sander 6d9ebbb264 Correctly bump version 2020-12-09 15:39:42 +01:00
Sander e136ab0101 Actually implement a working NullSessionInterface, fixes pgjones/quart#105, bump version 2020-12-09 15:39:42 +01:00
Sander 51668878df Release 1.0.0
- Added support for arbitrary usage of caching backends.
    - Exposed `get`, `set`, `delete` on the session interface for direct usage.
- Renamed `SESSION_HIJACK_REVERSE_PROXY` to `SESSION_REVERSE_PROXY`.
- Renamed `SESSION_HIJACK_PROTECTION` to `SESSION_PROTECTION`.
- Removed fallback when `X-Forwarded-For` is not present whilst USING `SESSION_REVERSE_PROXY`, emit error instead.
- Fixed a bug where session timeouts would default to 600 seconds.
- Deprecated/disabled the `dirty()` method.
2020-12-09 15:39:42 +01:00
Sander 45b8147b0a Add link to Quart repository on Gitlab 2020-12-09 15:39:42 +01:00
Sander b52e896ef0 Refactor addr 2020-12-09 15:39:42 +01:00
Sander 4012836eee Update example 2020-12-09 15:39:42 +01:00
Sander ad2a1db6c5 Release 0.0.1 2020-12-09 15:39:42 +01:00
Sander f05c49105e Update README 2020-12-09 15:39:42 +01:00
Sander 0f9ba5052c Redis+Trio support 2020-12-09 15:39:42 +01:00
Sander fb5678afbb Initial commit 2020-12-09 15:39:42 +01:00
8 changed files with 379 additions and 186 deletions
+36
View File
@@ -0,0 +1,36 @@
### 2.1.0
- `session_cookie_name` fix
### 2.0.0
- Move from aioredis to redis
### 1.0.7
- Updated memcached support (removed asyncio.loop, removed coroutine decorator)
### 1.0.6
- MongoDB support
- Transfer copyright to Kroket Ltd.
### 1.0.3 2021-08-31
- Migrated to aioredis 2
- SameSite support https://github.com/kroketio/quart-session/commit/8daae3a6734e8f7da13954d5a1a5da8f5fc5a49a
- Memcached stuff https://github.com/filak/quart-session/commit/004871c495a069784e57e604b69f65af1b7e645a
### 1.0.0 2020-01-15
- Added support for arbitrary usage of caching backends.
- Exposed `get`, `set`, `delete` on the session interface for direct usage.
- Renamed `SESSION_HIJACK_REVERSE_PROXY` to `SESSION_REVERSE_PROXY`.
- Renamed `SESSION_HIJACK_PROTECTION` to `SESSION_PROTECTION`.
- Removed fallback when `X-Forwarded-For` is not present whilst USING `SESSION_REVERSE_PROXY`, emit error instead.
- Fixed a bug where session timeouts would default to 600 seconds.
- Deprecated/disabled the `dirty()` method.
### 0.0.1 2020-01-04
- Released initial pre alpha version.
+1 -1
View File
@@ -1,5 +1,5 @@
Copyright (c) 2014 by Shipeng Feng.
Copyright (c) 2020 by dsc.
Copyright (c) 2020 by Kroket Ltd.
Some rights reserved.
+1
View File
@@ -7,3 +7,4 @@ recursive-include quart_session *.md
exclude .gitlab-ci.yml
exclude examples
exclude docs
exclude venv
+73 -63
View File
@@ -2,22 +2,21 @@
![pyversions](https://img.shields.io/pypi/pyversions/Quart-Session.svg) [![pypiversion](https://badge.fury.io/py/Quart-Session.svg)](https://pypi.org/project/Quart-Session/) ![PyPI license](https://img.shields.io/pypi/l/Quart-Session.svg)
Quart-Session is an extension for Quart that adds support for
Quart-Session is an extension for [Quart](https://gitlab.com/pgjones/quart/blob/master/README.rst) 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,
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,
and requires Python 3.7.0 or higher. A minimal Quart-Session example is:
```python3
from quart import Quart, session
@@ -30,7 +29,11 @@ Session(app)
@app.route('/')
async def hello():
session["foo"] = "bar"
return 'hello'
return "session key 'foo' set"
@app.route('/foo')
async def foo():
return session.get("foo", "session key 'foo' not found")
app.run()
```
@@ -38,9 +41,9 @@ app.run()
## Features
### Redis support
### Redis
via `aioredis`.
via `redis>=4.4.0`.
```python3
app = Quart(__name__)
@@ -48,8 +51,16 @@ 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,
By default, Quart-session connects to Redis at `127.0.0.1:6379`. If you
have a different location, use `SESSION_URI`
```python3
app = Quart(__name__)
app.config['SESSION_TYPE'] = 'redis'
app.config['SESSION_URI'] = 'redis://:password@localhost:6379'
```
Alternatively, for extra control, you may provide your own `aioredis.Client` instance altogether.
```python3
app = Quart(__name__)
@@ -57,15 +68,17 @@ app.config['SESSION_TYPE'] = 'redis'
@app.before_serving
async def setup():
cache = await aioredis.create_redis_pool(...)
cache = await aioredis.Redis(
host="foobar.com",
port=6379,
password="foobar"
)
app.config['SESSION_REDIS'] = cache
Session(app)
```
By default, Quart-session creates a single connection to Redis, while
the example above sets up a connection pool.
#### Trio support
#### Trio
Quart-Session comes with [an (experimental) Redis client](quart_session/redis_trio) for use with the [Trio](https://trio.readthedocs.io/en/stable/) eventloop.
@@ -78,7 +91,7 @@ app.config['SESSION_TYPE'] = 'redis'
Session(app)
```
### Memcached support
### Memcached
via `aiomcache`.
@@ -88,11 +101,23 @@ app.config['SESSION_TYPE'] = 'memcached'
Session(app)
```
### MongoDB
via `motor`.
```python3
app = Quart(__name__)
app.config['SESSION_TYPE'] = 'mongodb'
app.config['SESSION_MONGODB_URI'] = 'mongodb://localhost:27017/my_database'
app.config['SESSION_MONGODB_COLLECTION'] = 'sessions'
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`,
for session data while Quart-Session uses [a JSON serializer](https://gitlab.com/pgjones/quart/blob/37e249b9b146824a8668eaa1daa12392aeb00256/src/quart/json/tag.py#L141)
capable of serializing the usual JSON types, as well as: `Tuple`, `Bytes`,
`Markup`, `UUID`, and `DateTime`.
JSON as session data allows for greater interoperability with other
@@ -114,52 +139,44 @@ except ImportError:
app.session_interface.serialize = pickle
```
### Session control
### Back-end usage
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`.
At any point you may interface with the session back-end directly:
```python3
app = Quart(__name__)
app.config['SESSION_TYPE'] = 'redis'
app.config['SESSION_EXPLICIT'] = True
Session(app)
from quart_session.sessions import SessionInterface
@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()
@app.route("/")
async def hello():
cache: SessionInterface = app.session_interface
await cache.set("random_key", "val", expiry=3600)
data = await cache.get("random_key")
```
To re-gain the old behaviour of always emitting a `Set-Cookie` header on static file serves,
set `SESSION_STATIC_FILE` to `True`.
The interface will have the `get`, `set`, and `delete` methods available (regardless of
back-end - similar to how [aiocache](https://github.com/argaen/aiocache) works).
### Performance
[flask-session](https://pypi.org/project/Flask-Session/) sets a
session for each incoming request, including static files. From experience,
this often puts unneeded load on underlying session infrastructure,
especially in high-traffic environments.
Quart-Session only contacts the back-end when a session changed (or created). In addition,
static file serves never emit a `Set-Cookie` header. If you'd like to enable
this though, set `SESSION_STATIC_FILE` to `True`.
### Session pinning
Associates an user's session to his/her IP address. This mitigates cookie stealing via XSS etc, and is handy
for paranoid web applications.
for web applications that require extra security.
```python3
app = Quart(__name__)
app.config['SESSION_TYPE'] = 'redis'
app.config['SESSION_HIJACK_PROTECTION'] = True
app.config['SESSION_PROTECTION'] = True
Session(app)
```
@@ -167,32 +184,25 @@ Session reuse from a different IP will now result in the creation of a new sessi
**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`.
by explicitly setting `SESSION_REVERSE_PROXY` to `True`.
## Future development
The following session interfaces would be nice to have:
- `MongoDBSessionInterface`
- `FileSystemSessionInterface`
- `GoogleCloudDatastoreSessionInterface`
- Pytest
Other to-do's:
- Unit testing
- Documentation (Sphinx)
## Migrating from Flask
## Flask-Session
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:
The 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 does not emit a `Set-Cookie` on every request.
- Quart-Session does not emit a `Set-Cookie` on static file serves.
- Quart-Session uses a different serializer: `quart.json.tag.TaggedJSONSerializer` instead of `pickle`.
- Quart-Session disallows the client to supply their own made up `sid` cookie value.
- Quart-Session can do session protection.
- Quart-Session might not have all the back-end interfaces implemented (yet), such as "filesystem".
## Help
+3 -3
View File
@@ -5,7 +5,7 @@
Quart-Session demo.
:copyright: (c) 2020 by dsc.
:copyright: (c) 2020 by Kroket Ltd.
:license: BSD, see LICENSE for more details.
"""
from quart import Quart, session
@@ -21,13 +21,13 @@ Session(app)
@app.route('/set/')
def set():
async def set():
session['key'] = 'value'
return 'ok'
@app.route('/get/')
def get():
async def get():
return session.get('key', 'not set')
+42 -7
View File
@@ -6,18 +6,23 @@
Adds server session support to your application.
:copyright: (c) 2014 by Shipeng Feng.
:copyright: (c) 2020 by dsc.
:copyright: (c) 2020 by Kroket Ltd.
:license: BSD, see LICENSE for more details.
"""
__version__ = '0.0.1'
__version__ = '3.0.0'
import os
from typing import Optional
from quart import Quart
from .sessions import RedisSessionInterface, RedisTrioSessionInterface, MemcachedSessionInterface, NullSessionInterface
from .sessions import (
RedisSessionInterface,
RedisTrioSessionInterface,
MemcachedSessionInterface,
MongoDBSessionInterface,
NullSessionInterface
)
class Session(object):
@@ -79,8 +84,8 @@ class Session(object):
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_PROTECTION', False)
config.setdefault('SESSION_REVERSE_PROXY', False)
config.setdefault('SESSION_STATIC_FILE', False)
config.setdefault('SESSION_EXPLICIT', False)
config.setdefault('SESSION_REDIS', None)
@@ -91,6 +96,18 @@ class Session(object):
config.setdefault('SESSION_FILE_MODE', 384)
config = {k: v for k, v in config.items() if k.startswith('SESSION_')}
if isinstance(config.get("SESSION_HIJACK_PROTECTION"), bool):
app.logger.warning("Deprecation: `SESSION_HIJACK_PROTECTION` "
"has been renamed to `SESSION_PROTECTION`")
if isinstance(config.get("SESSION_HIJACK_REVERSE_PROXY"), str):
app.logger.warning("Deprecation: `SESSION_HIJACK_REVERSE_PROXY` "
"has been renamed to `SESSION_REVERSE_PROXY`")
backend_warning = f"Please specify a session backend. " \
f"Available interfaces: redis, redis+trio, " \
f"memcached, null. e.g: app.config['SESSION_TYPE'] = 'redis'"
if config['SESSION_TYPE'] == 'redis':
options = {
"redis": config['SESSION_REDIS'],
@@ -122,7 +139,25 @@ class Session(object):
use_signer=config['SESSION_USE_SIGNER'],
permanent=config['SESSION_PERMANENT'],
**config)
elif config['SESSION_TYPE'] == 'mongodb':
session_interface = MongoDBSessionInterface(
mongodb_uri=config['SESSION_MONGODB_URI'],
collection=config['SESSION_MONGODB_COLLECTION'],
client_kwargs=config.get('SESSION_MONGODB_CLIENT_KWARGS', {}),
set_callback=config.get('SESSION_MONGODB_SET_CALLBACK'),
key_prefix=config['SESSION_KEY_PREFIX'],
use_signer=config['SESSION_USE_SIGNER'],
permanent=config['SESSION_PERMANENT'],
**config)
elif config['SESSION_TYPE'] == 'null':
app.logger.warning(f"{backend_warning}. Currently using: null")
session_interface = NullSessionInterface(
key_prefix=config['SESSION_KEY_PREFIX'],
use_signer=config['SESSION_USE_SIGNER'],
permanent=config['SESSION_PERMANENT'],
**config)
else:
session_interface = NullSessionInterface()
raise NotImplementedError(f"No such session interface "
f"\"{config['SESSION_TYPE']}\". {backend_warning}")
return session_interface
+210 -103
View File
@@ -6,15 +6,16 @@
Server-side Sessions and SessionInterfaces.
:copyright: (c) 2014 by Shipeng Feng.
:copyright: (c) 2020 by dsc.
:copyright: (c) 2020 by Kroket Ltd.
:license: BSD, see LICENSE for more details.
"""
import time
from typing import Any, Callable, Optional, TYPE_CHECKING
from uuid import uuid4
from typing import Optional
from uuid import uuid4, UUID
import asyncio
import functools
from quart import Quart
from quart import Quart, current_app
from quart.wrappers import BaseRequestWebsocket, Response
from quart.wrappers.response import FileBody
from quart.sessions import SessionInterface as QuartSessionInterface, SecureCookieSession
@@ -30,13 +31,18 @@ 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 {})
super(ServerSideSession, self).__init__(initial or {})
self.sid = sid
if permanent:
self.permanent = permanent
if addr:
self.addr = addr
self._dirty = False
self.modified = False
def dirty(self):
current_app.logger.warning("Deprecation: `dirty()` has "
"been made obsolete. Will be "
"removed soon^tm.")
@property
def addr(self) -> str:
@@ -46,28 +52,6 @@ class ServerSideSession(SecureCookieSession):
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
@@ -77,6 +61,10 @@ class MemcachedSession(ServerSideSession):
pass
class MongoDBSession(ServerSideSession):
pass
class NullSession(ServerSideSession):
pass
@@ -104,9 +92,17 @@ class SessionInterface(QuartSessionInterface):
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
cname = app.config.get('SESSION_COOKIE_NAME', 'session')
sid = request.cookies.get(cname)
if self._config['SESSION_REVERSE_PROXY'] is True:
# and no, you cannot define your own incoming
# header, stick to standards :-)
addr = request.headers.get('X-Forwarded-For')
if not addr:
app.logger.error("Could not grab IP from reverse proxy, "
"session protection is DISABLED!")
else:
addr = request.remote_addr
options = {"sid": sid, "permanent": self.permanent, "addr": addr}
if not sid:
@@ -125,30 +121,30 @@ class SessionInterface(QuartSessionInterface):
options['sid'] = self._generate_sid()
return self.session_class(**options)
val = await self._backend_get(app, self.key_prefix + sid)
val = await self.get(key=self.key_prefix + sid, app=app)
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 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)
if self.serializer is None:
data = val
else:
try:
data = self.serializer.loads(val)
except:
app.logger.warning(f"Failed to deserialize session "
f"data for sid: {sid}. Generating new sid.")
app.logger.debug(f"data: {val}")
options['sid'] = self._generate_sid()
return self.session_class(**options)
protection = self._config['SESSION_PROTECTION']
if protection is True and addr is not None and \
data.get('_addr', addr) != addr:
await self.delete(key=self.key_prefix + sid, app=app)
options['sid'] = self._generate_sid()
return self.session_class(**options)
res = self.session_class(data, sid)
return res
@@ -158,9 +154,8 @@ class SessionInterface(QuartSessionInterface):
session: SecureCookieSession,
response: Response
) -> None:
# prevent set-cookie
if self._config['SESSION_EXPLICIT'] is True and \
not session._dirty:
# prevent set-cookie on unmodified session objects
if not session.modified:
return
# prevent set-cookie on (static) file responses
@@ -168,39 +163,47 @@ class SessionInterface(QuartSessionInterface):
if self._config['SESSION_STATIC_FILE'] is False and \
isinstance(response.response, FileBody):
return
cname = app.config.get('SESSION_COOKIE_NAME', 'session')
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,
await self.delete(key=session_key, app=app)
response.delete_cookie(cname,
domain=domain, path=path)
return
httponly = self.get_cookie_httponly(app)
samesite = self.get_cookie_samesite(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.serializer is None:
val = dict(session)
else:
val = self.serializer.dumps(dict(session))
await self.set(key=session_key, value=val, app=app)
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,
response.set_cookie(cname, session_id.decode('utf-8'),
expires=expires, httponly=httponly,
domain=domain, path=path, secure=secure)
domain=domain, path=path, secure=secure, samesite=samesite)
async def create(self, app: Quart):
raise NotImplementedError()
async def _backend_get(self, app: Quart, key: str):
async def get(self, key: str, app: Quart = None):
raise NotImplementedError()
async def _backend_set(self, app: Quart, key: str, value):
async def set(self, key: str, value, expiry: int = None,
app: Quart = None):
raise NotImplementedError()
async def _backend_delete(self, app: Quart, key: str):
async def delete(self, key: str, app: Quart = None):
raise NotImplementedError()
def _generate_sid(self) -> str:
@@ -227,7 +230,7 @@ class RedisSessionInterface(SessionInterface):
def __init__(self, redis, **kwargs):
super(RedisSessionInterface, self).__init__(**kwargs)
self.redis = redis
self.backend = redis
async def create(self, app: Quart) -> None:
"""Creates ``aioredis.Redis`` instance.
@@ -235,22 +238,29 @@ class RedisSessionInterface(SessionInterface):
.. note::
Creates a single Redis connection, you might prefer
pooling instead (see ``aioredis.Redis.create_redis_pool``)
pooling instead (see the `aioredis` documentation
for connection pool examples).
"""
if self.redis is None:
import aioredis
self.redis = await aioredis.create_redis("redis://localhost")
if self.backend is None:
from redis import asyncio as aioredis
uri = self._config.get('SESSION_URI', 'redis://localhost')
self.backend = await aioredis.from_url(
uri, encoding="utf-8", decode_responses=True
)
async def _backend_get(self, app: Quart, key: str):
return await self.redis.get(key)
async def get(self, key: str, app: Quart = None):
return await self.backend.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 set(self, key: str, value, expiry: int = None,
app: Quart = None):
if app and not expiry:
expiry = total_seconds(app.permanent_session_lifetime)
return await self.backend.setex(
name=key, value=value,
time=expiry)
async def _backend_delete(self, app: Quart, key: str):
return await self.redis.delete(key)
async def delete(self, key: str, app: Quart = None):
return await self.backend.delete(key)
class RedisTrioSessionInterface(SessionInterface):
@@ -267,7 +277,7 @@ class RedisTrioSessionInterface(SessionInterface):
def __init__(self, redis, **kwargs):
super(RedisTrioSessionInterface, self).__init__(**kwargs)
self.redis_trio = redis
self.backend = redis
async def create(self, app: Quart) -> None:
"""Creates ``aioredis.Redis`` instance.
@@ -277,23 +287,26 @@ class RedisTrioSessionInterface(SessionInterface):
Creates a single Redis connection. Pooling not
supported yet for ``RedisTrio``.
"""
if self.redis_trio is None:
if self.backend is None:
from quart_session.redis_trio import RedisTrio
self.redis_trio = RedisTrio()
await self.redis_trio.connect()
self.backend = RedisTrio()
await self.backend.connect()
async def _backend_get(self, app: Quart, key: str):
data = await self.redis_trio.get(key)
async def get(self, key: str, app: Quart = None):
data = await self.backend.get(key)
if data:
return data.decode()
async def _backend_set(self, app: Quart, key: str, value):
return await self.redis_trio.setex(
async def set(self, key: str, value, expiry: int = None,
app: Quart = None):
if app and not expiry:
expiry = total_seconds(app.permanent_session_lifetime)
return await self.backend.setex(
key=key, value=value,
seconds=total_seconds(app.permanent_session_lifetime))
seconds=expiry)
async def _backend_delete(self, app: Quart, key: str):
return await self.redis_trio.delete(key)
async def delete(self, key: str, app: Quart = None):
return await self.backend.delete(key)
class MemcachedSessionInterface(SessionInterface):
@@ -314,14 +327,14 @@ class MemcachedSessionInterface(SessionInterface):
super(MemcachedSessionInterface, self).__init__(
key_prefix=key_prefix, use_signer=use_signer,
permanent=permanent, **kwargs)
self.memcached = memcached
self.backend = memcached
@asyncio.coroutine
def create(self, app: Quart) -> None:
if self.memcached is None:
async def create(self, app: Quart) -> None:
if self.backend is None:
import aiomcache
loop = asyncio.get_running_loop()
self.memcached = aiomcache.Client("127.0.0.1", 11211, loop=loop)
# self.backend = aiomcache.Client("127.0.0.1", 11211)
self.backend = aiomcache.Client(self._config.get('SESSION_MEMCACHED_HOST', '127.0.0.1'),
self._config.get('SESSION_MEMCACHED_PORT', 11211))
def _get_memcache_timeout(self, timeout):
"""
@@ -338,26 +351,120 @@ class MemcachedSessionInterface(SessionInterface):
timeout += int(time.time())
return timeout
async def _backend_get(self, app: Quart, key: str):
async def get(self, key: str, app: Quart = None):
key = key.encode()
return await self.memcached.get(key)
return await self.backend.get(key)
async def set(self, key: str, value, expiry: int = None,
app: Quart = None):
if app and not expiry:
expiry = self._get_memcache_timeout(
total_seconds(app.permanent_session_lifetime))
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)
return await self.backend.set(key=key, value=value,
exptime=expiry)
async def _backend_delete(self, app: Quart, key: str):
async def delete(self, key: str, app: Quart = None):
key = key.encode()
return await self.memcached.delete(key)
return await self.backend.delete(key)
def _convert_key_to_uuid(func):
"""
convert the session UUID to a UUID object for mongodb
example:
"session:b8ebbf02-cc7a-4b0b-824f-22a984c8c0b8" ->
UUID("b8ebbf02-cc7a-4b0b-824f-22a984c8c0b8")
"""
@functools.wraps(func)
async def wrapper(*args, **kwargs):
if 'key' in kwargs:
key = kwargs['key']
try:
if key.startswith('session:'):
_, _uuid = tuple(key.split(':'))
kwargs['key'] = UUID(_uuid)
except Exception as e:
current_app.logger.warning(
f"session could not be converted to a uuid object: {key}"
)
return await func(*args, **kwargs)
return wrapper
class MongoDBSessionInterface(SessionInterface):
# mongodb does not a serializer as many object types are properly handled by the connector
serializer = None
session_class = MongoDBSession
def __init__(self, mongodb_uri, collection, client_kwargs={}, set_callback=None, **kwargs):
from motor.motor_asyncio import AsyncIOMotorClient
super().__init__(**kwargs)
self.mongodb_uri = mongodb_uri
self.client_kwargs = client_kwargs
self.set_callback = set_callback
self._collection = collection
self._client = AsyncIOMotorClient(self.mongodb_uri, uuidRepresentation='standard', **self.client_kwargs)
self._database = self._client.get_database()
async def create(self, app: Quart) -> None:
pass
@_convert_key_to_uuid
async def get(self, key, app):
value = await self.collection.find_one({'_id': key}, {'data': True})
if value:
return value.get('data', {})
else:
return None
@_convert_key_to_uuid
async def set(self, key, value, expiry=None, app=None):
doc = {
'data': value,
}
# allows the document to be modified prior upsert
if callable(self.set_callback):
self.set_callback(doc)
await self.collection.update_one({
'_id': key
}, {
'$set': doc
},
upsert=True
)
@property
def collection(self):
return self._database.get_collection(self._collection)
class NullSessionInterface(SessionInterface):
"""Used to open a :class:`quart.sessions.NullSession` instance.
"""
"""This class does absolutely nothing"""
session_class = NullSession
def open_session(self, app: Quart, request: BaseRequestWebsocket):
return None
def __init__(
self, key_prefix: str, use_signer: bool = False,
permanent: bool = True, **kwargs):
super(NullSessionInterface, self).__init__(
key_prefix=key_prefix, use_signer=use_signer,
permanent=permanent, **kwargs)
self.backend = None
async def create(self, app: Quart) -> None:
pass
async def get(self, key: str, app: Quart = None) -> None:
pass
async def set(self, key: str, value, expiry: int = None,
app: Quart = None) -> None:
pass
+11 -7
View File
@@ -9,7 +9,7 @@ Links
`````
* `Github
<https://github.com/xmrdsc/quart-session>`_
<https://github.com/kroketio/quart-session>`_
"""
from setuptools import setup
@@ -19,16 +19,16 @@ with open('README.md') as f:
INSTALL_REQUIRES = [
"Quart>=0.10.0"
"Quart>=0.19.0"
]
setup(
name='Quart-Session',
version='0.0.1',
url='https://github.com/xmrdsc/quart-session',
version='3.0.1',
url='https://github.com/kroketio/quart-session',
license='BSD',
author='dsc',
author_email='dsc@xmr.pm',
author='Kroket Ltd.',
author_email='code@kroket.io',
description='Adds server-side session support to your Quart application',
long_description=long_description,
long_description_content_type='text/markdown',
@@ -38,7 +38,11 @@ setup(
platforms='any',
install_requires=INSTALL_REQUIRES,
tests_require=INSTALL_REQUIRES + ["asynctest", "hypothesis", "pytest", "pytest-asyncio"],
extras_require={"dotenv": ["python-dotenv"]},
extras_require={
"dotenv": ["python-dotenv"],
"mongodb": ["motor>=2.5.1"],
"redis": ["redis>=4.4.0"]
},
classifiers=[
'Environment :: Web Environment',
'Intended Audience :: Developers',