Permalink
Browse files

Fixed #2066: session data can now be stored in the cache or on the fi…

…lesystem. This should be fully backwards-compatible (the database cache store is still the default). A big thanks to John D'Agostino for the bulk of this code.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@6333 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
1 parent e6460e4 commit bcf7e9a9fe037eff4d5dea0cdd8c35104590e1a8 @jacobian jacobian committed Sep 15, 2007
View
@@ -87,6 +87,7 @@ answer newbie questions, and generally made Django that much better:
Matt Croydon <http://www.postneo.com/>
flavio.curella@gmail.com
Jure Cuhalev <gandalf@owca.info>
+ John D'Agostino <john.dagostino@gmail.com>
dackze+django@gmail.com
David Danier <goliath.mailinglist@gmx.de>
Dirk Datzert <dummy@habmalnefrage.de>
@@ -271,12 +271,14 @@
# SESSIONS #
############
-SESSION_COOKIE_NAME = 'sessionid' # Cookie name. This can be whatever you want.
-SESSION_COOKIE_AGE = 60 * 60 * 24 * 7 * 2 # Age of cookie, in seconds (default: 2 weeks).
-SESSION_COOKIE_DOMAIN = None # A string like ".lawrence.com", or None for standard domain cookie.
-SESSION_COOKIE_SECURE = False # Whether the session cookie should be secure (https:// only).
-SESSION_SAVE_EVERY_REQUEST = False # Whether to save the session data on every request.
-SESSION_EXPIRE_AT_BROWSER_CLOSE = False # Whether sessions expire when a user closes his browser.
+SESSION_COOKIE_NAME = 'sessionid' # Cookie name. This can be whatever you want.
+SESSION_COOKIE_AGE = 60 * 60 * 24 * 7 * 2 # Age of cookie, in seconds (default: 2 weeks).
+SESSION_COOKIE_DOMAIN = None # A string like ".lawrence.com", or None for standard domain cookie.
+SESSION_COOKIE_SECURE = False # Whether the session cookie should be secure (https:// only).
+SESSION_SAVE_EVERY_REQUEST = False # Whether to save the session data on every request.
+SESSION_EXPIRE_AT_BROWSER_CLOSE = False # Whether sessions expire when a user closes his browser.
+SESSION_ENGINE = 'django.contrib.sessions.backends.db' # The module to store session data
+SESSION_FILE_PATH = '/tmp/' # Directory to store session files if using the file session module
#########
# CACHE #
@@ -0,0 +1,143 @@
+import base64
+import md5
+import os
+import random
+import sys
+import time
+from django.conf import settings
+from django.core.exceptions import SuspiciousOperation
+
+try:
+ import cPickle as pickle
+except ImportError:
+ import pickle
+
+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
+
+ 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 keys(self):
+ return self._session.keys()
+
+ def items(self):
+ return self._session.items()
+
+ def get(self, key, default=None):
+ return self._session.get(key, default)
+
+ def pop(self, key, *args):
+ return self._session.pop(key, *args)
+
+ 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):
+ "Returns the given session dictionary pickled and encoded as a string."
+ pickled = pickle.dumps(session_dict, pickle.HIGHEST_PROTOCOL)
+ pickled_md5 = md5.new(pickled + settings.SECRET_KEY).hexdigest()
+ return base64.encodestring(pickled + pickled_md5)
+
+ def decode(self, session_data):
+ encoded_data = base64.decodestring(session_data)
+ pickled, tamper_check = encoded_data[:-32], encoded_data[-32:]
+ if md5.new(pickled + settings.SECRET_KEY).hexdigest() != tamper_check:
+ raise SuspiciousOperation("User tampered with session cookie.")
+ try:
+ return pickle.loads(pickled)
+ # Unpickling can cause a variety of exceptions. If something happens,
+ # just return an empty dictionary (an empty session).
+ except:
+ return {}
+
+ def _get_new_session_key(self):
+ "Returns session key that isn't being used."
+ # The random module is seeded when this Apache child is created.
+ # Use settings.SECRET_KEY as added salt.
+ while 1:
+ session_key = md5.new("%s%s%s%s" % (random.randint(0, sys.maxint - 1),
+ os.getpid(), time.time(), settings.SECRET_KEY)).hexdigest()
+ if not self.exists(session_key):
+ break
+ return session_key
+
+ def _get_session_key(self):
+ if self._session_key:
+ return self._session_key
+ else:
+ self._session_key = self._get_new_session_key()
+ return self._session_key
+
+ def _set_session_key(self, session_key):
+ self._session_key = session_key
+
+ session_key = property(_get_session_key, _set_session_key)
+
+ def _get_session(self):
+ # Lazily loads session from storage.
+ self.accessed = True
+ try:
+ return self._session_cache
+ except AttributeError:
+ if self.session_key is None:
+ self._session_cache = {}
+ else:
+ self._session_cache = self.load()
+ return self._session_cache
+
+ _session = property(_get_session)
+
+ # Methods that child classes must implement.
+
+ def exists(self, session_key):
+ """
+ Returns True if the given session_key already exists.
+ """
+ raise NotImplementedError
+
+ def save(self):
+ """
+ Saves the session data.
+ """
+ raise NotImplementedError
+
+ def delete(self, session_key):
+ """
+ Clears out the session data under this key.
+ """
+ raise NotImplementedError
+
+ def load(self):
+ """
+ Loads the session data and returns a dictionary.
+ """
+ raise NotImplementedError
+
@@ -0,0 +1,26 @@
+from django.conf import settings
+from django.contrib.sessions.backends.base import SessionBase
+from django.core.cache import cache
+
+class SessionStore(SessionBase):
+ """
+ A cache-based session store.
+ """
+ def __init__(self, session_key=None):
+ self._cache = cache
+ super(SessionStore, self).__init__(session_key)
+
+ def load(self):
+ session_data = self._cache.get(self.session_key)
+ return session_data or {}
+
+ def save(self):
+ self._cache.set(self.session_key, self._session, settings.SESSION_COOKIE_AGE)
+
+ def exists(self, session_key):
+ if self._cache.get(session_key):
+ return True
+ return False
+
+ def delete(self, session_key):
+ self._cache.delete(session_key)
@@ -0,0 +1,49 @@
+from django.conf import settings
+from django.contrib.sessions.models import Session
+from django.contrib.sessions.backends.base import SessionBase
+from django.core.exceptions import SuspiciousOperation
+import datetime
+
+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=datetime.datetime.now()
+ )
+ return self.decode(s.session_data)
+ except (Session.DoesNotExist, SuspiciousOperation):
+
+ # Create a new session_key for extra security.
+ self.session_key = self._get_new_session_key()
+ self._session_cache = {}
+
+ # Save immediately to minimize collision
+ self.save()
+ return {}
+
+ def exists(self, session_key):
+ try:
+ Session.objects.get(session_key=session_key)
+ except Session.DoesNotExist:
+ return False
+ return True
+
+ def save(self):
+ Session.objects.create(
+ session_key = self.session_key,
+ session_data = self.encode(self._session),
+ expire_date = datetime.datetime.now() + datetime.timedelta(seconds=settings.SESSION_COOKIE_AGE)
+ )
+
+ def delete(self, session_key):
+ try:
+ Session.objects.get(session_key=session_key).delete()
+ except Session.DoesNotExist:
+ pass
@@ -0,0 +1,67 @@
+import os
+from django.conf import settings
+from django.contrib.sessions.backends.base import SessionBase
+from django.core.exceptions import SuspiciousOperation
+
+class SessionStore(SessionBase):
+ """
+ Implements a file based session store.
+ """
+ def __init__(self, session_key=None):
+ self.storage_path = settings.SESSION_FILE_PATH
+ self.file_prefix = settings.SESSION_COOKIE_NAME
+ super(SessionStore, self).__init__(session_key)
+
+ def _key_to_file(self, session_key=None):
+ """
+ Get the file associated with this session key.
+ """
+ if session_key is None:
+ session_key = self.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 os.path.sep in session_key:
+ raise SuspiciousOperation("Invalid characters (directory components) in session key")
+
+ return os.path.join(self.storage_path, self.file_prefix + session_key)
+
+ def load(self):
+ session_data = {}
+ try:
+ session_file = open(self._key_to_file(), "rb")
+ try:
+ session_data = self.decode(session_file.read())
+ except(EOFError, SuspiciousOperation):
+ self._session_key = self._get_new_session_key()
+ self._session_cache = {}
+ self.save()
+ finally:
+ session_file.close()
+ except(IOError):
+ pass
+ return session_data
+
+ def save(self):
+ try:
+ f = open(self._key_to_file(self.session_key), "wb")
+ try:
+ f.write(self.encode(self._session))
+ finally:
+ f.close()
+ except(IOError, EOFError):
+ pass
+
+ def exists(self, session_key):
+ if os.path.exists(self._key_to_file(session_key)):
+ return True
+ return False
+
+ def delete(self, session_key):
+ try:
+ os.unlink(self._key_to_file(session_key))
+ except OSError:
+ pass
+
+ def clean(self):
+ pass
Oops, something went wrong.

0 comments on commit bcf7e9a

Please sign in to comment.