Skip to content

Commit

Permalink
Fixed #2066: session data can now be stored in the cache or on the fi…
Browse files Browse the repository at this point in the history
…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
jacobian committed Sep 15, 2007
1 parent e6460e4 commit bcf7e9a
Show file tree
Hide file tree
Showing 13 changed files with 467 additions and 120 deletions.
1 change: 1 addition & 0 deletions AUTHORS
Expand Up @@ -87,6 +87,7 @@ answer newbie questions, and generally made Django that much better:
Matt Croydon <http://www.postneo.com/> Matt Croydon <http://www.postneo.com/>
flavio.curella@gmail.com flavio.curella@gmail.com
Jure Cuhalev <gandalf@owca.info> Jure Cuhalev <gandalf@owca.info>
John D'Agostino <john.dagostino@gmail.com>
dackze+django@gmail.com dackze+django@gmail.com
David Danier <goliath.mailinglist@gmx.de> David Danier <goliath.mailinglist@gmx.de>
Dirk Datzert <dummy@habmalnefrage.de> Dirk Datzert <dummy@habmalnefrage.de>
Expand Down
14 changes: 8 additions & 6 deletions django/conf/global_settings.py
Expand Up @@ -271,12 +271,14 @@
# SESSIONS # # SESSIONS #
############ ############


SESSION_COOKIE_NAME = 'sessionid' # Cookie name. This can be whatever you want. 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_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_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_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_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_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 # # CACHE #
Expand Down
Empty file.
143 changes: 143 additions & 0 deletions django/contrib/sessions/backends/base.py
@@ -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

26 changes: 26 additions & 0 deletions django/contrib/sessions/backends/cache.py
@@ -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)
49 changes: 49 additions & 0 deletions django/contrib/sessions/backends/db.py
@@ -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
67 changes: 67 additions & 0 deletions django/contrib/sessions/backends/file.py
@@ -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

0 comments on commit bcf7e9a

Please sign in to comment.