file.py 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  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)
  27. if not storage_path:
  28. storage_path = tempfile.gettempdir()
  29. # Make sure the storage path is valid.
  30. if not os.path.isdir(storage_path):
  31. raise ImproperlyConfigured(
  32. "The session storage path %r doesn't exist. Please set your"
  33. " SESSION_FILE_PATH setting to an existing directory in which"
  34. " Django can store session data." % storage_path)
  35. cls._storage_path = storage_path
  36. return storage_path
  37. def _key_to_file(self, session_key=None):
  38. """
  39. Get the file associated with this session key.
  40. """
  41. if session_key is None:
  42. session_key = self._get_or_create_session_key()
  43. # Make sure we're not vulnerable to directory traversal. Session keys
  44. # should always be md5s, so they should never contain directory
  45. # components.
  46. if not set(session_key).issubset(VALID_KEY_CHARS):
  47. raise InvalidSessionKey(
  48. "Invalid characters in session key")
  49. return os.path.join(self.storage_path, self.file_prefix + session_key)
  50. def _last_modification(self):
  51. """
  52. Return the modification time of the file storing the session's content.
  53. """
  54. modification = os.stat(self._key_to_file()).st_mtime
  55. if settings.USE_TZ:
  56. modification = datetime.datetime.utcfromtimestamp(modification)
  57. modification = modification.replace(tzinfo=timezone.utc)
  58. else:
  59. modification = datetime.datetime.fromtimestamp(modification)
  60. return modification
  61. def _expiry_date(self, session_data):
  62. """
  63. Return the expiry time of the file storing the session's content.
  64. """
  65. expiry = session_data.get('_session_expiry')
  66. if not expiry:
  67. expiry = self._last_modification() + datetime.timedelta(seconds=settings.SESSION_COOKIE_AGE)
  68. return expiry
  69. def load(self):
  70. session_data = {}
  71. try:
  72. with open(self._key_to_file(), "rb") as session_file:
  73. file_data = session_file.read()
  74. # Don't fail if there is no data in the session file.
  75. # We may have opened the empty placeholder file.
  76. if file_data:
  77. try:
  78. session_data = self.decode(file_data)
  79. except (EOFError, SuspiciousOperation) as e:
  80. if isinstance(e, SuspiciousOperation):
  81. logger = logging.getLogger('django.security.%s' % e.__class__.__name__)
  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 (IOError, 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(dir=dir, prefix=prefix + '_out_')
  141. renamed = False
  142. try:
  143. try:
  144. os.write(output_file_fd, self.encode(session_data).encode())
  145. finally:
  146. os.close(output_file_fd)
  147. # This will atomically rename the file (os.rename) if the OS
  148. # supports it. Otherwise this will result in a shutil.copy2
  149. # and os.unlink (for example on Windows). See #9084.
  150. shutil.move(output_file_name, session_file_name)
  151. renamed = True
  152. finally:
  153. if not renamed:
  154. os.unlink(output_file_name)
  155. except (OSError, IOError, EOFError):
  156. pass
  157. def exists(self, session_key):
  158. return os.path.exists(self._key_to_file(session_key))
  159. def delete(self, session_key=None):
  160. if session_key is None:
  161. if self.session_key is None:
  162. return
  163. session_key = self.session_key
  164. try:
  165. os.unlink(self._key_to_file(session_key))
  166. except OSError:
  167. pass
  168. def clean(self):
  169. pass
  170. @classmethod
  171. def clear_expired(cls):
  172. storage_path = cls._get_storage_path()
  173. file_prefix = settings.SESSION_COOKIE_NAME
  174. for session_file in os.listdir(storage_path):
  175. if not session_file.startswith(file_prefix):
  176. continue
  177. session_key = session_file[len(file_prefix):]
  178. session = cls(session_key)
  179. # When an expired session is loaded, its file is removed, and a
  180. # new file is immediately created. Prevent this by disabling
  181. # the create() method.
  182. session.create = lambda: None
  183. session.load()