No Description
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

main.py 8.3KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. #
  2. # This file is part of Lodel 2 (https://github.com/OpenEdition)
  3. #
  4. # Copyright (C) 2015-2017 Cléo UMS-3287
  5. #
  6. # This program is free software: you can redistribute it and/or modify
  7. # it under the terms of the GNU Affero General Public License as published
  8. # by the Free Software Foundation, either version 3 of the License, or
  9. # (at your option) any later version.
  10. #
  11. # This program is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU Affero General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU Affero General Public License
  17. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  18. #
  19. ## @package lodel.plugins.filesystem_session.main Main entry point of the plugin
  20. import binascii
  21. import datetime
  22. import os
  23. import pickle
  24. import re
  25. import time
  26. from .filesystem_session import FileSystemSession
  27. from lodel.context import LodelContext
  28. LodelContext.expose_modules(globals(), {
  29. 'lodel.logger': 'logger',
  30. 'lodel.auth.exceptions': ['ClientAuthenticationFailure'],
  31. 'lodel.settings': ['Settings']})
  32. __sessions = dict()
  33. SESSION_TOKENSIZE = 150
  34. ##
  35. # @brief generates a new session token
  36. #
  37. # @return str
  38. #
  39. # @warning The tokensize should absolutely be used as set! os.urandom function
  40. # takes a number of bytes as a parameter, dividing it by 2 is an
  41. # extremely dangerous idea as it drastically decrease the token expected
  42. # entropy expected from the value set in configs.
  43. # @remarks There is no valid reason for checking the generated token uniqueness:
  44. # - checking for uniqueness is slow ;
  45. # - keeping a dict with a few thousand keys of hundred bytes also is
  46. # memory expensive ;
  47. # - should the system get distributed while sharing session storage, there
  48. # would be no reasonable way to efficiently check for uniqueness ;
  49. # - sessions do have a really short life span, drastically reducing
  50. # even more an already close to inexistent risk of collision. A 64 bits
  51. # id would perfectly do the job, or to be really cautious, a 128 bits
  52. # one (actual size of UUIDs) ;
  53. # - if we are still willing to ensure uniqueness, then simply salt it
  54. # with a counter, or a timestamp, and hash the whole thing with a
  55. # cryptographically secured method such as sha-2 if we are paranoids
  56. # and trying to avoid what will never happen, ever ;
  57. # - sure, two hexadecimal characters is one byte long. Simply go for
  58. # bit length, not chars length.
  59. def generate_token():
  60. token = binascii.hexlify(os.urandom(SESSION_TOKENSIZE//2))
  61. if token in __sessions.keys():
  62. token = generate_token()
  63. return token.decode('utf-8')
  64. ##
  65. # @brief checks the validity of a given session token
  66. #
  67. # @param token str
  68. # @raise ClientAuthenticationFailure for invalid or not found session token
  69. #
  70. # @remarks It is useless to check the token size, unless urandom you don't
  71. # trust in PRNG such as urandom.
  72. # @remarks Linear key search...
  73. # @remarks Consider renaming. The "validity of a session token" usually means
  74. # that it is a active session token and/or that it was actually
  75. # produced by the application (signed for exemple).
  76. def check_token(token):
  77. if len(token) != SESSION_TOKENSIZE:
  78. raise ClientAuthenticationFailure("Invalid token string")
  79. if token not in __sessions.keys():
  80. raise ClientAuthenticationFailure("No session found for this token")
  81. ## @brief returns a session file path for a specific token
  82. # @param token str
  83. # @return str
  84. def generate_file_path(token):
  85. return os.path.abspath(os.path.join(Settings.sessions.directory, Settings.sessions.file_template) % token)
  86. ##
  87. # @brief Retrieve the token from the file system
  88. #
  89. # @param filepath str
  90. # @return str|None : returns the token or None if no token was found
  91. #
  92. # @remarks What is the purpose of the regex right here? There should be a way
  93. # to avoid slow operations.
  94. def get_token_from_filepath(filepath):
  95. token_regex = re.compile(os.path.abspath(os.path.join(Settings.sessions.directory, Settings.sessions.file_template % '(?P<token>.*)')))
  96. token_search_result = token_regex.match(filepath)
  97. if token_search_result is not None:
  98. return token_search_result.groupdict()['token']
  99. return None
  100. ##
  101. # @brief Returns the session's last modification timestamp
  102. #
  103. # @param token str
  104. # @return float
  105. # @raise ValueError if the given token doesn't match with an existing session
  106. #
  107. # @remarks Consider renaming
  108. # @warning Linear search in array, again. See @ref generate_token().
  109. def get_session_last_modified(token):
  110. if token in __sessions[token]:
  111. return os.stat(__sessions[token]).st_mtime
  112. else:
  113. raise ValueError("The given token %s doesn't match with an existing session")
  114. ##
  115. # @brief Starts a new session and returns a new token
  116. #
  117. # @return str : the new token
  118. def start_session():
  119. session = FileSystemSession(generate_token())
  120. session.path = generate_file_path(session.token)
  121. with open(session.path, 'wb') as session_file:
  122. pickle.dump(session, session_file)
  123. __sessions[session.token] = session.path
  124. logger.debug("New session created")
  125. return session.token
  126. ##
  127. # @brief destroys a session given its token
  128. #
  129. # @param token str
  130. def destroy_session(token):
  131. check_token(token)
  132. if os.path.isfile(__sessions[token]):
  133. os.unlink(__sessions[token])
  134. logger.debug("Session file for %s destroyed" % token)
  135. del(__sessions[token])
  136. logger.debug("Session %s unregistered" % token)
  137. ##
  138. # @brief Restores a session's content
  139. #
  140. # @param token str
  141. # @return FileSystemSession|None
  142. def restore_session(token):
  143. gc()
  144. check_token(token)
  145. logger.debug("Restoring session : %s" % token)
  146. if os.path.isfile(__sessions[token]):
  147. with open(__sessions[token], 'rb') as session_file:
  148. session = pickle.load(session_file)
  149. return session
  150. else:
  151. return None # raise FileNotFoundError("Session file not found for the token %s" % token)
  152. ## @brief saves the session's content to a file
  153. # @param token str
  154. # @param datas dict
  155. def save_session(token, datas):
  156. session = datas
  157. if not isinstance(datas, FileSystemSession):
  158. session = FileSystemSession(token)
  159. session.path = generate_file_path(token)
  160. session.update(datas)
  161. with open(__sessions[token], 'wb') as session_file:
  162. pickle.dump(session, session_file)
  163. if token not in __sessions.keys():
  164. __sessions[token] = session.path
  165. logger.debug("Session %s saved" % token)
  166. ## @brief session store's garbage collector
  167. #
  168. # @remarks
  169. def gc():
  170. # Unregistered files in the session directory
  171. session_files_directory = os.path.abspath(Settings.sessions.directory)
  172. 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))]:
  173. session_file_path = os.path.join(session_files_directory, session_file)
  174. token = get_token_from_filepath(session_file_path)
  175. if token is None or token not in __sessions.keys():
  176. os.unlink(session_file_path)
  177. logger.debug("Unregistered session file %s has been deleted" % session_file)
  178. # Expired registered sessions
  179. for token in __sessions.keys():
  180. if os.path.isfile(__sessions[token]):
  181. now_timestamp = time.mktime(datetime.datetime.now().timetuple())
  182. if now_timestamp - get_session_last_modified(token) > Settings.sessions.expiration:
  183. destroy_session(token)
  184. logger.debug("Expired session %s has been destroyed" % token)
  185. ##
  186. # @param token str
  187. # @param key str
  188. # @param value
  189. def set_session_value(token, key, value):
  190. session = restore_session(token)
  191. session[key] = value
  192. save_session(token, session)
  193. ##
  194. # @param token str
  195. # @param key str
  196. def get_session_value(token, key):
  197. session = restore_session(token)
  198. return session[key]
  199. ##
  200. # @brief deletes a session value
  201. #
  202. # @param token str
  203. # @param key str
  204. #
  205. # @todo Should we add a save_session at the end of this method?
  206. def del_session_value(token, key):
  207. session = restore_session(token)
  208. if key in session:
  209. del(session[key])