import logging import string from datetime import datetime, timedelta from django.conf import settings from django.core import signing from django.utils import timezone from django.utils.crypto import get_random_string from django.utils.module_loading import import_string # session_key should not be case sensitive because some backends can store it # on case insensitive file systems. VALID_KEY_CHARS = string.ascii_lowercase + string.digits class CreateError(Exception): """ Used internally as a consistent exception type to catch from save (see the docstring for SessionBase.save() for details). """ pass class UpdateError(Exception): """ Occurs if Django tries to update a session that was deleted. """ pass class SessionBase: """ Base class for all Session classes. """ TEST_COOKIE_NAME = "testcookie" TEST_COOKIE_VALUE = "worked" __not_given = object() def __init__(self, session_key=None): self._session_key = session_key self.accessed = False self.modified = False self.serializer = import_string(settings.SESSION_SERIALIZER) def __contains__(self, key): return key in self._session def __getitem__(self, key): return self._session[key] def __setitem__(self, key, value): self._session[key] = value self.modified = True def __delitem__(self, key): del self._session[key] self.modified = True @property def key_salt(self): return "django.contrib.sessions." + self.__class__.__qualname__ def get(self, key, default=None): return self._session.get(key, default) def pop(self, key, default=__not_given): self.modified = self.modified or key in self._session args = () if default is self.__not_given else (default,) return self._session.pop(key, *args) def setdefault(self, key, value): if key in self._session: return self._session[key] else: self.modified = True self._session[key] = value return value def set_test_cookie(self): self[self.TEST_COOKIE_NAME] = self.TEST_COOKIE_VALUE def test_cookie_worked(self): return self.get(self.TEST_COOKIE_NAME) == self.TEST_COOKIE_VALUE def delete_test_cookie(self): del self[self.TEST_COOKIE_NAME] def encode(self, session_dict): "Return the given session dictionary serialized and encoded as a string." return signing.dumps( session_dict, salt=self.key_salt, serializer=self.serializer, compress=True, ) def decode(self, session_data): try: return signing.loads( session_data, salt=self.key_salt, serializer=self.serializer ) except signing.BadSignature: logger = logging.getLogger("django.security.SuspiciousSession") logger.warning("Session data corrupted") except Exception: # ValueError, unpickling exceptions. If any of these happen, just # return an empty dictionary (an empty session). pass return {} def update(self, dict_): self._session.update(dict_) self.modified = True def has_key(self, key): return key in self._session def keys(self): return self._session.keys() def values(self): return self._session.values() def items(self): return self._session.items() def clear(self): # To avoid unnecessary persistent storage accesses, we set up the # internals directly (loading data wastes time, since we are going to # set it to an empty dict anyway). self._session_cache = {} self.accessed = True self.modified = True def is_empty(self): "Return True when there is no session_key and the session is empty." try: return not self._session_key and not self._session_cache except AttributeError: return True def _get_new_session_key(self): "Return session key that isn't being used." while True: session_key = get_random_string(32, VALID_KEY_CHARS) if not self.exists(session_key): return session_key def _get_or_create_session_key(self): if self._session_key is None: self._session_key = self._get_new_session_key() return self._session_key def _validate_session_key(self, key): """ Key must be truthy and at least 8 characters long. 8 characters is an arbitrary lower bound for some minimal key security. """ return key and len(key) >= 8 def _get_session_key(self): return self.__session_key def _set_session_key(self, value): """ Validate session key on assignment. Invalid values will set to None. """ if self._validate_session_key(value): self.__session_key = value else: self.__session_key = None session_key = property(_get_session_key) _session_key = property(_get_session_key, _set_session_key) def _get_session(self, no_load=False): """ Lazily load session from storage (unless "no_load" is True, when only an empty dict is stored) and store it in the current instance. """ self.accessed = True try: return self._session_cache except AttributeError: if self.session_key is None or no_load: self._session_cache = {} else: self._session_cache = self.load() return self._session_cache _session = property(_get_session) def get_session_cookie_age(self): return settings.SESSION_COOKIE_AGE def get_expiry_age(self, **kwargs): """Get the number of seconds until the session expires. Optionally, this function accepts `modification` and `expiry` keyword arguments specifying the modification and expiry of the session. """ try: modification = kwargs["modification"] except KeyError: modification = timezone.now() # Make the difference between "expiry=None passed in kwargs" and # "expiry not passed in kwargs", in order to guarantee not to trigger # self.load() when expiry is provided. try: expiry = kwargs["expiry"] except KeyError: expiry = self.get("_session_expiry") if not expiry: # Checks both None and 0 cases return self.get_session_cookie_age() if not isinstance(expiry, (datetime, str)): return expiry if isinstance(expiry, str): expiry = datetime.fromisoformat(expiry) delta = expiry - modification return delta.days * 86400 + delta.seconds def get_expiry_date(self, **kwargs): """Get session the expiry date (as a datetime object). Optionally, this function accepts `modification` and `expiry` keyword arguments specifying the modification and expiry of the session. """ try: modification = kwargs["modification"] except KeyError: modification = timezone.now() # Same comment as in get_expiry_age try: expiry = kwargs["expiry"] except KeyError: expiry = self.get("_session_expiry") if isinstance(expiry, datetime): return expiry elif isinstance(expiry, str): return datetime.fromisoformat(expiry) expiry = expiry or self.get_session_cookie_age() return modification + timedelta(seconds=expiry) def set_expiry(self, value): """ Set a custom expiration for the session. ``value`` can be an integer, a Python ``datetime`` or ``timedelta`` object or ``None``. If ``value`` is an integer, the session will expire after that many seconds of inactivity. If set to ``0`` then the session will expire on browser close. If ``value`` is a ``datetime`` or ``timedelta`` object, the session will expire at that specific future time. If ``value`` is ``None``, the session uses the global session expiry policy. """ if value is None: # Remove any custom expiration for this session. try: del self["_session_expiry"] except KeyError: pass return if isinstance(value, timedelta): value = timezone.now() + value if isinstance(value, datetime): value = value.isoformat() self["_session_expiry"] = value def get_expire_at_browser_close(self): """ Return ``True`` if the session is set to expire when the browser closes, and ``False`` if there's an expiry date. Use ``get_expiry_date()`` or ``get_expiry_age()`` to find the actual expiry date/age, if there is one. """ if (expiry := self.get("_session_expiry")) is None: return settings.SESSION_EXPIRE_AT_BROWSER_CLOSE return expiry == 0 def flush(self): """ Remove the current session data from the database and regenerate the key. """ self.clear() self.delete() self._session_key = None def cycle_key(self): """ Create a new session key, while retaining the current session data. """ data = self._session key = self.session_key self.create() self._session_cache = data if key: self.delete(key) # Methods that child classes must implement. def exists(self, session_key): """ Return True if the given session_key already exists. """ raise NotImplementedError( "subclasses of SessionBase must provide an exists() method" ) def create(self): """ Create a new session instance. Guaranteed to create a new object with a unique key and will have saved the result once (with empty data) before the method returns. """ raise NotImplementedError( "subclasses of SessionBase must provide a create() method" ) def save(self, must_create=False): """ Save the session data. If 'must_create' is True, create a new session object (or raise CreateError). Otherwise, only update an existing object and don't create one (raise UpdateError if needed). """ raise NotImplementedError( "subclasses of SessionBase must provide a save() method" ) def delete(self, session_key=None): """ Delete the session data under this key. If the key is None, use the current session key value. """ raise NotImplementedError( "subclasses of SessionBase must provide a delete() method" ) def load(self): """ Load the session data and return a dictionary. """ raise NotImplementedError( "subclasses of SessionBase must provide a load() method" ) @classmethod def clear_expired(cls): """ Remove expired sessions from the session store. If this operation isn't possible on a given backend, it should raise NotImplementedError. If it isn't necessary, because the backend has a built-in expiration mechanism, it should be a no-op. """ raise NotImplementedError("This backend does not support clear_expired().")