file.py 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  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. async def aload(self):
  94. return self.load()
  95. def create(self):
  96. while True:
  97. self._session_key = self._get_new_session_key()
  98. try:
  99. self.save(must_create=True)
  100. except CreateError:
  101. continue
  102. self.modified = True
  103. return
  104. async def acreate(self):
  105. return self.create()
  106. def save(self, must_create=False):
  107. if self.session_key is None:
  108. return self.create()
  109. # Get the session data now, before we start messing
  110. # with the file it is stored within.
  111. session_data = self._get_session(no_load=must_create)
  112. session_file_name = self._key_to_file()
  113. try:
  114. # Make sure the file exists. If it does not already exist, an
  115. # empty placeholder file is created.
  116. flags = os.O_WRONLY | getattr(os, "O_BINARY", 0)
  117. if must_create:
  118. flags |= os.O_EXCL | os.O_CREAT
  119. fd = os.open(session_file_name, flags)
  120. os.close(fd)
  121. except FileNotFoundError:
  122. if not must_create:
  123. raise UpdateError
  124. except FileExistsError:
  125. if must_create:
  126. raise CreateError
  127. # Write the session file without interfering with other threads
  128. # or processes. By writing to an atomically generated temporary
  129. # file and then using the atomic os.rename() to make the complete
  130. # file visible, we avoid having to lock the session file, while
  131. # still maintaining its integrity.
  132. #
  133. # Note: Locking the session file was explored, but rejected in part
  134. # because in order to be atomic and cross-platform, it required a
  135. # long-lived lock file for each session, doubling the number of
  136. # files in the session storage directory at any given time. This
  137. # rename solution is cleaner and avoids any additional overhead
  138. # when reading the session data, which is the more common case
  139. # unless SESSION_SAVE_EVERY_REQUEST = True.
  140. #
  141. # See ticket #8616.
  142. dir, prefix = os.path.split(session_file_name)
  143. try:
  144. output_file_fd, output_file_name = tempfile.mkstemp(
  145. dir=dir, prefix=prefix + "_out_"
  146. )
  147. renamed = False
  148. try:
  149. try:
  150. os.write(output_file_fd, self.encode(session_data).encode())
  151. finally:
  152. os.close(output_file_fd)
  153. # This will atomically rename the file (os.rename) if the OS
  154. # supports it. Otherwise this will result in a shutil.copy2
  155. # and os.unlink (for example on Windows). See #9084.
  156. shutil.move(output_file_name, session_file_name)
  157. renamed = True
  158. finally:
  159. if not renamed:
  160. os.unlink(output_file_name)
  161. except (EOFError, OSError):
  162. pass
  163. async def asave(self, must_create=False):
  164. return self.save(must_create=must_create)
  165. def exists(self, session_key):
  166. return os.path.exists(self._key_to_file(session_key))
  167. async def aexists(self, session_key):
  168. return self.exists(session_key)
  169. def delete(self, session_key=None):
  170. if session_key is None:
  171. if self.session_key is None:
  172. return
  173. session_key = self.session_key
  174. try:
  175. os.unlink(self._key_to_file(session_key))
  176. except OSError:
  177. pass
  178. async def adelete(self, session_key=None):
  179. return self.delete(session_key=session_key)
  180. @classmethod
  181. def clear_expired(cls):
  182. storage_path = cls._get_storage_path()
  183. file_prefix = settings.SESSION_COOKIE_NAME
  184. for session_file in os.listdir(storage_path):
  185. if not session_file.startswith(file_prefix):
  186. continue
  187. session_key = session_file.removeprefix(file_prefix)
  188. session = cls(session_key)
  189. # When an expired session is loaded, its file is removed, and a
  190. # new file is immediately created. Prevent this by disabling
  191. # the create() method.
  192. session.create = lambda: None
  193. session.load()
  194. @classmethod
  195. async def aclear_expired(cls):
  196. cls.clear_expired()