From b5308f564324fb5fb3ab2c5f9d1a721b4d02beba Mon Sep 17 00:00:00 2001
From: Eric Charles <eric@datalayer.io>
Date: Fri, 22 Jan 2021 19:56:13 +0100
Subject: [PATCH] add torndsession src

---
 jupyter_server/torndsession/__init__.py       |  11 +
 jupyter_server/torndsession/compat.py         |  67 +++++
 jupyter_server/torndsession/driver.py         |  48 ++++
 jupyter_server/torndsession/memorysession.py  |  80 ++++++
 jupyter_server/torndsession/session.py        | 258 ++++++++++++++++++
 jupyter_server/torndsession/sessionhandler.py |  30 ++
 6 files changed, 494 insertions(+)
 create mode 100644 jupyter_server/torndsession/__init__.py
 create mode 100644 jupyter_server/torndsession/compat.py
 create mode 100644 jupyter_server/torndsession/driver.py
 create mode 100644 jupyter_server/torndsession/memorysession.py
 create mode 100644 jupyter_server/torndsession/session.py
 create mode 100644 jupyter_server/torndsession/sessionhandler.py

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