file.py 7.6 KB

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