diff options
Diffstat (limited to 'lib/python2.7/site-packages/django/contrib/sessions/backends')
7 files changed, 853 insertions, 0 deletions
diff --git a/lib/python2.7/site-packages/django/contrib/sessions/backends/__init__.py b/lib/python2.7/site-packages/django/contrib/sessions/backends/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/lib/python2.7/site-packages/django/contrib/sessions/backends/__init__.py diff --git a/lib/python2.7/site-packages/django/contrib/sessions/backends/base.py b/lib/python2.7/site-packages/django/contrib/sessions/backends/base.py new file mode 100644 index 0000000..7f5e958 --- /dev/null +++ b/lib/python2.7/site-packages/django/contrib/sessions/backends/base.py @@ -0,0 +1,327 @@ +from __future__ import unicode_literals + +import base64 +from datetime import datetime, timedelta +import logging +import string + +from django.conf import settings +from django.core.exceptions import SuspiciousOperation +from django.utils.crypto import constant_time_compare +from django.utils.crypto import get_random_string +from django.utils.crypto import salted_hmac +from django.utils import timezone +from django.utils.encoding import force_bytes, force_text +from django.utils.module_loading import import_by_path + +from django.contrib.sessions.exceptions import SuspiciousSession + +# 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 SessionBase(object): + """ + Base class for all Session classes. + """ + TEST_COOKIE_NAME = 'testcookie' + TEST_COOKIE_VALUE = 'worked' + + def __init__(self, session_key=None): + self._session_key = session_key + self.accessed = False + self.modified = False + self.serializer = import_by_path(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 + + def get(self, key, default=None): + return self._session.get(key, default) + + def pop(self, key, *args): + self.modified = self.modified or key in self._session + 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 _hash(self, value): + key_salt = "django.contrib.sessions" + self.__class__.__name__ + return salted_hmac(key_salt, value).hexdigest() + + def encode(self, session_dict): + "Returns the given session dictionary serialized and encoded as a string." + serialized = self.serializer().dumps(session_dict) + hash = self._hash(serialized) + return base64.b64encode(hash.encode() + b":" + serialized).decode('ascii') + + def decode(self, session_data): + encoded_data = base64.b64decode(force_bytes(session_data)) + try: + # could produce ValueError if there is no ':' + hash, serialized = encoded_data.split(b':', 1) + expected_hash = self._hash(serialized) + if not constant_time_compare(hash.decode(), expected_hash): + raise SuspiciousSession("Session data corrupted") + else: + return self.serializer().loads(serialized) + except Exception as e: + # ValueError, SuspiciousOperation, unpickling exceptions. If any of + # these happen, just return an empty dictionary (an empty session). + if isinstance(e, SuspiciousOperation): + logger = logging.getLogger('django.security.%s' % + e.__class__.__name__) + logger.warning(force_text(e)) + 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 iterkeys(self): + return self._session.iterkeys() + + def itervalues(self): + return self._session.itervalues() + + def iteritems(self): + return self._session.iteritems() + + 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 _get_new_session_key(self): + "Returns session key that isn't being used." + while True: + session_key = get_random_string(32, VALID_KEY_CHARS) + if not self.exists(session_key): + break + 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 _get_session_key(self): + return self._session_key + + session_key = property(_get_session_key) + + def _get_session(self, no_load=False): + """ + Lazily loads session from storage (unless "no_load" is True, when only + an empty dict is stored) and stores 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_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 settings.SESSION_COOKIE_AGE + if not isinstance(expiry, datetime): + return 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 + if not expiry: # Checks both None and 0 cases + expiry = settings.SESSION_COOKIE_AGE + return modification + timedelta(seconds=expiry) + + def set_expiry(self, value): + """ + Sets 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 + self['_session_expiry'] = value + + def get_expire_at_browser_close(self): + """ + Returns ``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 self.get('_session_expiry') is None: + return settings.SESSION_EXPIRE_AT_BROWSER_CLOSE + return self.get('_session_expiry') == 0 + + def flush(self): + """ + Removes the current session data from the database and regenerates the + key. + """ + self.clear() + self.delete() + self.create() + + def cycle_key(self): + """ + Creates a new session key, whilst retaining the current session data. + """ + data = self._session_cache + key = self.session_key + self.create() + self._session_cache = data + self.delete(key) + + # Methods that child classes must implement. + + def exists(self, session_key): + """ + Returns True if the given session_key already exists. + """ + raise NotImplementedError + + def create(self): + """ + Creates 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 + + def save(self, must_create=False): + """ + Saves the session data. If 'must_create' is True, a new session object + is created (otherwise a CreateError exception is raised). Otherwise, + save() can update an existing object with the same key. + """ + raise NotImplementedError + + def delete(self, session_key=None): + """ + Deletes the session data under this key. If the key is None, the + current session key value is used. + """ + raise NotImplementedError + + def load(self): + """ + Loads the session data and returns a dictionary. + """ + raise NotImplementedError + + @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 diff --git a/lib/python2.7/site-packages/django/contrib/sessions/backends/cache.py b/lib/python2.7/site-packages/django/contrib/sessions/backends/cache.py new file mode 100644 index 0000000..596042f --- /dev/null +++ b/lib/python2.7/site-packages/django/contrib/sessions/backends/cache.py @@ -0,0 +1,74 @@ +from django.conf import settings +from django.contrib.sessions.backends.base import SessionBase, CreateError +from django.core.cache import get_cache +from django.utils.six.moves import xrange + +KEY_PREFIX = "django.contrib.sessions.cache" + + +class SessionStore(SessionBase): + """ + A cache-based session store. + """ + def __init__(self, session_key=None): + self._cache = get_cache(settings.SESSION_CACHE_ALIAS) + super(SessionStore, self).__init__(session_key) + + @property + def cache_key(self): + return KEY_PREFIX + self._get_or_create_session_key() + + def load(self): + try: + session_data = self._cache.get(self.cache_key, None) + except Exception: + # Some backends (e.g. memcache) raise an exception on invalid + # cache keys. If this happens, reset the session. See #17810. + session_data = None + if session_data is not None: + return session_data + self.create() + return {} + + def create(self): + # Because a cache can fail silently (e.g. memcache), we don't know if + # we are failing to create a new session because of a key collision or + # because the cache is missing. So we try for a (large) number of times + # and then raise an exception. That's the risk you shoulder if using + # cache backing. + for i in xrange(10000): + self._session_key = self._get_new_session_key() + try: + self.save(must_create=True) + except CreateError: + continue + self.modified = True + return + raise RuntimeError( + "Unable to create a new session key. " + "It is likely that the cache is unavailable.") + + def save(self, must_create=False): + if must_create: + func = self._cache.add + else: + func = self._cache.set + result = func(self.cache_key, + self._get_session(no_load=must_create), + self.get_expiry_age()) + if must_create and not result: + raise CreateError + + def exists(self, session_key): + return (KEY_PREFIX + session_key) in self._cache + + def delete(self, session_key=None): + if session_key is None: + if self.session_key is None: + return + session_key = self.session_key + self._cache.delete(KEY_PREFIX + session_key) + + @classmethod + def clear_expired(cls): + pass diff --git a/lib/python2.7/site-packages/django/contrib/sessions/backends/cached_db.py b/lib/python2.7/site-packages/django/contrib/sessions/backends/cached_db.py new file mode 100644 index 0000000..be22c1f --- /dev/null +++ b/lib/python2.7/site-packages/django/contrib/sessions/backends/cached_db.py @@ -0,0 +1,84 @@ +""" +Cached, database-backed sessions. +""" + +import logging + +from django.contrib.sessions.backends.db import SessionStore as DBStore +from django.core.cache import cache +from django.core.exceptions import SuspiciousOperation +from django.utils import timezone +from django.utils.encoding import force_text + +KEY_PREFIX = "django.contrib.sessions.cached_db" + + +class SessionStore(DBStore): + """ + Implements cached, database backed sessions. + """ + + def __init__(self, session_key=None): + super(SessionStore, self).__init__(session_key) + + @property + def cache_key(self): + return KEY_PREFIX + self._get_or_create_session_key() + + def load(self): + try: + data = cache.get(self.cache_key, None) + except Exception: + # Some backends (e.g. memcache) raise an exception on invalid + # cache keys. If this happens, reset the session. See #17810. + data = None + + if data is None: + # Duplicate DBStore.load, because we need to keep track + # of the expiry date to set it properly in the cache. + try: + s = Session.objects.get( + session_key=self.session_key, + expire_date__gt=timezone.now() + ) + data = self.decode(s.session_data) + cache.set(self.cache_key, data, + self.get_expiry_age(expiry=s.expire_date)) + except (Session.DoesNotExist, SuspiciousOperation) as e: + if isinstance(e, SuspiciousOperation): + logger = logging.getLogger('django.security.%s' % + e.__class__.__name__) + logger.warning(force_text(e)) + self.create() + data = {} + return data + + def exists(self, session_key): + if (KEY_PREFIX + session_key) in cache: + return True + return super(SessionStore, self).exists(session_key) + + def save(self, must_create=False): + super(SessionStore, self).save(must_create) + cache.set(self.cache_key, self._session, self.get_expiry_age()) + + def delete(self, session_key=None): + super(SessionStore, self).delete(session_key) + if session_key is None: + if self.session_key is None: + return + session_key = self.session_key + cache.delete(KEY_PREFIX + session_key) + + def flush(self): + """ + Removes the current session data from the database and regenerates the + key. + """ + self.clear() + self.delete(self.session_key) + self.create() + + +# At bottom to avoid circular import +from django.contrib.sessions.models import Session diff --git a/lib/python2.7/site-packages/django/contrib/sessions/backends/db.py b/lib/python2.7/site-packages/django/contrib/sessions/backends/db.py new file mode 100644 index 0000000..7be99c3 --- /dev/null +++ b/lib/python2.7/site-packages/django/contrib/sessions/backends/db.py @@ -0,0 +1,85 @@ +import logging + +from django.contrib.sessions.backends.base import SessionBase, CreateError +from django.core.exceptions import SuspiciousOperation +from django.db import IntegrityError, transaction, router +from django.utils import timezone +from django.utils.encoding import force_text + +class SessionStore(SessionBase): + """ + Implements database session store. + """ + def __init__(self, session_key=None): + super(SessionStore, self).__init__(session_key) + + def load(self): + try: + s = Session.objects.get( + session_key=self.session_key, + expire_date__gt=timezone.now() + ) + return self.decode(s.session_data) + except (Session.DoesNotExist, SuspiciousOperation) as e: + if isinstance(e, SuspiciousOperation): + logger = logging.getLogger('django.security.%s' % + e.__class__.__name__) + logger.warning(force_text(e)) + self.create() + return {} + + def exists(self, session_key): + return Session.objects.filter(session_key=session_key).exists() + + def create(self): + while True: + self._session_key = self._get_new_session_key() + try: + # Save immediately to ensure we have a unique entry in the + # database. + self.save(must_create=True) + except CreateError: + # Key wasn't unique. Try again. + continue + self.modified = True + self._session_cache = {} + return + + def save(self, must_create=False): + """ + Saves the current session data to the database. If 'must_create' is + True, a database error will be raised if the saving operation doesn't + create a *new* entry (as opposed to possibly updating an existing + entry). + """ + obj = Session( + session_key=self._get_or_create_session_key(), + session_data=self.encode(self._get_session(no_load=must_create)), + expire_date=self.get_expiry_date() + ) + using = router.db_for_write(Session, instance=obj) + try: + with transaction.atomic(using=using): + obj.save(force_insert=must_create, using=using) + except IntegrityError: + if must_create: + raise CreateError + raise + + def delete(self, session_key=None): + if session_key is None: + if self.session_key is None: + return + session_key = self.session_key + try: + Session.objects.get(session_key=session_key).delete() + except Session.DoesNotExist: + pass + + @classmethod + def clear_expired(cls): + Session.objects.filter(expire_date__lt=timezone.now()).delete() + + +# At bottom to avoid circular import +from django.contrib.sessions.models import Session diff --git a/lib/python2.7/site-packages/django/contrib/sessions/backends/file.py b/lib/python2.7/site-packages/django/contrib/sessions/backends/file.py new file mode 100644 index 0000000..f47aa2d --- /dev/null +++ b/lib/python2.7/site-packages/django/contrib/sessions/backends/file.py @@ -0,0 +1,202 @@ +import datetime +import errno +import logging +import os +import shutil +import tempfile + +from django.conf import settings +from django.contrib.sessions.backends.base import SessionBase, CreateError, VALID_KEY_CHARS +from django.core.exceptions import SuspiciousOperation, ImproperlyConfigured +from django.utils import timezone +from django.utils.encoding import force_text + +from django.contrib.sessions.exceptions import InvalidSessionKey + +class SessionStore(SessionBase): + """ + Implements a file based session store. + """ + def __init__(self, session_key=None): + self.storage_path = type(self)._get_storage_path() + self.file_prefix = settings.SESSION_COOKIE_NAME + super(SessionStore, self).__init__(session_key) + + @classmethod + def _get_storage_path(cls): + try: + return cls._storage_path + except AttributeError: + storage_path = getattr(settings, "SESSION_FILE_PATH", None) + if not storage_path: + storage_path = tempfile.gettempdir() + + # Make sure the storage path is valid. + if not os.path.isdir(storage_path): + raise ImproperlyConfigured( + "The session storage path %r doesn't exist. Please set your" + " SESSION_FILE_PATH setting to an existing directory in which" + " Django can store session data." % storage_path) + + cls._storage_path = storage_path + return storage_path + + def _key_to_file(self, session_key=None): + """ + Get the file associated with this session key. + """ + if session_key is None: + session_key = self._get_or_create_session_key() + + # Make sure we're not vulnerable to directory traversal. Session keys + # should always be md5s, so they should never contain directory + # components. + if not set(session_key).issubset(set(VALID_KEY_CHARS)): + raise InvalidSessionKey( + "Invalid characters in session key") + + return os.path.join(self.storage_path, self.file_prefix + session_key) + + def _last_modification(self): + """ + Return the modification time of the file storing the session's content. + """ + modification = os.stat(self._key_to_file()).st_mtime + if settings.USE_TZ: + modification = datetime.datetime.utcfromtimestamp(modification) + modification = modification.replace(tzinfo=timezone.utc) + else: + modification = datetime.datetime.fromtimestamp(modification) + return modification + + def load(self): + session_data = {} + try: + with open(self._key_to_file(), "rb") as session_file: + file_data = session_file.read() + # Don't fail if there is no data in the session file. + # We may have opened the empty placeholder file. + if file_data: + try: + session_data = self.decode(file_data) + except (EOFError, SuspiciousOperation) as e: + if isinstance(e, SuspiciousOperation): + logger = logging.getLogger('django.security.%s' % + e.__class__.__name__) + logger.warning(force_text(e)) + self.create() + + # Remove expired sessions. + expiry_age = self.get_expiry_age( + modification=self._last_modification(), + expiry=session_data.get('_session_expiry')) + if expiry_age < 0: + session_data = {} + self.delete() + self.create() + except (IOError, SuspiciousOperation): + self.create() + return session_data + + def create(self): + while True: + self._session_key = self._get_new_session_key() + try: + self.save(must_create=True) + except CreateError: + continue + self.modified = True + self._session_cache = {} + return + + def save(self, must_create=False): + # Get the session data now, before we start messing + # with the file it is stored within. + session_data = self._get_session(no_load=must_create) + + session_file_name = self._key_to_file() + + try: + # Make sure the file exists. If it does not already exist, an + # empty placeholder file is created. + flags = os.O_WRONLY | os.O_CREAT | getattr(os, 'O_BINARY', 0) + if must_create: + flags |= os.O_EXCL + fd = os.open(session_file_name, flags) + os.close(fd) + + except OSError as e: + if must_create and e.errno == errno.EEXIST: + raise CreateError + raise + + # Write the session file without interfering with other threads + # or processes. By writing to an atomically generated temporary + # file and then using the atomic os.rename() to make the complete + # file visible, we avoid having to lock the session file, while + # still maintaining its integrity. + # + # Note: Locking the session file was explored, but rejected in part + # because in order to be atomic and cross-platform, it required a + # long-lived lock file for each session, doubling the number of + # files in the session storage directory at any given time. This + # rename solution is cleaner and avoids any additional overhead + # when reading the session data, which is the more common case + # unless SESSION_SAVE_EVERY_REQUEST = True. + # + # See ticket #8616. + dir, prefix = os.path.split(session_file_name) + + try: + output_file_fd, output_file_name = tempfile.mkstemp(dir=dir, + prefix=prefix + '_out_') + renamed = False + try: + try: + os.write(output_file_fd, self.encode(session_data).encode()) + finally: + os.close(output_file_fd) + + # This will atomically rename the file (os.rename) if the OS + # supports it. Otherwise this will result in a shutil.copy2 + # and os.unlink (for example on Windows). See #9084. + shutil.move(output_file_name, session_file_name) + renamed = True + finally: + if not renamed: + os.unlink(output_file_name) + + except (OSError, IOError, EOFError): + pass + + def exists(self, session_key): + return os.path.exists(self._key_to_file(session_key)) + + def delete(self, session_key=None): + if session_key is None: + if self.session_key is None: + return + session_key = self.session_key + try: + os.unlink(self._key_to_file(session_key)) + except OSError: + pass + + def clean(self): + pass + + @classmethod + def clear_expired(cls): + storage_path = cls._get_storage_path() + file_prefix = settings.SESSION_COOKIE_NAME + + for session_file in os.listdir(storage_path): + if not session_file.startswith(file_prefix): + continue + session_key = session_file[len(file_prefix):] + session = cls(session_key) + # When an expired session is loaded, its file is removed, and a + # new file is immediately created. Prevent this by disabling + # the create() method. + session.create = lambda: None + session.load() diff --git a/lib/python2.7/site-packages/django/contrib/sessions/backends/signed_cookies.py b/lib/python2.7/site-packages/django/contrib/sessions/backends/signed_cookies.py new file mode 100644 index 0000000..77a6750 --- /dev/null +++ b/lib/python2.7/site-packages/django/contrib/sessions/backends/signed_cookies.py @@ -0,0 +1,81 @@ +from django.conf import settings +from django.core import signing + +from django.contrib.sessions.backends.base import SessionBase + + +class SessionStore(SessionBase): + + def load(self): + """ + We load the data from the key itself instead of fetching from + some external data store. Opposite of _get_session_key(), + raises BadSignature if signature fails. + """ + try: + return signing.loads(self.session_key, + serializer=self.serializer, + # This doesn't handle non-default expiry dates, see #19201 + max_age=settings.SESSION_COOKIE_AGE, + salt='django.contrib.sessions.backends.signed_cookies') + except (signing.BadSignature, ValueError): + self.create() + return {} + + def create(self): + """ + To create a new key, we simply make sure that the modified flag is set + so that the cookie is set on the client for the current request. + """ + self.modified = True + + def save(self, must_create=False): + """ + To save, we get the session key as a securely signed string and then + set the modified flag so that the cookie is set on the client for the + current request. + """ + self._session_key = self._get_session_key() + self.modified = True + + def exists(self, session_key=None): + """ + This method makes sense when you're talking to a shared resource, but + it doesn't matter when you're storing the information in the client's + cookie. + """ + return False + + def delete(self, session_key=None): + """ + To delete, we clear the session key and the underlying data structure + and set the modified flag so that the cookie is set on the client for + the current request. + """ + self._session_key = '' + self._session_cache = {} + self.modified = True + + def cycle_key(self): + """ + Keeps the same data but with a new key. To do this, we just have to + call ``save()`` and it will automatically save a cookie with a new key + at the end of the request. + """ + self.save() + + def _get_session_key(self): + """ + Most session backends don't need to override this method, but we do, + because instead of generating a random string, we want to actually + generate a secure url-safe Base64-encoded string of data as our + session key. + """ + session_cache = getattr(self, '_session_cache', {}) + return signing.dumps(session_cache, compress=True, + salt='django.contrib.sessions.backends.signed_cookies', + serializer=self.serializer) + + @classmethod + def clear_expired(cls): + pass |