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.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. import datetime
  2. import logging
  3. import os
  4. import shutil
  5. import tempfile
  6. from django.conf import settings
  7. from django.contrib.sessions.backends.base import (
  8. VALID_KEY_CHARS, CreateError, SessionBase, UpdateError,
  9. )
  10. from django.contrib.sessions.exceptions import InvalidSessionKey
  11. from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation
  12. from django.utils import timezone
  13. class SessionStore(SessionBase):
  14. """
  15. Implement a file based session store.
  16. """
  17. def __init__(self, session_key=None):
  18. self.storage_path = type(self)._get_storage_path()
  19. self.file_prefix = settings.SESSION_COOKIE_NAME
  20. super().__init__(session_key)
  21. @classmethod
  22. def _get_storage_path(cls):
  23. try:
  24. return cls._storage_path
  25. except AttributeError:
  26. storage_path = getattr(settings, 'SESSION_FILE_PATH', None) or tempfile.gettempdir()
  27. # Make sure the storage path is valid.
  28. if not os.path.isdir(storage_path):
  29. raise ImproperlyConfigured(
  30. "The session storage path %r doesn't exist. Please set your"
  31. " SESSION_FILE_PATH setting to an existing directory in which"
  32. " Django can store session data." % storage_path)
  33. cls._storage_path = storage_path
  34. return storage_path
  35. def _key_to_file(self, session_key=None):
  36. """
  37. Get the file associated with this session key.
  38. """
  39. if session_key is None:
  40. session_key = self._get_or_create_session_key()
  41. # Make sure we're not vulnerable to directory traversal. Session keys
  42. # should always be md5s, so they should never contain directory
  43. # components.
  44. if not set(session_key).issubset(VALID_KEY_CHARS):
  45. raise InvalidSessionKey(
  46. "Invalid characters in session key")
  47. return os.path.join(self.storage_path, self.file_prefix + session_key)
  48. def _last_modification(self):
  49. """
  50. Return the modification time of the file storing the session's content.
  51. """
  52. modification = os.stat(self._key_to_file()).st_mtime
  53. if settings.USE_TZ:
  54. modification = datetime.datetime.utcfromtimestamp(modification)
  55. return modification.replace(tzinfo=timezone.utc)
  56. return datetime.datetime.fromtimestamp(modification)
  57. def _expiry_date(self, session_data):
  58. """
  59. Return the expiry time of the file storing the session's content.
  60. """
  61. return session_data.get('_session_expiry') or (
  62. self._last_modification() + datetime.timedelta(seconds=settings.SESSION_COOKIE_AGE)
  63. )
  64. def load(self):
  65. session_data = {}
  66. try:
  67. with open(self._key_to_file(), "r", encoding="ascii") as session_file:
  68. file_data = session_file.read()
  69. # Don't fail if there is no data in the session file.
  70. # We may have opened the empty placeholder file.
  71. if file_data:
  72. try:
  73. session_data = self.decode(file_data)
  74. except (EOFError, SuspiciousOperation) as e:
  75. if isinstance(e, SuspiciousOperation):
  76. logger = logging.getLogger('django.security.%s' % e.__class__.__name__)
  77. logger.warning(str(e))
  78. self.create()
  79. # Remove expired sessions.
  80. expiry_age = self.get_expiry_age(expiry=self._expiry_date(session_data))
  81. if expiry_age <= 0:
  82. session_data = {}
  83. self.delete()
  84. self.create()
  85. except (IOError, SuspiciousOperation):
  86. self._session_key = None
  87. return session_data
  88. def create(self):
  89. while True:
  90. self._session_key = self._get_new_session_key()
  91. try:
  92. self.save(must_create=True)
  93. except CreateError:
  94. continue
  95. self.modified = True
  96. return
  97. def save(self, must_create=False):
  98. if self.session_key is None:
  99. return self.create()
  100. # Get the session data now, before we start messing
  101. # with the file it is stored within.
  102. session_data = self._get_session(no_load=must_create)
  103. session_file_name = self._key_to_file()
  104. try:
  105. # Make sure the file exists. If it does not already exist, an
  106. # empty placeholder file is created.
  107. flags = os.O_WRONLY | getattr(os, 'O_BINARY', 0)
  108. if must_create:
  109. flags |= os.O_EXCL | os.O_CREAT
  110. fd = os.open(session_file_name, flags)
  111. os.close(fd)
  112. except FileNotFoundError:
  113. if not must_create:
  114. raise UpdateError
  115. except FileExistsError:
  116. if must_create:
  117. raise CreateError
  118. # Write the session file without interfering with other threads
  119. # or processes. By writing to an atomically generated temporary
  120. # file and then using the atomic os.rename() to make the complete
  121. # file visible, we avoid having to lock the session file, while
  122. # still maintaining its integrity.
  123. #
  124. # Note: Locking the session file was explored, but rejected in part
  125. # because in order to be atomic and cross-platform, it required a
  126. # long-lived lock file for each session, doubling the number of
  127. # files in the session storage directory at any given time. This
  128. # rename solution is cleaner and avoids any additional overhead
  129. # when reading the session data, which is the more common case
  130. # unless SESSION_SAVE_EVERY_REQUEST = True.
  131. #
  132. # See ticket #8616.
  133. dir, prefix = os.path.split(session_file_name)
  134. try:
  135. output_file_fd, output_file_name = tempfile.mkstemp(dir=dir, prefix=prefix + '_out_')
  136. renamed = False
  137. try:
  138. try:
  139. os.write(output_file_fd, self.encode(session_data).encode())
  140. finally:
  141. os.close(output_file_fd)
  142. # This will atomically rename the file (os.rename) if the OS
  143. # supports it. Otherwise this will result in a shutil.copy2
  144. # and os.unlink (for example on Windows). See #9084.
  145. shutil.move(output_file_name, session_file_name)
  146. renamed = True
  147. finally:
  148. if not renamed:
  149. os.unlink(output_file_name)
  150. except (OSError, IOError, EOFError):
  151. pass
  152. def exists(self, session_key):
  153. return os.path.exists(self._key_to_file(session_key))
  154. def delete(self, session_key=None):
  155. if session_key is None:
  156. if self.session_key is None:
  157. return
  158. session_key = self.session_key
  159. try:
  160. os.unlink(self._key_to_file(session_key))
  161. except OSError:
  162. pass
  163. def clean(self):
  164. pass
  165. @classmethod
  166. def clear_expired(cls):
  167. storage_path = cls._get_storage_path()
  168. file_prefix = settings.SESSION_COOKIE_NAME
  169. for session_file in os.listdir(storage_path):
  170. if not session_file.startswith(file_prefix):
  171. continue
  172. session_key = session_file[len(file_prefix):]
  173. session = cls(session_key)
  174. # When an expired session is loaded, its file is removed, and a
  175. # new file is immediately created. Prevent this by disabling
  176. # the create() method.
  177. session.create = lambda: None
  178. session.load()