123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247 |
- #
- # This file is part of Lodel 2 (https://github.com/OpenEdition)
- #
- # Copyright (C) 2015-2017 Cléo UMS-3287
- #
- # This program is free software: you can redistribute it and/or modify
- # it under the terms of the GNU Affero General Public License as published
- # by the Free Software Foundation, either version 3 of the License, or
- # (at your option) any later version.
- #
- # This program is distributed in the hope that it will be useful,
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- # GNU Affero General Public License for more details.
- #
- # You should have received a copy of the GNU Affero General Public License
- # along with this program. If not, see <http://www.gnu.org/licenses/>.
- #
-
-
- ## @package lodel.plugins.filesystem_session.main Main entry point of the plugin
-
- import binascii
- import datetime
- import os
- import pickle
- import re
- import time
- from .filesystem_session import FileSystemSession
- from lodel.context import LodelContext
-
-
- LodelContext.expose_modules(globals(), {
- 'lodel.logger': 'logger',
- 'lodel.auth.exceptions': ['ClientAuthenticationFailure'],
- 'lodel.settings': ['Settings']})
-
- __sessions = dict()
-
- SESSION_TOKENSIZE = 150
-
-
- ##
- # @brief generates a new session token
- #
- # @return str
- #
- # @warning The tokensize should absolutely be used as set! os.urandom function
- # takes a number of bytes as a parameter, dividing it by 2 is an
- # extremely dangerous idea as it drastically decrease the token expected
- # entropy expected from the value set in configs.
- # @remarks There is no valid reason for checking the generated token uniqueness:
- # - checking for uniqueness is slow ;
- # - keeping a dict with a few thousand keys of hundred bytes also is
- # memory expensive ;
- # - should the system get distributed while sharing session storage, there
- # would be no reasonable way to efficiently check for uniqueness ;
- # - sessions do have a really short life span, drastically reducing
- # even more an already close to inexistent risk of collision. A 64 bits
- # id would perfectly do the job, or to be really cautious, a 128 bits
- # one (actual size of UUIDs) ;
- # - if we are still willing to ensure uniqueness, then simply salt it
- # with a counter, or a timestamp, and hash the whole thing with a
- # cryptographically secured method such as sha-2 if we are paranoids
- # and trying to avoid what will never happen, ever ;
- # - sure, two hexadecimal characters is one byte long. Simply go for
- # bit length, not chars length.
- def generate_token():
- token = binascii.hexlify(os.urandom(SESSION_TOKENSIZE//2))
- if token in __sessions.keys():
- token = generate_token()
- return token.decode('utf-8')
-
-
- ##
- # @brief checks the validity of a given session token
- #
- # @param token str
- # @raise ClientAuthenticationFailure for invalid or not found session token
- #
- # @remarks It is useless to check the token size, unless urandom you don't
- # trust in PRNG such as urandom.
- # @remarks Linear key search...
- # @remarks Consider renaming. The "validity of a session token" usually means
- # that it is a active session token and/or that it was actually
- # produced by the application (signed for exemple).
- def check_token(token):
- if len(token) != SESSION_TOKENSIZE:
- raise ClientAuthenticationFailure("Invalid token string")
- if token not in __sessions.keys():
- raise ClientAuthenticationFailure("No session found for this token")
-
-
- ## @brief returns a session file path for a specific token
- # @param token str
- # @return str
- def generate_file_path(token):
- return os.path.abspath(os.path.join(Settings.sessions.directory, Settings.sessions.file_template) % token)
-
-
- ##
- # @brief Retrieve the token from the file system
- #
- # @param filepath str
- # @return str|None : returns the token or None if no token was found
- #
- # @remarks What is the purpose of the regex right here? There should be a way
- # to avoid slow operations.
- def get_token_from_filepath(filepath):
- token_regex = re.compile(os.path.abspath(os.path.join(Settings.sessions.directory, Settings.sessions.file_template % '(?P<token>.*)')))
- token_search_result = token_regex.match(filepath)
- if token_search_result is not None:
- return token_search_result.groupdict()['token']
- return None
-
-
- ##
- # @brief Returns the session's last modification timestamp
- #
- # @param token str
- # @return float
- # @raise ValueError if the given token doesn't match with an existing session
- #
- # @remarks Consider renaming
- # @warning Linear search in array, again. See @ref generate_token().
- def get_session_last_modified(token):
- if token in __sessions[token]:
- return os.stat(__sessions[token]).st_mtime
- else:
- raise ValueError("The given token %s doesn't match with an existing session")
-
-
- ##
- # @brief Starts a new session and returns a new token
- #
- # @return str : the new token
- def start_session():
- session = FileSystemSession(generate_token())
- session.path = generate_file_path(session.token)
-
- with open(session.path, 'wb') as session_file:
- pickle.dump(session, session_file)
-
- __sessions[session.token] = session.path
- logger.debug("New session created")
-
- return session.token
-
-
- ##
- # @brief destroys a session given its token
- #
- # @param token str
- def destroy_session(token):
- check_token(token)
- if os.path.isfile(__sessions[token]):
- os.unlink(__sessions[token])
- logger.debug("Session file for %s destroyed" % token)
- del(__sessions[token])
- logger.debug("Session %s unregistered" % token)
-
-
- ##
- # @brief Restores a session's content
- #
- # @param token str
- # @return FileSystemSession|None
- def restore_session(token):
- gc()
- check_token(token)
- logger.debug("Restoring session : %s" % token)
- if os.path.isfile(__sessions[token]):
- with open(__sessions[token], 'rb') as session_file:
- session = pickle.load(session_file)
- return session
- else:
- return None # raise FileNotFoundError("Session file not found for the token %s" % token)
-
-
- ## @brief saves the session's content to a file
- # @param token str
- # @param datas dict
- def save_session(token, datas):
- session = datas
- if not isinstance(datas, FileSystemSession):
- session = FileSystemSession(token)
- session.path = generate_file_path(token)
- session.update(datas)
-
- with open(__sessions[token], 'wb') as session_file:
- pickle.dump(session, session_file)
-
- if token not in __sessions.keys():
- __sessions[token] = session.path
-
- logger.debug("Session %s saved" % token)
-
-
- ## @brief session store's garbage collector
- #
- # @remarks
- def gc():
- # Unregistered files in the session directory
- session_files_directory = os.path.abspath(Settings.sessions.directory)
- for session_file in [file_path for file_path in os.listdir(session_files_directory) if os.path.isfile(os.path.join(session_files_directory, file_path))]:
- session_file_path = os.path.join(session_files_directory, session_file)
- token = get_token_from_filepath(session_file_path)
- if token is None or token not in __sessions.keys():
- os.unlink(session_file_path)
- logger.debug("Unregistered session file %s has been deleted" % session_file)
-
- # Expired registered sessions
- for token in __sessions.keys():
- if os.path.isfile(__sessions[token]):
- now_timestamp = time.mktime(datetime.datetime.now().timetuple())
- if now_timestamp - get_session_last_modified(token) > Settings.sessions.expiration:
- destroy_session(token)
- logger.debug("Expired session %s has been destroyed" % token)
-
-
- ##
- # @param token str
- # @param key str
- # @param value
- def set_session_value(token, key, value):
- session = restore_session(token)
- session[key] = value
- save_session(token, session)
-
- ##
- # @param token str
- # @param key str
- def get_session_value(token, key):
- session = restore_session(token)
- return session[key]
-
- ##
- # @brief deletes a session value
- #
- # @param token str
- # @param key str
- #
- # @todo Should we add a save_session at the end of this method?
- def del_session_value(token, key):
- session = restore_session(token)
- if key in session:
- del(session[key])
|