mangono-addon-redis_session_store 2.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mangono_addon_redis_session_store-2.2.0.dist-info/METADATA +32 -0
- mangono_addon_redis_session_store-2.2.0.dist-info/RECORD +14 -0
- mangono_addon_redis_session_store-2.2.0.dist-info/WHEEL +5 -0
- mangono_addon_redis_session_store-2.2.0.dist-info/entry_points.txt +6 -0
- mangono_addon_redis_session_store-2.2.0.dist-info/top_level.txt +1 -0
- odoo/addons/redis_session_store/__init__.py +34 -0
- odoo/addons/redis_session_store/__manifest__.py +18 -0
- odoo/addons/redis_session_store/env_config.py +73 -0
- odoo/addons/redis_session_store/environ_odoo_config/__init__.py +0 -0
- odoo/addons/redis_session_store/environ_odoo_config/auto_load.py +8 -0
- odoo/addons/redis_session_store/environ_odoo_config/mapper.py +17 -0
- odoo/addons/redis_session_store/json_encoding.py +42 -0
- odoo/addons/redis_session_store/odoo_monkey_patch.py +74 -0
- odoo/addons/redis_session_store/redis_session.py +240 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mangono-addon-redis_session_store
|
|
3
|
+
Version: 2.2.0
|
|
4
|
+
Project-URL: Homepage, http://mangono.fr/
|
|
5
|
+
Author-Email: mangono <opensource+odoo@mangono.fr>
|
|
6
|
+
License-Expression: AGPL-3.0
|
|
7
|
+
Classifier: Framework :: Odoo
|
|
8
|
+
Classifier: Framework :: Odoo :: 12.0
|
|
9
|
+
Classifier: Framework :: Odoo :: 13.0
|
|
10
|
+
Classifier: Framework :: Odoo :: 14.0
|
|
11
|
+
Classifier: Framework :: Odoo :: 15.0
|
|
12
|
+
Classifier: Framework :: Odoo :: 16.0
|
|
13
|
+
Classifier: Framework :: Odoo :: 17.0
|
|
14
|
+
Classifier: Framework :: Odoo :: 18.0
|
|
15
|
+
Classifier: Framework :: Odoo :: 19.0
|
|
16
|
+
Classifier: License :: OSI Approved :: GNU Affero General Public License v3
|
|
17
|
+
Classifier: Programming Language :: Python
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
26
|
+
Requires-Dist: redis==5.2.1
|
|
27
|
+
Requires-Dist: environ-odoo-config<2,>=0.6.5
|
|
28
|
+
Requires-Dist: typing-extensions
|
|
29
|
+
Requires-Dist: wrapt==1.17.3
|
|
30
|
+
Description-Content-Type: text/plain
|
|
31
|
+
Description: Use Redis Session instead of File system to store sessions
|
|
32
|
+
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
odoo/addons/redis_session_store/__init__.py,sha256=OY0COk3W8fkudSBfPqdrOVDba4pPQaMsxF6J3S0NjdY,1146
|
|
2
|
+
odoo/addons/redis_session_store/__manifest__.py,sha256=0FZYxlcFNZh2zTsnoZ7kHKh9Rm3QHGwEIHHl8rKLIdg,488
|
|
3
|
+
odoo/addons/redis_session_store/env_config.py,sha256=dHWpucfThhyRaUSzdP1NCNWSk0GMyrHvnDA-yYrUEqs,2730
|
|
4
|
+
odoo/addons/redis_session_store/json_encoding.py,sha256=Zb9MqFWi9aCCaWml5ErXlNtmDqmfUK1C6fD0MRRa_ok,1219
|
|
5
|
+
odoo/addons/redis_session_store/odoo_monkey_patch.py,sha256=5MpZASEbF6ZjMUPP_trxbZg5HmS7EL_VVA793THykfY,2243
|
|
6
|
+
odoo/addons/redis_session_store/redis_session.py,sha256=sLlOeJ3NWJQ0SP8uF-7l1uPyXKZRApR4lJny9b68Q7c,9670
|
|
7
|
+
odoo/addons/redis_session_store/environ_odoo_config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
odoo/addons/redis_session_store/environ_odoo_config/auto_load.py,sha256=FpE2fsxPn04E20xVSe7cumf1weUl5-x4NpsIFbZIfHE,256
|
|
9
|
+
odoo/addons/redis_session_store/environ_odoo_config/mapper.py,sha256=rsh5D6tF4ucsdBgBAQmm5e4mRMWKz4AoaaAXltT60yE,452
|
|
10
|
+
mangono_addon_redis_session_store-2.2.0.dist-info/METADATA,sha256=fbFTKnvHL5qjC1mi6PHovCBAeE8Tlue9TajSASeM_us,1305
|
|
11
|
+
mangono_addon_redis_session_store-2.2.0.dist-info/WHEEL,sha256=UYBUuRA3yPiZ7Eiy54KdZLBuO4pAoV-5dppQ0JKx5y4,98
|
|
12
|
+
mangono_addon_redis_session_store-2.2.0.dist-info/entry_points.txt,sha256=VCn3ggBV8ioYxOYvqhOc1cDvuOEnskUk5_JSE1FHevE,278
|
|
13
|
+
mangono_addon_redis_session_store-2.2.0.dist-info/top_level.txt,sha256=QE6RBQ0QX5f4eFuUcGgU5Kbq1A_qJcDs-e_vpr6pmfU,4
|
|
14
|
+
mangono_addon_redis_session_store-2.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
[environ_odoo_config.mapper]
|
|
2
|
+
redis_mapper = odoo.addons.redis_session_store.environ_odoo_config.mapper:redis_mapper
|
|
3
|
+
|
|
4
|
+
[environ_odoo_config.auto_server_wide_module]
|
|
5
|
+
redis_session_store = odoo.addons.redis_session_store.environ_odoo_config.auto_load:auto_load_redis_session_store
|
|
6
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
odoo
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from environ_odoo_config import odoo_utils
|
|
3
|
+
|
|
4
|
+
_logger = logging.getLogger("odoo.session.REDIS")
|
|
5
|
+
|
|
6
|
+
try:
|
|
7
|
+
import redis
|
|
8
|
+
|
|
9
|
+
_logger.info("Lib redis installed")
|
|
10
|
+
except ImportError:
|
|
11
|
+
redis = None
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _post_load_module():
|
|
15
|
+
if not redis:
|
|
16
|
+
raise ImportError("Please install package redis")
|
|
17
|
+
import odoo.release
|
|
18
|
+
|
|
19
|
+
if "redis_session_store" not in odoo_utils.get_server_wide_modules(odoo.release.serie):
|
|
20
|
+
return
|
|
21
|
+
from environ_odoo_config.environ import Environ
|
|
22
|
+
from .env_config import RedisEnvConfig
|
|
23
|
+
|
|
24
|
+
redis_config = RedisEnvConfig(Environ.new())
|
|
25
|
+
server_info = redis_config.connect().info()
|
|
26
|
+
# In case this is a Materia KV Redis compatible databaseOdooSessionClass
|
|
27
|
+
if not server_info.get("redis_version") and server_info.get("Materia KV "):
|
|
28
|
+
server_info = {"redis_version": f"Materia KV - {server_info['Materia KV ']}"}
|
|
29
|
+
if not server_info:
|
|
30
|
+
raise ValueError("Can't display server info")
|
|
31
|
+
_logger.info("Redis Session enable [%s]", server_info.get("redis_version", "No version provided"))
|
|
32
|
+
from . import odoo_monkey_patch
|
|
33
|
+
|
|
34
|
+
odoo_monkey_patch.patch_odoo(redis_config)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Redis Session Store",
|
|
3
|
+
"version": "2.2.0",
|
|
4
|
+
"depends": ["base"],
|
|
5
|
+
"author": "mangono",
|
|
6
|
+
"website": "http://mangono.fr/",
|
|
7
|
+
"license": "AGPL-3",
|
|
8
|
+
"description": """Use Redis Session instead of File system to store sessions""",
|
|
9
|
+
"summary": "",
|
|
10
|
+
"category": "Tools",
|
|
11
|
+
"auto_install": False,
|
|
12
|
+
"installable": False,
|
|
13
|
+
"application": False,
|
|
14
|
+
"post_load": "_post_load_module",
|
|
15
|
+
"external_dependencies": {
|
|
16
|
+
"python": ["redis"],
|
|
17
|
+
},
|
|
18
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import redis
|
|
4
|
+
from environ_odoo_config.config_section.api import OdooConfigGroup, RepeatableKey, SimpleKey
|
|
5
|
+
from environ_odoo_config.environ import Environ
|
|
6
|
+
from environ_odoo_config.odoo_version import OdooVersion
|
|
7
|
+
|
|
8
|
+
DEFAULT_SESSION_TIMEOUT = 60 * 60 * 24 * 3 # 3 days in seconds
|
|
9
|
+
DEFAULT_SESSION_TIMEOUT_ANONYMOUS = 60 * 2 # 2 minutes in seconds
|
|
10
|
+
DEFAULT_SESSION_TIMEOUT_ON_INACTIVITY = "True"
|
|
11
|
+
DEFAULT_SESSION_TIMEOUT_IGNORED_URLS = ["/longpolling", "/calendar/notify"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class RedisEnvConfig(OdooConfigGroup):
|
|
15
|
+
_ini_section = "redis_session"
|
|
16
|
+
|
|
17
|
+
host: str = SimpleKey("REDIS_HOST", ini_dest="redis_host", py_default="localhost")
|
|
18
|
+
port: int = SimpleKey("REDIS_PORT", ini_dest="redis_port", py_default=6379)
|
|
19
|
+
prefix: str = SimpleKey("REDIS_PREFIX", ini_dest="redis_prefix")
|
|
20
|
+
url: str = SimpleKey("REDIS_URL", ini_dest="redis_url")
|
|
21
|
+
password: str = SimpleKey("REDIS_PASSWORD", ini_dest="redis_password")
|
|
22
|
+
redis_ssl: bool = SimpleKey(
|
|
23
|
+
"REDIS_SSL",
|
|
24
|
+
ini_dest="redis_ssl",
|
|
25
|
+
py_default=True,
|
|
26
|
+
)
|
|
27
|
+
expiration: int = SimpleKey(
|
|
28
|
+
"ODOO_SESSION_REDIS_EXPIRATION", ini_dest="redis_expiration", py_default=DEFAULT_SESSION_TIMEOUT
|
|
29
|
+
)
|
|
30
|
+
anon_expiration: int = SimpleKey(
|
|
31
|
+
"ODOO_SESSION_REDIS_EXPIRATION_ANONYMOUS",
|
|
32
|
+
ini_dest="redis_anon_expiration",
|
|
33
|
+
py_default=DEFAULT_SESSION_TIMEOUT_ANONYMOUS,
|
|
34
|
+
)
|
|
35
|
+
timeout_on_inactivity: bool = SimpleKey(
|
|
36
|
+
"ODOO_SESSION_REDIS_TIMEOUT_ON_INACTIVITY",
|
|
37
|
+
ini_dest="redis_timeout_on_inactivity",
|
|
38
|
+
py_default=DEFAULT_SESSION_TIMEOUT_ON_INACTIVITY,
|
|
39
|
+
)
|
|
40
|
+
ignored_urls: set[str] = RepeatableKey(
|
|
41
|
+
"ODOO_SESSION_REDIS_TIMEOUT_IGNORED_URLS",
|
|
42
|
+
ini_dest="redis_ignored_urls",
|
|
43
|
+
ini_default=DEFAULT_SESSION_TIMEOUT_IGNORED_URLS,
|
|
44
|
+
)
|
|
45
|
+
db: int = SimpleKey("REDIS_DB_INDEX", py_default=0)
|
|
46
|
+
disable_gc: bool = SimpleKey("ODOO_DISABLE_SESSION_GC")
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def enable(self) -> bool:
|
|
50
|
+
return bool(self.host and self.password)
|
|
51
|
+
|
|
52
|
+
def _post_parse_env(self, environ: Environ):
|
|
53
|
+
if self.for_version > OdooVersion.V16:
|
|
54
|
+
self.disable_gc = True
|
|
55
|
+
|
|
56
|
+
def connect(self) -> redis.Redis:
|
|
57
|
+
"""
|
|
58
|
+
Return the connection to the Redis server.
|
|
59
|
+
If `self.url` is filled, then `url` all other connection info are exclude.
|
|
60
|
+
|
|
61
|
+
expiration, and anon_expiration are not passed to the connection.
|
|
62
|
+
|
|
63
|
+
:return: A connection to the Redis server.
|
|
64
|
+
"""
|
|
65
|
+
if self.url:
|
|
66
|
+
return redis.Redis.from_url(self.url)
|
|
67
|
+
return redis.Redis(
|
|
68
|
+
host=self.host,
|
|
69
|
+
port=self.port,
|
|
70
|
+
db=self.db,
|
|
71
|
+
password=self.password,
|
|
72
|
+
ssl=self.redis_ssl,
|
|
73
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from environ_odoo_config.environ import Environ
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def redis_mapper(curr_env: Environ | dict[str, Any]) -> Environ:
|
|
9
|
+
return _kv_clevercloud_redis(Environ(curr_env))
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _kv_clevercloud_redis(curr_env: Environ) -> Environ:
|
|
13
|
+
""" """
|
|
14
|
+
return curr_env + {
|
|
15
|
+
"REDIS_HOST": curr_env.gets("REDIS_HOST", "KV_HOST"),
|
|
16
|
+
"REDIS_PORT": curr_env.gets("REDIS_PORT", "KV_PORT"),
|
|
17
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from datetime import date, datetime
|
|
5
|
+
|
|
6
|
+
import dateutil
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _object_decoder(obj):
|
|
10
|
+
if "_type" not in obj:
|
|
11
|
+
return obj
|
|
12
|
+
type_ = obj["_type"]
|
|
13
|
+
if type_ == "datetime_isoformat":
|
|
14
|
+
return dateutil.parser.parse(obj["value"])
|
|
15
|
+
elif type_ == "date_isoformat":
|
|
16
|
+
return dateutil.parser.parse(obj["value"]).date()
|
|
17
|
+
elif type_ == "set":
|
|
18
|
+
return set(obj["value"])
|
|
19
|
+
return obj
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SessionEncoder(json.JSONEncoder):
|
|
23
|
+
"""Encode date/datetime objects
|
|
24
|
+
|
|
25
|
+
So that we can later recompose them if they were stored in the session
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def default(self, obj):
|
|
29
|
+
if isinstance(obj, datetime):
|
|
30
|
+
return {"_type": "datetime_isoformat", "value": obj.isoformat()}
|
|
31
|
+
elif isinstance(obj, date):
|
|
32
|
+
return {"_type": "date_isoformat", "value": obj.isoformat()}
|
|
33
|
+
elif isinstance(obj, set):
|
|
34
|
+
return {"_type": "set", "value": tuple(obj)}
|
|
35
|
+
return json.JSONEncoder.default(self, obj)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class SessionDecoder(json.JSONDecoder):
|
|
39
|
+
"""Decode json, recomposing recordsets and date/datetime"""
|
|
40
|
+
|
|
41
|
+
def __init__(self, **kwargs):
|
|
42
|
+
super().__init__(object_hook=_object_decoder, **kwargs)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import random
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import wrapt
|
|
8
|
+
|
|
9
|
+
import odoo
|
|
10
|
+
from odoo import http as odoo_http
|
|
11
|
+
from odoo.http import request
|
|
12
|
+
from odoo.tools import func as odoo_func
|
|
13
|
+
|
|
14
|
+
from .env_config import RedisEnvConfig
|
|
15
|
+
from .redis_session import RedisSessionStore
|
|
16
|
+
|
|
17
|
+
_logger = logging.getLogger("odoo.session.REDIS")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _get_session_class(major_odoo_version: int):
|
|
21
|
+
if major_odoo_version < 16:
|
|
22
|
+
return odoo_http.OpenERPSession
|
|
23
|
+
return odoo_http.Session
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _reset_cached_properties(major_odoo_version: int, obj: Any):
|
|
27
|
+
if major_odoo_version >= 19:
|
|
28
|
+
odoo_func.reset_cached_properties(obj)
|
|
29
|
+
else:
|
|
30
|
+
odoo_func.lazy_property.reset_all(obj)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def session_gc(session_store):
|
|
34
|
+
# session_gc is called at setup_session so we keep the randomness bit to only vacuum once in a while.
|
|
35
|
+
if random.random() < 0.001:
|
|
36
|
+
session_store.vacuum()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _update_expiration():
|
|
40
|
+
if (
|
|
41
|
+
hasattr(odoo_http.root.session_store, "update_expiration")
|
|
42
|
+
and request
|
|
43
|
+
and request.session
|
|
44
|
+
and request.session.uid
|
|
45
|
+
and not request.env["res.users"].browse(request.session.uid)._is_public()
|
|
46
|
+
):
|
|
47
|
+
odoo_http.root.session_store.update_expiration(request.session)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def patch_odoo(redis_config: RedisEnvConfig):
|
|
51
|
+
_patch_odoo_http_root(redis_config)
|
|
52
|
+
_patch_odoo_authenticate(redis_config)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _patch_odoo_http_root(redis_config: RedisEnvConfig):
|
|
56
|
+
_reset_cached_properties(redis_config.for_version.value, odoo.http.root)
|
|
57
|
+
type(odoo_http.root).session_store = RedisSessionStore(
|
|
58
|
+
redis_config, session_class=_get_session_class(redis_config.for_version.value)
|
|
59
|
+
)
|
|
60
|
+
# Keep compatibility with odoo env config.
|
|
61
|
+
# There is no more session_gc global function, so no more patch needed.
|
|
62
|
+
# Now see FilesystemSessionStore#vacuum.
|
|
63
|
+
if not redis_config.disable_gc:
|
|
64
|
+
odoo_http.session_gc = session_gc
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _patch_odoo_authenticate(redis_config: RedisEnvConfig):
|
|
68
|
+
@wrapt.patch_function_wrapper(
|
|
69
|
+
"odoo.addons.base",
|
|
70
|
+
"models.ir_http.IrHttp._authenticate",
|
|
71
|
+
)
|
|
72
|
+
def _patch_IrHttp__authenticate(wrapped, instance, args, kwargs):
|
|
73
|
+
_update_expiration()
|
|
74
|
+
return wrapped(*args, **kwargs)
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
import time
|
|
9
|
+
import warnings
|
|
10
|
+
from hashlib import sha512
|
|
11
|
+
|
|
12
|
+
import odoo
|
|
13
|
+
from odoo import http
|
|
14
|
+
from odoo.service import security
|
|
15
|
+
|
|
16
|
+
from . import json_encoding
|
|
17
|
+
from .env_config import RedisEnvConfig
|
|
18
|
+
|
|
19
|
+
# The amount of bytes of the session that will remain static and can be used
|
|
20
|
+
# for calculating the csrf token and be stored inside the database.
|
|
21
|
+
STORED_SESSION_BYTES = 42
|
|
22
|
+
# After a session is rotated, the session should be kept for a couple of
|
|
23
|
+
# seconds to account for network delay between multiple requests which are
|
|
24
|
+
# made at the same time and all use the same old cookie.
|
|
25
|
+
SESSION_DELETION_TIMER = 120
|
|
26
|
+
MAJOR = odoo.release.version_info[0]
|
|
27
|
+
try:
|
|
28
|
+
with warnings.catch_warnings(record=True):
|
|
29
|
+
import werkzeug.contrib.sessions as sessions
|
|
30
|
+
except ImportError:
|
|
31
|
+
from odoo.tools._vendor import sessions
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
_logger = logging.getLogger("odoo.session.REDIS")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# this is equal to the duration of the session garbage collector in
|
|
38
|
+
# odoo.http.session_gc()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
_logger = logging.getLogger(__name__)
|
|
42
|
+
_base64_urlsafe_re = re.compile(r"^[A-Za-z0-9_-]{84}$")
|
|
43
|
+
_session_identifier_re = re.compile(r"^[A-Za-z0-9_-]{%s}$" % STORED_SESSION_BYTES) # noqa
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class RedisSessionStore(sessions.SessionStore):
|
|
47
|
+
"""SessionStore that saves session to redis"""
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
redis_config: RedisEnvConfig,
|
|
52
|
+
session_class,
|
|
53
|
+
):
|
|
54
|
+
super().__init__(session_class=session_class)
|
|
55
|
+
self.redis = redis_config.connect()
|
|
56
|
+
self.expiration = redis_config.expiration
|
|
57
|
+
self.anon_expiration = redis_config.anon_expiration
|
|
58
|
+
self.timeout_on_inactivity = redis_config.timeout_on_inactivity
|
|
59
|
+
self.ignored_urls = redis_config.ignored_urls
|
|
60
|
+
self.support_expire = b"expire" in self.redis.command_list()
|
|
61
|
+
self.prefix = "session:"
|
|
62
|
+
if redis_config.prefix:
|
|
63
|
+
self.prefix = f"{self.prefix}:{redis_config.prefix}:"
|
|
64
|
+
|
|
65
|
+
def build_key(self, sid):
|
|
66
|
+
return f"{self.prefix}{sid}"
|
|
67
|
+
|
|
68
|
+
def is_valid_key(self, key):
|
|
69
|
+
return _base64_urlsafe_re.match(key) is not None
|
|
70
|
+
|
|
71
|
+
def get_expiration(self, session):
|
|
72
|
+
# session.expiration allow to set a custom expiration for a session
|
|
73
|
+
# such as a very short one for monitoring requests
|
|
74
|
+
if not self.support_expire:
|
|
75
|
+
return -1
|
|
76
|
+
session_expiration = getattr(session, "expiration", 0)
|
|
77
|
+
expiration = session_expiration or self.anon_expiration
|
|
78
|
+
if session.uid:
|
|
79
|
+
expiration = session_expiration or self.expiration
|
|
80
|
+
return expiration
|
|
81
|
+
|
|
82
|
+
def update_expiration(self, session):
|
|
83
|
+
if not self.support_expire or not self.timeout_on_inactivity:
|
|
84
|
+
return
|
|
85
|
+
path = http.request.httprequest.path
|
|
86
|
+
if any(path.startswith(url) for url in self.ignored_urls):
|
|
87
|
+
return
|
|
88
|
+
key = self.build_key(session.sid)
|
|
89
|
+
expiration = self.get_expiration(session)
|
|
90
|
+
|
|
91
|
+
return self.redis.expire(key, expiration)
|
|
92
|
+
|
|
93
|
+
def save(self, session):
|
|
94
|
+
key = self.build_key(session.sid)
|
|
95
|
+
expiration = self.get_expiration(session)
|
|
96
|
+
if _logger.isEnabledFor(logging.DEBUG):
|
|
97
|
+
if session.uid:
|
|
98
|
+
user_msg = f"user '{session.login}' (id: {session.uid})"
|
|
99
|
+
else:
|
|
100
|
+
user_msg = "anonymous user"
|
|
101
|
+
_logger.debug(
|
|
102
|
+
"saving session with key '%s' and expiration of %s seconds for %s",
|
|
103
|
+
key,
|
|
104
|
+
expiration,
|
|
105
|
+
user_msg,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
data = json.dumps(dict(session), cls=json_encoding.SessionEncoder).encode("utf-8")
|
|
109
|
+
result = self.redis.set(key, data)
|
|
110
|
+
if result and self.support_expire:
|
|
111
|
+
return self.redis.expire(key, expiration)
|
|
112
|
+
return -1
|
|
113
|
+
|
|
114
|
+
def delete(self, session):
|
|
115
|
+
key = self.build_key(session.sid)
|
|
116
|
+
_logger.debug("deleting session with key %s", key)
|
|
117
|
+
return self.redis.delete(key)
|
|
118
|
+
|
|
119
|
+
def delete_old_sessions(self, session):
|
|
120
|
+
pass
|
|
121
|
+
|
|
122
|
+
def get(self, sid):
|
|
123
|
+
if not self.is_valid_key(sid):
|
|
124
|
+
_logger.debug(
|
|
125
|
+
"session with invalid sid '%s' has been asked, returning a new one",
|
|
126
|
+
sid,
|
|
127
|
+
)
|
|
128
|
+
return self.new()
|
|
129
|
+
|
|
130
|
+
key = self.build_key(sid)
|
|
131
|
+
saved = self.redis.get(key)
|
|
132
|
+
if not saved:
|
|
133
|
+
_logger.debug(
|
|
134
|
+
"session with non-existent key '%s' has been asked, returning a new one",
|
|
135
|
+
key,
|
|
136
|
+
)
|
|
137
|
+
return self.new()
|
|
138
|
+
try:
|
|
139
|
+
data = json.loads(saved.decode("utf-8"), cls=json_encoding.SessionDecoder)
|
|
140
|
+
except ValueError:
|
|
141
|
+
_logger.debug(
|
|
142
|
+
"session for key '%s' has been asked but its json content could not be read, it has been reset",
|
|
143
|
+
key,
|
|
144
|
+
)
|
|
145
|
+
data = {}
|
|
146
|
+
return self.session_class(data, sid, False)
|
|
147
|
+
|
|
148
|
+
def list(self):
|
|
149
|
+
keys = self.redis.keys(f"{self.prefix}*")
|
|
150
|
+
_logger.debug("a listing redis keys has been called")
|
|
151
|
+
return [key[len(self.prefix) :] for key in keys]
|
|
152
|
+
|
|
153
|
+
def rotate(self, session, env, soft=False):
|
|
154
|
+
if soft:
|
|
155
|
+
# Multiple network requests can occur at the same time, all using the old session.
|
|
156
|
+
# We don't want to create a new session for each request, it's better to reference the one already made.
|
|
157
|
+
static = session.sid[:STORED_SESSION_BYTES]
|
|
158
|
+
recent_session = self.get(session.sid)
|
|
159
|
+
if "next_sid" in recent_session:
|
|
160
|
+
# A new session has already been saved on disk by a concurrent request,
|
|
161
|
+
# the _save_session is going to simply use session.sid to set a new cookie.
|
|
162
|
+
session.sid = recent_session["next_sid"]
|
|
163
|
+
return
|
|
164
|
+
next_sid = static + self.generate_key()[STORED_SESSION_BYTES:]
|
|
165
|
+
session["next_sid"] = next_sid
|
|
166
|
+
session["deletion_time"] = time.time() + SESSION_DELETION_TIMER
|
|
167
|
+
self.save(session)
|
|
168
|
+
# Now prepare the new session
|
|
169
|
+
session["gc_previous_sessions"] = True
|
|
170
|
+
session.sid = next_sid
|
|
171
|
+
del session["deletion_time"]
|
|
172
|
+
del session["next_sid"]
|
|
173
|
+
else:
|
|
174
|
+
self.delete(session)
|
|
175
|
+
session.sid = self.generate_key()
|
|
176
|
+
if session.uid:
|
|
177
|
+
assert env, "saving this session requires an environment"
|
|
178
|
+
session.session_token = security.compute_session_token(session, env)
|
|
179
|
+
session.should_rotate = False
|
|
180
|
+
session["create_time"] = time.time()
|
|
181
|
+
self.save(session)
|
|
182
|
+
|
|
183
|
+
def vacuum(self, *args, **kwargs):
|
|
184
|
+
"""
|
|
185
|
+
Vacuum all expired keys.
|
|
186
|
+
"""
|
|
187
|
+
# For MateriaKV, there is currently no active expiration. But `DBSIZE` seems to trigger the database gc.
|
|
188
|
+
# https://www.clever.cloud/developers/doc/addons/materia-kv/
|
|
189
|
+
# Useless for pure Redis config since there is an active expiration process. See :
|
|
190
|
+
# https://redis.io/docs/latest/commands/expire/#how-redis-expires-keys
|
|
191
|
+
self.redis.dbsize()
|
|
192
|
+
_logger.debug("retrieving dbsize to trigger keys vacuum")
|
|
193
|
+
return True
|
|
194
|
+
|
|
195
|
+
def get_missing_session_identifiers(self, identifiers: list[str]) -> list[str]:
|
|
196
|
+
"""
|
|
197
|
+
:param identifiers: session identifiers whose file existence must be checked
|
|
198
|
+
identifiers are a part session sid (first 42 chars)
|
|
199
|
+
:type identifiers: iterable
|
|
200
|
+
:return: the identifiers which are not present on the filesystem
|
|
201
|
+
:rtype: set
|
|
202
|
+
"""
|
|
203
|
+
existing_keys = set()
|
|
204
|
+
for key in identifiers:
|
|
205
|
+
if self.redis.exists(self.build_key(key)):
|
|
206
|
+
existing_keys.add(key)
|
|
207
|
+
# Remove the identifiers for which a key is present on the session store.
|
|
208
|
+
missing_keys = set(identifiers) - existing_keys
|
|
209
|
+
return list(missing_keys)
|
|
210
|
+
|
|
211
|
+
def delete_from_identifiers(self, identifiers: list[str]):
|
|
212
|
+
for identifier in identifiers:
|
|
213
|
+
# Avoid to remove a session if it does not match an identifier.
|
|
214
|
+
# This prevents malicious user to delete sessions from a different
|
|
215
|
+
# database by specifying a custom ``res.device.log``.
|
|
216
|
+
if not _session_identifier_re.match(identifier):
|
|
217
|
+
raise ValueError(
|
|
218
|
+
f"Identifier format incorrect, did you pass in a string instead of a list? {identifier}"
|
|
219
|
+
)
|
|
220
|
+
redis_key = self.build_key(identifier)
|
|
221
|
+
self.redis.delete(redis_key)
|
|
222
|
+
|
|
223
|
+
def generate_key(self, salt=None):
|
|
224
|
+
# The generated key is case sensitive (base64) and the length is 84 chars.
|
|
225
|
+
# In the worst-case scenario, i.e. in an insensitive filesystem (NTFS for example)
|
|
226
|
+
# taking into account the proportion of characters in the pool and a length
|
|
227
|
+
# of 42 (stored part in the database), the entropy for the base64 generated key
|
|
228
|
+
# is 217.875 bits which is better than the 160 bits entropy of a hexadecimal key
|
|
229
|
+
# with a length of 40 (method ``generate_key`` of ``SessionStore``).
|
|
230
|
+
# The risk of collision is negligible in practice.
|
|
231
|
+
# Formulas:
|
|
232
|
+
# - L: length of generated word
|
|
233
|
+
# - p_char: probability of obtaining the character in the pool
|
|
234
|
+
# - n: size of the pool
|
|
235
|
+
# - k: number of generated word
|
|
236
|
+
# Entropy = - L * sum(p_char * log2(p_char))
|
|
237
|
+
# Collision ~= (1 - exp((-k * (k - 1)) / (2 * (n**L))))
|
|
238
|
+
key = str(time.time()).encode() + os.urandom(64)
|
|
239
|
+
hash_key = sha512(key).digest()[:-1] # prevent base64 padding
|
|
240
|
+
return base64.urlsafe_b64encode(hash_key).decode("utf-8")
|