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.

file.py 7.5 KiB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. import datetime
  2. import errno
  3. import logging
  4. import os
  5. import shutil
  6. import tempfile
  7. from django.conf import settings
  8. from django.contrib.sessions.backends.base import (
  9. VALID_KEY_CHARS, CreateError, SessionBase,
  10. )
  11. from django.contrib.sessions.exceptions import InvalidSessionKey
  12. from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation
  13. from django.utils import timezone
  14. from django.utils.encoding import force_text
  15. class SessionStore(SessionBase):
  16. """
  17. Implements a file based session store.
  18. """
  19. def __init__(self, session_key=None):
  20. self.storage_path = type(self)._get_storage_path()
  21. self.file_prefix = settings.SESSION_COOKIE_NAME
  22. super(SessionStore, self).__init__(session_key)
  23. @classmethod
  24. def _get_storage_path(cls):
  25. try:
  26. return cls._storage_path
  27. except AttributeError:
  28. storage_path = getattr(settings, "SESSION_FILE_PATH", None)
  29. if not storage_path:
  30. storage_path = tempfile.gettempdir()
  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. cls._storage_path = storage_path
  38. return storage_path
  39. def _key_to_file(self, session_key=None):
  40. """
  41. Get the file associated with this session key.
  42. """
  43. if session_key is None:
  44. session_key = self._get_or_create_session_key()
  45. # Make sure we're not vulnerable to directory traversal. Session keys
  46. # should always be md5s, so they should never contain directory
  47. # components.
  48. if not set(session_key).issubset(set(VALID_KEY_CHARS)):
  49. raise InvalidSessionKey(
  50. "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. if settings.USE_TZ:
  58. modification = datetime.datetime.utcfromtimestamp(modification)
  59. modification = modification.replace(tzinfo=timezone.utc)
  60. else:
  61. modification = datetime.datetime.fromtimestamp(modification)
  62. return modification
  63. def load(self):
  64. session_data = {}
  65. try:
  66. with open(self._key_to_file(), "rb") as session_file:
  67. file_data = session_file.read()
  68. # Don't fail if there is no data in the session file.
  69. # We may have opened the empty placeholder file.
  70. if file_data:
  71. try:
  72. session_data = self.decode(file_data)
  73. except (EOFError, SuspiciousOperation) as e:
  74. if isinstance(e, SuspiciousOperation):
  75. logger = logging.getLogger('django.security.%s' %
  76. e.__class__.__name__)
  77. logger.warning(force_text(e))
  78. self.create()
  79. # Remove expired sessions.
  80. expiry_age = self.get_expiry_age(
  81. modification=self._last_modification(),
  82. expiry=session_data.get('_session_expiry'))
  83. if expiry_age < 0:
  84. session_data = {}
  85. self.delete()
  86. self.create()
  87. except (IOError, SuspiciousOperation):
  88. self._session_key = None
  89. return session_data
  90. def create(self):
  91. while True:
  92. self._session_key = self._get_new_session_key()
  93. try:
  94. self.save(must_create=True)
  95. except CreateError:
  96. continue
  97. self.modified = True
  98. return
  99. def save(self, must_create=False):
  100. if self.session_key is None:
  101. return self.create()
  102. # Get the session data now, before we start messing
  103. # with the file it is stored within.
  104. session_data = self._get_session(no_load=must_create)
  105. session_file_name = self._key_to_file()
  106. try:
  107. # Make sure the file exists. If it does not already exist, an
  108. # empty placeholder file is created.
  109. flags = os.O_WRONLY | os.O_CREAT | getattr(os, 'O_BINARY', 0)
  110. if must_create:
  111. flags |= os.O_EXCL
  112. fd = os.open(session_file_name, flags)
  113. os.close(fd)
  114. except OSError as e:
  115. if must_create and e.errno == errno.EEXIST:
  116. raise CreateError
  117. raise
  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,
  136. prefix=prefix + '_out_')
  137. renamed = False
  138. try:
  139. try:
  140. os.write(output_file_fd, self.encode(session_data).encode())
  141. finally:
  142. os.close(output_file_fd)
  143. # This will atomically rename the file (os.rename) if the OS
  144. # supports it. Otherwise this will result in a shutil.copy2
  145. # and os.unlink (for example on Windows). See #9084.
  146. shutil.move(output_file_name, session_file_name)
  147. renamed = True
  148. finally:
  149. if not renamed:
  150. os.unlink(output_file_name)
  151. except (OSError, IOError, EOFError):
  152. pass
  153. def exists(self, session_key):
  154. return os.path.exists(self._key_to_file(session_key))
  155. def delete(self, session_key=None):
  156. if session_key is None:
  157. if self.session_key is None:
  158. return
  159. session_key = self.session_key
  160. try:
  161. os.unlink(self._key_to_file(session_key))
  162. except OSError:
  163. pass
  164. def clean(self):
  165. pass
  166. @classmethod
  167. def clear_expired(cls):
  168. storage_path = cls._get_storage_path()
  169. file_prefix = settings.SESSION_COOKIE_NAME
  170. for session_file in os.listdir(storage_path):
  171. if not session_file.startswith(file_prefix):
  172. continue
  173. session_key = session_file[len(file_prefix):]
  174. session = cls(session_key)
  175. # When an expired session is loaded, its file is removed, and a
  176. # new file is immediately created. Prevent this by disabling
  177. # the create() method.
  178. session.create = lambda: None
  179. session.load()