Added guaranteed atomic creation of new session objects. Slightly backwards
incompatible for custom session backends. Whilst we were in the neighbourhood, use a larger range of session key values to save a small amount of time and use the hardware-base random numbers where available (transparently falls back to pseudo-RNG otherwise). Fixed #1080 git-svn-id: http://code.djangoproject.com/svn/django/trunk@8340 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
parent
81fba12daa
commit
af7b6475ca
|
@ -13,6 +13,19 @@ from django.conf import settings
|
||||||
from django.core.exceptions import SuspiciousOperation
|
from django.core.exceptions import SuspiciousOperation
|
||||||
from django.utils.hashcompat import md5_constructor
|
from django.utils.hashcompat import md5_constructor
|
||||||
|
|
||||||
|
# Use the system (hardware-based) random number generator if it exists.
|
||||||
|
if hasattr(random, 'SystemRandom'):
|
||||||
|
randint = random.SystemRandom().randint
|
||||||
|
else:
|
||||||
|
randint = random.randint
|
||||||
|
MAX_SESSION_KEY = 18446744073709551616L # 2 << 63
|
||||||
|
|
||||||
|
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):
|
class SessionBase(object):
|
||||||
"""
|
"""
|
||||||
|
@ -117,8 +130,9 @@ class SessionBase(object):
|
||||||
# No getpid() in Jython, for example
|
# No getpid() in Jython, for example
|
||||||
pid = 1
|
pid = 1
|
||||||
while 1:
|
while 1:
|
||||||
session_key = md5_constructor("%s%s%s%s" % (random.randint(0, sys.maxint - 1),
|
session_key = md5_constructor("%s%s%s%s"
|
||||||
pid, time.time(), settings.SECRET_KEY)).hexdigest()
|
% (random.randrange(0, MAX_SESSION_KEY), pid, time.time(),
|
||||||
|
settings.SECRET_KEY)).hexdigest()
|
||||||
if not self.exists(session_key):
|
if not self.exists(session_key):
|
||||||
break
|
break
|
||||||
return session_key
|
return session_key
|
||||||
|
@ -213,9 +227,19 @@ class SessionBase(object):
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def save(self):
|
def create(self):
|
||||||
"""
|
"""
|
||||||
Saves the session data.
|
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
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from django.contrib.sessions.backends.base import SessionBase
|
from django.contrib.sessions.backends.base import SessionBase, CreateError
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
|
|
||||||
class SessionStore(SessionBase):
|
class SessionStore(SessionBase):
|
||||||
|
@ -11,10 +11,28 @@ class SessionStore(SessionBase):
|
||||||
|
|
||||||
def load(self):
|
def load(self):
|
||||||
session_data = self._cache.get(self.session_key)
|
session_data = self._cache.get(self.session_key)
|
||||||
return session_data or {}
|
if session_data is not None:
|
||||||
|
return session_data
|
||||||
|
self.create()
|
||||||
|
|
||||||
def save(self):
|
def create(self):
|
||||||
self._cache.set(self.session_key, self._session, self.get_expiry_age())
|
while True:
|
||||||
|
self.session_key = self._get_new_session_key()
|
||||||
|
try:
|
||||||
|
self.save(must_create=True)
|
||||||
|
except CreateError:
|
||||||
|
continue
|
||||||
|
self.modified = True
|
||||||
|
return
|
||||||
|
|
||||||
|
def save(self, must_create=False):
|
||||||
|
if must_create:
|
||||||
|
func = self._cache.add
|
||||||
|
else:
|
||||||
|
func = self._cache.set
|
||||||
|
result = func(self.session_key, self._session, self.get_expiry_age())
|
||||||
|
if must_create and not result:
|
||||||
|
raise CreateError
|
||||||
|
|
||||||
def exists(self, session_key):
|
def exists(self, session_key):
|
||||||
if self._cache.get(session_key):
|
if self._cache.get(session_key):
|
||||||
|
@ -23,3 +41,4 @@ class SessionStore(SessionBase):
|
||||||
|
|
||||||
def delete(self, session_key):
|
def delete(self, session_key):
|
||||||
self._cache.delete(session_key)
|
self._cache.delete(session_key)
|
||||||
|
|
||||||
|
|
|
@ -1,15 +1,13 @@
|
||||||
import datetime
|
import datetime
|
||||||
from django.contrib.sessions.models import Session
|
from django.contrib.sessions.models import Session
|
||||||
from django.contrib.sessions.backends.base import SessionBase
|
from django.contrib.sessions.backends.base import SessionBase, CreateError
|
||||||
from django.core.exceptions import SuspiciousOperation
|
from django.core.exceptions import SuspiciousOperation
|
||||||
|
from django.db import IntegrityError, transaction
|
||||||
|
|
||||||
class SessionStore(SessionBase):
|
class SessionStore(SessionBase):
|
||||||
"""
|
"""
|
||||||
Implements database session store.
|
Implements database session store.
|
||||||
"""
|
"""
|
||||||
def __init__(self, session_key=None):
|
|
||||||
super(SessionStore, self).__init__(session_key)
|
|
||||||
|
|
||||||
def load(self):
|
def load(self):
|
||||||
try:
|
try:
|
||||||
s = Session.objects.get(
|
s = Session.objects.get(
|
||||||
|
@ -18,15 +16,7 @@ class SessionStore(SessionBase):
|
||||||
)
|
)
|
||||||
return self.decode(s.session_data)
|
return self.decode(s.session_data)
|
||||||
except (Session.DoesNotExist, SuspiciousOperation):
|
except (Session.DoesNotExist, SuspiciousOperation):
|
||||||
|
self.create()
|
||||||
# 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()
|
|
||||||
# Ensure the user is notified via a new cookie.
|
|
||||||
self.modified = True
|
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
def exists(self, session_key):
|
def exists(self, session_key):
|
||||||
|
@ -36,12 +26,40 @@ class SessionStore(SessionBase):
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def save(self):
|
def create(self):
|
||||||
Session.objects.create(
|
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.session_key,
|
session_key = self.session_key,
|
||||||
session_data = self.encode(self._session),
|
session_data = self.encode(self._session),
|
||||||
expire_date = self.get_expiry_date()
|
expire_date = self.get_expiry_date()
|
||||||
)
|
)
|
||||||
|
sid = transaction.savepoint()
|
||||||
|
try:
|
||||||
|
obj.save(force_insert=must_create)
|
||||||
|
except IntegrityError:
|
||||||
|
if must_create:
|
||||||
|
transaction.savepoint_rollback(sid)
|
||||||
|
raise CreateError
|
||||||
|
raise
|
||||||
|
|
||||||
def delete(self, session_key):
|
def delete(self, session_key):
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -2,7 +2,7 @@ import os
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.sessions.backends.base import SessionBase
|
from django.contrib.sessions.backends.base import SessionBase, CreateError
|
||||||
from django.core.exceptions import SuspiciousOperation, ImproperlyConfigured
|
from django.core.exceptions import SuspiciousOperation, ImproperlyConfigured
|
||||||
|
|
||||||
|
|
||||||
|
@ -48,26 +48,40 @@ class SessionStore(SessionBase):
|
||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
session_data = self.decode(session_file.read())
|
session_data = self.decode(session_file.read())
|
||||||
except(EOFError, SuspiciousOperation):
|
except (EOFError, SuspiciousOperation):
|
||||||
self._session_key = self._get_new_session_key()
|
self.create()
|
||||||
self._session_cache = {}
|
|
||||||
self.save()
|
|
||||||
# Ensure the user is notified via a new cookie.
|
|
||||||
self.modified = True
|
|
||||||
finally:
|
finally:
|
||||||
session_file.close()
|
session_file.close()
|
||||||
except(IOError):
|
except IOError:
|
||||||
pass
|
pass
|
||||||
return session_data
|
return session_data
|
||||||
|
|
||||||
def save(self):
|
def create(self):
|
||||||
try:
|
while True:
|
||||||
f = open(self._key_to_file(self.session_key), "wb")
|
self._session_key = self._get_new_session_key()
|
||||||
try:
|
try:
|
||||||
f.write(self.encode(self._session))
|
self.save(must_create=True)
|
||||||
|
except CreateError:
|
||||||
|
continue
|
||||||
|
self.modified = True
|
||||||
|
self._session_cache = {}
|
||||||
|
return
|
||||||
|
|
||||||
|
def save(self, must_create=False):
|
||||||
|
flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC | getattr(os, 'O_BINARY', 0)
|
||||||
|
if must_create:
|
||||||
|
flags |= os.O_EXCL
|
||||||
|
try:
|
||||||
|
fd = os.open(self._key_to_file(self.session_key), flags)
|
||||||
|
try:
|
||||||
|
os.write(fd, self.encode(self._session))
|
||||||
finally:
|
finally:
|
||||||
f.close()
|
os.close(fd)
|
||||||
except(IOError, EOFError):
|
except OSError, e:
|
||||||
|
if must_create and e.errno == errno.EEXIST:
|
||||||
|
raise CreateError
|
||||||
|
raise
|
||||||
|
except (IOError, EOFError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def exists(self, session_key):
|
def exists(self, session_key):
|
||||||
|
|
Loading…
Reference in New Issue