From eef54f2aadfc0221784e950cdb745a3792875e0f Mon Sep 17 00:00:00 2001 From: Yann Date: Wed, 24 Aug 2016 17:35:18 +0200 Subject: [PATCH] Begin session handling with cookies in webui --- lodel/auth/client.py | 61 +++++++++---------- lodel/auth/exceptions.py | 14 +++-- plugins/ram_sessions/main.py | 2 + plugins/webui/run.py | 113 ++++++++++++++++++++++++++--------- 4 files changed, 123 insertions(+), 67 deletions(-) diff --git a/lodel/auth/client.py b/lodel/auth/client.py index 39ffc66..80ca85d 100644 --- a/lodel/auth/client.py +++ b/lodel/auth/client.py @@ -121,31 +121,20 @@ a session is allready started !!!") #@todo Maybe we can delete this metaclass.... class ClientMetaclass(type): - SESSION_ID_NAME = '__SESSION_ID__' - def __init__(self, name, bases, attrs): - self.__session = dict() return super(ClientMetaclass, self).__init__(name, bases, attrs) def __getitem__(self, key): - if key not in self.__session: - raise KeyError("This client instance does not have a '%s' data" % key) - return self.__session[key] + return self.session()[key] + + def __delitem__(self, key): + del(self.session()[key]) def __setitem__(self, key, value): - if SESSION_ID_NAME not in self.__session: - self.__session[SESSION_ID_NAME] = SessionHandler.start_session() - self.__session[key] = value - - def __token(self): - return None if SESSION_ID_NAME not in self.__sessions else self.__session[SESSION_ID_NAME] - - ##@brief Return a copy of sessions infos - def session_dump(self): - #first set all sessions values - SessionHandler.save_session(self.__session) - return copy.copy(self.__session) + self.session()[key] = value + def __str__(self): + return str(self._instance) ##@brief Abstract singleton class designed to handle client informations # @@ -196,20 +185,6 @@ class Client(object, metaclass = ClientMetaclass): self.__session = LodelSession(session_token) logger.debug("New client : %s" % self) - ##@brief Attempt to restore a session given a session token - #@param token mixed : a session token - #@return Session datas (a dict) - #@throw ClientAuthenticationFailure if token is not valid or not - #existing - def _restore_session(self, token): - return self.__session.restore(token) - - ##@brief Return the current session token or None - #@return A session token or None - @classmethod - def session_token(cls): - return self.__session.retrieve_token() - ##@brief Try to authenticate a user with a login and a password #@param login str : provided login #@param password str : provided password (hash) @@ -245,6 +220,28 @@ class Client(object, metaclass = ClientMetaclass): break if self.is_anon(): self.fail() #Security logging + + ##@brief Attempt to restore a session given a session token + #@param token mixed : a session token + #@return Session datas (a dict) + #@throw ClientAuthenticationFailure if token is not valid or not + #existing + @classmethod + def restore_session(self, token): + cls._assert_instance() + return self.__session.restore(token) + + ##@brief Return the current session token or None + #@return A session token or None + @classmethod + def session_token(cls): + cls._assert_instance() + return cls._instance.__session.retrieve_token() + + @classmethod + def session(cls): + cls._assert_instance() + return cls._instance.__session @classmethod def destroy(cls): diff --git a/lodel/auth/exceptions.py b/lodel/auth/exceptions.py index e715ba2..2a9f897 100644 --- a/lodel/auth/exceptions.py +++ b/lodel/auth/exceptions.py @@ -1,9 +1,10 @@ from lodel import logger +from lodel.plugin.hooks import LodelHook ##@brief Handles common errors with a Client class ClientError(Exception): ##@brief The logger function to use to log the error message - _loglvl = logger.warning + _loglvl = 'warning' ##@brief Error str _err_str = "Error" ##@brief the hook name to trigger with error @@ -17,8 +18,9 @@ class ClientError(Exception): #" : <_err_str>[ : ]" def __init__(self, client, msg = ""): msg = self.build_message(client, msg) - if cls._loglvl is not None: - cls._loglvl(msg) + if self._loglvl is not None: + logfun = getattr(logger, self._loglvl) + logfun(msg) super().__init__(msg) if self._action is not None: LodelHook.call_hook(self._action, self, self._payload) @@ -32,21 +34,21 @@ class ClientError(Exception): ##@brief Handles authentication failure errors class ClientAuthenticationFailure(ClientError): - _loglvl = logger.security + _loglvl = 'security' _err_str = 'Authentication failure' _action = 'lodel2_ui_authentication_failure' ##@brief Handles permission denied errors class ClientPermissionDenied(ClientError): - _loglvl = logger.security + _loglvl = 'security' _err_str = 'Permission denied' _action = 'lodel2_ui_permission_denied' ##@brief Handles common errors on authentication class ClientAuthenticationError(ClientError): - _loglvl = logger.error + _loglvl = 'error' _err_str = 'Authentication error' _action = 'lodel2_ui_error' diff --git a/plugins/ram_sessions/main.py b/plugins/ram_sessions/main.py index 31981f1..eec0ad2 100644 --- a/plugins/ram_sessions/main.py +++ b/plugins/ram_sessions/main.py @@ -2,6 +2,7 @@ import os import copy +from lodel import logger from lodel.settings import Settings from lodel.auth.exceptions import * @@ -24,6 +25,7 @@ def destroy_session(token): def restore_session(token): _check_token(token) + logger.debug("Restoring session : %s" %__sessions[token]) return __sessions[token] def save_session(token, datas): diff --git a/plugins/webui/run.py b/plugins/webui/run.py index 17cb938..d649065 100644 --- a/plugins/webui/run.py +++ b/plugins/webui/run.py @@ -2,14 +2,19 @@ import loader # Lodel2 loader import os +import hashlib +import time + from werkzeug.contrib.sessions import FilesystemSessionStore from werkzeug.wrappers import Response +from werkzeug.contrib.securecookie import SecureCookie from lodel.settings import Settings from .interface.router import get_controller from .interface.lodelrequest import LodelRequest from .exceptions import * from .client import WebUiClient +from lodel.auth.exceptions import * from lodel.utils.datetime import get_utc_timestamp from lodel.plugin.hooks import LodelHook @@ -19,6 +24,48 @@ SESSION_EXPIRATION_LIMIT = Settings.webui.sessions.expiration session_store = FilesystemSessionStore(path=SESSION_FILES_BASE_DIR, filename_template=SESSION_FILES_TEMPLATE) +COOKIE_SESSION_ID = 'toktoken' +COOKIE_SESSION_HASH = 'nekotkot' +COOKIE_SESSION_HASH_SALT = [ os.urandom(32) for _ in range(2) ] #Before and after salt (maybe useless) +COOKIE_SESSION_HASH_ALGO = hashlib.sha512 + +##@brief Return a salted hash of a cookie +def cookie_hash(token): + return COOKIE_SESSION_HASH_ALGO( + COOKIE_SESSION_HASH_SALT[0]+token+COOKIE_SESSION_HASH_SALT[1]).hexdigest() + + +##@brief Load cookie from request +#@note Can produce security warning logs +#@param request +#@return None or a session token +def load_cookie(request): + token = request.cookies.get(COOKIE_SESSION_ID) + if token is None and token != '': + return None + token = bytes(token, 'utf-8') + hashtok = request.cookies.get(COOKIE_SESSION_HASH) + if hashtok is None: + raise ClientAuthenticationFailure( + WebUiClient, 'Bad cookies : no hash provided') + if cookie_hash(token) != hashtok: + raise ClientAuthenticationFailure( + WebUiClient, 'Bad cookies : hash mismatch') + return token + +##@brief Properly set cookies and hash given a token +#@param response +#@param token str : the session token +def save_cookie(response, token): + response.set_cookie(COOKIE_SESSION_ID, token) + response.set_cookie(COOKIE_SESSION_HASH, cookie_hash(token)) + +def empty_cookie(response): + response.set_cookie(COOKIE_SESSION_ID, '') + response.set_cookie(COOKIE_SESSION_HASH, '') + + + #Starting instance loader.start() #providing access to dyncode @@ -49,38 +96,46 @@ def is_session_file_expired(timestamp_now, sid): # WSGI Application def application(env, start_response): - WebUiClient(env['REMOTE_ADDR'], env['HTTP_USER_AGENT']) - current_timestamp = get_utc_timestamp() - delete_old_session_files(current_timestamp) request = LodelRequest(env) - sid = request.cookies.get('sid') - if sid is None or sid not in session_store.list(): - request.session = session_store.new() - request.session['last_accessed'] = current_timestamp - else: - request.session = session_store.get(sid) - if is_session_file_expired(current_timestamp, sid): - session_store.delete(request.session) - request.session = session_store.new() - request.session['user_context'] = None - request.session['last_accessed'] = current_timestamp - + session_token = None try: - controller = get_controller(request) - response = controller(request) - except HttpException as e: + #We have to create the client before restoring cookie in order to be able + #to log messages with client infos + client = WebUiClient(env['REMOTE_ADDR'], env['HTTP_USER_AGENT'], None) + session_token = load_cookie(request) + + if session_token is not None: + WebClient.restore_session(token) + session_token = None + #test + WebUiClient['last_request'] = time.time() try: - response = e.render(request) - except Exception as eb: - res = Response() - res.status_code = 500 - return res - - - if request.session.should_save: - session_store.save(request.session) - response.set_cookie('sid', request.session.sid) - + controller = get_controller(request) + response = controller(request) + except HttpException as e: + try: + response = e.render(request) + except Exception as eb: + raise eb + res = Response() + res.status_code = 500 + return res + session_token = WebUiClient.session_token() + if session_token is not None: + save_cookie(response,session_token) + session_token = None + + + except (ClientError, ClientAuthenticationError): + response = HttpException(400).render(request) + empty_cookie(response) + except ClientAuthenticationFailure: + response = HttpException(401).render(request) + empty_cookie(response) + except Exception as e: + raise e + res = response(env, start_response) + WebUiClient.destroy() return res