diff --git a/jupyter_server/torndsession/__init__.py b/jupyter_server/torndsession/__init__.py new file mode 100644 index 0000000000..b7f095563c --- /dev/null +++ b/jupyter_server/torndsession/__init__.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python +# -*- coding:utf-8 -*- + +# Copyright 2014 Mitchell Chu + +"""This is a Tornado Session Extension """ + +from __future__ import absolute_import, division, print_function, with_statement + +version = "1.1.5.1" +version_info = (1, 1, 5, 1) diff --git a/jupyter_server/torndsession/compat.py b/jupyter_server/torndsession/compat.py new file mode 100644 index 0000000000..cd179778a7 --- /dev/null +++ b/jupyter_server/torndsession/compat.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python +# -*- coding:utf-8 -*- +# +# Copyright (c) 2014 Mitchell Chu + +import sys + +# __all__ = ( +# 'text_type', 'string_types', 'izip', 'iteritems', 'itervalues', +# 'with_metaclass', +# ) + +PY3 = sys.version_info >= (3,) + +if PY3: + text_type = str + string_types = (str, ) + integer_types = int + izip = zip + _xrange = range + MAXSIZE = sys.maxsize + + def iteritems(o): + return iter(o.items()) + + def itervalues(o): + return iter(o.values()) + + def bytes_from_hex(h): + return bytes.fromhex(h) + + def reraise(exctype, value, trace=None): + raise exctype(str(value)).with_traceback(trace) + + def _unicode(s): + return s +else: + text_type = unicode + string_types = (basestring, ) + integer_types = (int, long) + from itertools import izip + _xrange = xrange + MAXSIZE = sys.maxint + + def b(s): + # See comments above. In python 2.x b('foo') is just 'foo'. + return s + + def iteritems(o): + return o.iteritems() + + def itervalues(o): + return o.itervalues() + + def bytes_from_hex(h): + return h.decode('hex') + + # "raise x, y, z" raises SyntaxError in Python 3 + exec("""def reraise(exctype, value, trace=None): + raise exctype, str(value), trace +""") + + _unicode = unicode + + +def with_metaclass(meta, base=object): + return meta("NewBase", (base,), {}) diff --git a/jupyter_server/torndsession/driver.py b/jupyter_server/torndsession/driver.py new file mode 100644 index 0000000000..9e6f2ca3cf --- /dev/null +++ b/jupyter_server/torndsession/driver.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright @ 2014 Mitchell Chu + +from __future__ import (absolute_import, division, print_function, + with_statement) + + +class SessionDriver(object): + ''' + abstact class for all real session driver implements. + ''' + def __init__(self, **settings): + self.settings = settings + + def get(self, session_id): + raise NotImplementedError() + + def save(self, session_id, session_data, expires=None): + raise NotImplementedError() + + def clear(self, session_id): + raise NotImplementedError() + + def remove_expires(self): + raise NotImplementedError() + +class SessionDriverFactory(object): + ''' + session driver factory + use input settings to return suitable driver's instance + ''' + @staticmethod + def create_driver(driver, **settings): + module_name = 'jupyter_server.torndsession.%ssession' % driver.lower() + module = __import__(module_name, globals(), locals(), ['object']) + # must use this form. + # use __import__('torndsession.' + driver.lower()) just load torndsession.__init__.pyc + cls = getattr(module, '%sSession' % driver.capitalize()) + if not 'SessionDriver' in [base.__name__ for base in cls.__bases__]: + raise InvalidSessionDriverException( + '%s not found in current driver implements ' % driver) + return cls + + +class InvalidSessionDriverException(Exception): + pass diff --git a/jupyter_server/torndsession/memorysession.py b/jupyter_server/torndsession/memorysession.py new file mode 100644 index 0000000000..9d739bac4f --- /dev/null +++ b/jupyter_server/torndsession/memorysession.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright @ 2014 Mitchell Chu + +from __future__ import (absolute_import, division, print_function, + with_statement) + +from datetime import datetime + +from .driver import SessionDriver +from .session import SessionConfigurationError +from .compat import iteritems + + +class MemorySession(SessionDriver): + """ + save session data in process memory + """ + + MAX_SESSION_OBJECTS = 1024 + """The max session objects save in memory. + when session objects count large than this value, + system will auto to clear the expired session data. + """ + + def __init__(self, **settings): + # check settings + super(MemorySession, self).__init__(**settings) + host = settings.get("host") + if not host: + raise SessionConfigurationError( + 'memory session driver can not found persistence position') + if not hasattr(host, "session_container"): + setattr(host, "session_container", {}) + self._data_handler = host.session_container + + def get(self, session_id): + """ + get session object from host. + """ + if session_id not in self._data_handler: + return {} + + session_obj = self._data_handler[session_id] + now = datetime.utcnow() + expires = session_obj.get('__expires__', now) + if expires > now: + return session_obj + return {} + + def save(self, session_id, session_data, expires=None): + """ + save session data to host. + if host's session objects is more then MAX_SESSION_OBJECTS + system will auto to clear expired session data. + after cleared, system will add current to session pool, however the pool is full. + """ + session_data = session_data or {} + if expires: + session_data.update(__expires__=expires) + if len(self._data_handler) >= self.MAX_SESSION_OBJECTS: + self.remove_expires() + if len(self._data_handler) >= self.MAX_SESSION_OBJECTS: + print("system session pool is full. need more memory to save session object.") + self._data_handler[session_id] = session_data + + def clear(self, session_id): + if self._data_handler.haskey(session_id): + del self._data_handler[session_id] + + def remove_expires(self): + keys = [] + for key, val in iteritems(self._data_handler): + now = datetime.utcnow() + expires = val.get("__expires__", now) + if now >= expires: + keys.append(key) + for key in keys: + del self._data_handler[key] diff --git a/jupyter_server/torndsession/session.py b/jupyter_server/torndsession/session.py new file mode 100644 index 0000000000..9187dab685 --- /dev/null +++ b/jupyter_server/torndsession/session.py @@ -0,0 +1,258 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright @ 2014 Mitchell Chu +# + +from __future__ import (absolute_import, division, print_function, + with_statement) + +from datetime import datetime, timedelta +# from uuid import uuid4 +from os import urandom +from binascii import b2a_base64 + +from .driver import SessionDriverFactory +from .compat import _xrange + +l = [c for c in map(chr, _xrange(256))] +l[47] = '-' +l[43] = '_' +l[61] = '.' +_smap = str('').join(l) +del l + + +class SessionManager(object): + + SESSION_ID = 'msid' + DEFAULT_SESSION_LIFETIME = 1200 # seconds + + def __init__(self, handler): + self.handler = handler + self.settings = {} + self.__init_settings() + self._default_session_lifetime = datetime.utcnow() + timedelta( + seconds=self.settings.get('session_lifetime', self.DEFAULT_SESSION_LIFETIME)) + self._expires = self._default_session_lifetime + self._is_dirty = True + self.__init_session_driver() + self.__init_session_object() # initialize session object + + def __init_session_object(self): + cookiename = self.settings.get('sid_name', self.SESSION_ID) + session_id = self.handler.get_cookie(cookiename) + if not session_id: + session_id = self._generate_session_id(30) + self.handler.set_cookie(cookiename, + session_id, + **self.__session_settings()) + self._is_dirty = True + self.web_session = {} + else: + self.web_session = self._get_session_object_from_driver(session_id) + if not self.web_session: + self.web_session = {} + self._is_dirty = True + else: + self._is_dirty = False + cookie_config = self.settings.get("cookie_config") + if cookie_config: + expires = cookie_config.get("expires") + expires_days = cookie_config.get("expires_days") + if expires_days is not None and not expires: + expires = datetime.utcnow() + timedelta(days=expires_days) + if expires and isinstance(expires, datetime): + self._expires = expires + self._expires = self._expires if self._expires else self._default_session_lifetime + self._id = session_id + + def __init_session_driver(self): + """ + setup session driver. + """ + + driver = self.settings.get("driver") + if not driver: + raise SessionConfigurationError('driver not found') + driver_settings = self.settings.get("driver_settings", {}) + if not driver_settings: + raise SessionConfigurationError('driver settings not found.') + + cache_driver = self.settings.get("cache_driver", True) + + if cache_driver: + cache_name = '__cached_session_driver' + cache_handler = self.handler.application + if not hasattr(cache_handler, cache_name): + setattr( + cache_handler, + cache_name, + SessionDriverFactory.create_driver(driver, **driver_settings)) + session_driver = getattr(cache_handler, cache_name) + else: + session_driver = SessionDriverFactory.create_driver(driver, **driver_settings) + self.driver = session_driver(**driver_settings) # create session driver instance. + + def __init_settings(self): + """ + Init session relative configurations. + all configuration settings as follow: + settings = dict( + cookie_secret = "00a03c657e749caa89ef650a57b53ba(&#)(", + debug = True, + session = { + driver = 'memory', + driver_settings = {'host': self,}, # use application to save session data. + force_persistence = True, + cache_driver = True, # cache driver in application. + cookie_config = {'expires_days': 10, 'expires': datetime.datetime.utcnow(),}, # tornado cookies configuration + }, + ) + + driver: default enum value: memory, file, redis, memcache. + driver_settings: the data driver need. settings may be the host, database, password, and so on. + redis settings as follow: + driver_settings = { + host = '127.0.0.1', + port = '6379', + db = 0, # where the session data to save. + password = 'session_db_password', # if database has password + } + force_persistence: default is False. + In default, session's data exists in memory only, you must persistence it by manual. + Generally, rewrite Tornado RequestHandler's prepare(self) and on_finish(self) to persist session data is recommended. + when this value set to True, session data will be force to persist everytime when it has any change. + + """ + session_settings = self.handler.settings.get("session") + if not session_settings: # use default + session_settings = {} + session_settings.update( + driver='memory', + driver_settings={'host': self.handler.application}, + force_persistence=True, + cache_driver=True) + self.settings = session_settings + + def _generate_session_id(self, blength=24): + """generate session id + + Implement: https://github.com/MitchellChu/torndsession/issues/12 + + :arg int blength: give the bytes to generate. + :return string: session string + + .. versionadded:: 1.1.5 + """ + session_id = (b2a_base64(urandom(blength)))[:-1] + if isinstance(session_id, str): + # PY2 + return session_id.translate(_smap) + return session_id.decode('utf-8').translate(_smap) + + def _get_session_object_from_driver(self, session_id): + """ + Get session data from driver. + """ + return self.driver.get(session_id) + + def get(self, key, default=None): + """ + Return session value with name as key. + """ + return self.web_session.get(key, default) + + def set(self, key, value): + """ + Add/Update session value + """ + self.web_session[key] = value + self._is_dirty = True + force_update = self.settings.get("force_persistence") + if force_update: + self.driver.save(self._id, self.web_session, self._expires) + self._is_dirty = False + + def delete(self, key): + """ + Delete session key-value pair + """ + if key in self.web_session: + del self.web_session[key] + self._is_dirty = True + force_update = self.settings.get("force_persistence") + if force_update: + self.driver.save(self._id, self.web_session, self._expires) + self._is_dirty = False + __delitem__ = delete + + def iterkeys(self): + return iter(self.web_session) + __iter__ = iterkeys + + def keys(self): + """ + Return all keys in session object + """ + return self.web_session.keys() + + def flush(self): + """ + this method force system to do session data persistence. + """ + if self._is_dirty: + self.driver.save(self._id, self.web_session, self._expires) + + def __setitem__(self, key, value): + self.set(key, value) + + def __getitem__(self, key): + val = self.get(key) + if val: + return val + raise KeyError('%s not found' % key) + + def __contains__(self, key): + return key in self.web_session + + @property + def id(self): + """ + Return current session id + """ + if not hasattr(self, '_id'): + self.__init_session_object() + return self._id + + @property + def expires(self): + """ + The session object lifetime on server. + this property could not be used to cookie expires setting. + """ + if not hasattr(self, '_expires'): + self.__init_session_object() + return self._expires + + def __session_settings(self): + session_settings = self.settings.get('cookie_config', {}) + session_settings.setdefault('expires', None) + session_settings.setdefault('expires_days', None) + return session_settings + + +class SessionMixin(object): + + @property + def web_session(self): + return self._create_mixin(self, '__session_manager', SessionManager) + + def _create_mixin(self, context, inner_property_name, session_handler): + if not hasattr(context, inner_property_name): + setattr(context, inner_property_name, session_handler(context)) + return getattr(context, inner_property_name) + + +class SessionConfigurationError(Exception): + pass diff --git a/jupyter_server/torndsession/sessionhandler.py b/jupyter_server/torndsession/sessionhandler.py new file mode 100644 index 0000000000..55c5696e7e --- /dev/null +++ b/jupyter_server/torndsession/sessionhandler.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright @ 2014 Mitchell Chu + +from __future__ import (absolute_import, division, print_function, + with_statement) + +import tornado.web +import SessionMixin from .session + + +class SessionBaseHandler(tornado.web.RequestHandler, SessionMixin): + """ + This is a tornado web request handler which is base on torndsession. + Generally, user must persistent session object with manual operation when force_persistence is False. + but when the handler is inherit from SessionBaseHandler, in your handler, you just need to add/update/delete session values, SessionBaseHandler will auto save it. + """ + + def prepare(self): + """ + Overwrite tornado.web.RequestHandler prepare. + """ + pass + + def on_finish(self): + """ + Overwrite tornado.web.RequestHandler on_finish. + """ + self.web_session.flush() # try to save session