diff --git a/session_redis/README.rst b/session_redis/README.rst new file mode 100644 index 0000000..3f15914 --- /dev/null +++ b/session_redis/README.rst @@ -0,0 +1,33 @@ +Sessions in Redis +================= + +This addon allows to store the web sessions in Redis. + +Configuration +------------- + +The storage of sessions in Redis is activated using environment variables. + +* ``ODOO_SESSION_REDIS`` has to be ``1`` or ``true`` +* ``ODOO_SESSION_REDIS_HOST`` is the redis hostname (default is ``localhost``) +* ``ODOO_SESSION_REDIS_PORT`` is the redis port (default is ``6379``) +* ``ODOO_SESSION_REDIS_PASSWORD`` is the password for the AUTH command + (optional) +* ``ODOO_SESSION_REDIS_PREFIX`` is the prefix for the session keys (optional) +* ``ODOO_SESSION_REDIS_EXPIRATION`` is the time in seconds before expiration of + the sessions (default is 7 days) + + +The keys are set to ``session:``. +When a prefix is defined, the keys are ``session::`` + +Limitations +----------- + +* The server has to be restarted in order for the sessions to be stored in + Redis. +* All the users will have to login again as their previous session will be + dropped. +* The addon monkey-patch ``openerp.http.Root.session_store`` with a custom + method when the Redis mode is active, so incompatibilities with other addons + is possible if they do the same. diff --git a/session_redis/__init__.py b/session_redis/__init__.py new file mode 100644 index 0000000..af491fc --- /dev/null +++ b/session_redis/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- + +from . import http +from . import session diff --git a/session_redis/__openerp__.py b/session_redis/__openerp__.py new file mode 100644 index 0000000..22efc7d --- /dev/null +++ b/session_redis/__openerp__.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + + +{'name': 'Sessions in Redis', + 'summary': 'Store web sessions in Redis', + 'version': '9.0.1.0.0', + 'author': 'Camptocamp', + 'license': 'AGPL-3', + 'category': 'Extra Tools', + 'depends': ['base'], + 'external_dependencies': { + 'python': ['redis'], + }, + 'website': 'http://www.camptocamp.com', + 'data': [], + 'installable': True, + } diff --git a/session_redis/http.py b/session_redis/http.py new file mode 100644 index 0000000..4c814e8 --- /dev/null +++ b/session_redis/http.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + +import logging +import os + +from distutils.util import strtobool + +import redis + +import openerp +from openerp import http +from openerp.tools.func import lazy_property + +from .session import RedisSessionStore + +_logger = logging.getLogger(__name__) + + +def is_true(strval): + return bool(strtobool(strval or '0'.lower())) + + +@lazy_property +def session_store(self): + host = os.environ.get('ODOO_SESSION_REDIS_HOST') or 'localhost' + port = int(os.environ.get('ODOO_SESSION_REDIS_PORT') or 6379) + prefix = os.environ.get('ODOO_SESSION_REDIS_PREFIX') + password = os.environ.get('ODOO_SESSION_REDIS_PASSWORD') + expiration = os.environ.get('ODOO_SESSION_REDIS_EXPIRATION') + _logger.debug("HTTP sessions stored in Redis %s:%s with prefix '%s'", + host, port, prefix or '') + redis_client = redis.Redis(host=host, port=port, password=password) + return RedisSessionStore(redis=redis_client, prefix=prefix, + expiration=expiration, + session_class=http.OpenERPSession) + + +def session_gc(session_store): + """ Do not garbage collect the sessions + + Redis keys are automatically cleaned at the end of their + expiration. + """ + return + + +def purge_fs_sessions(path): + for fname in os.listdir(path): + path = os.path.join(path, fname) + try: + os.unlink(path) + except OSError: + pass + + +if is_true(os.environ.get('ODOO_SESSION_REDIS')): + http.Root.session_store = session_store + http.session_gc = session_gc + # clean the existing sessions on the file system + purge_fs_sessions(openerp.tools.config.session_dir) diff --git a/session_redis/session.py b/session_redis/session.py new file mode 100644 index 0000000..3965e1d --- /dev/null +++ b/session_redis/session.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + +import json + +from werkzeug.contrib.sessions import SessionStore + +# this is equal to the duration of the session garbage collector in +# openerp.http.session_gc() +DEFAULT_SESSION_TIMEOUT = 60 * 60 * 24 * 7 # 7 days in seconds + + +class RedisSessionStore(SessionStore): + """ SessionStore that saves session to redis """ + + def __init__(self, redis, session_class=None, + prefix='', expiration=None): + super(RedisSessionStore, self).__init__(session_class=session_class) + self.redis = redis + if expiration is None: + self.expiration = DEFAULT_SESSION_TIMEOUT + else: + self.expiration = expiration + self.prefix = u'session:' + if prefix: + self.prefix = u'%s:%s:' % ( + self.prefix, prefix + ) + + def build_key(self, sid): + if isinstance(sid, unicode): + sid = sid.encode('utf-8') + return '%s%s' % (self.prefix, sid) + + def save(self, session): + key = self.build_key(session.sid) + if self.redis.set(key, json.dumps(dict(session))): + return self.redis.expire(key, self.expiration) + + def delete(self, session): + key = self.build_key(session.sid) + return self.redis.delete(key) + + def get(self, sid): + if not self.is_valid_key(sid): + return self.new() + + key = self.build_key(sid) + saved = self.redis.get(key) + if not saved: + return self.new() + try: + data = json.loads(saved) + except ValueError: + data = {} + return self.session_class(data, sid, False) + + def list(self): + keys = self.redis.keys('%s*' % self.prefix) + return [key[len(self.prefix):] for key in keys]