Ticket #16199: 16199.7.diff

File 16199.7.diff, 17.4 KB (added by Jannis Leidel, 13 years ago)

Removed code duplication and disabled test.

  • new file django/contrib/sessions/backends/signed_cookies.py

    diff --git a/django/contrib/sessions/backends/signed_cookies.py b/django/contrib/sessions/backends/signed_cookies.py
    new file mode 100644
    index 0000000..966cbf6
    - +  
     1try:
     2    import cPickle as pickle
     3except ImportError:
     4    import pickle
     5
     6from django.conf import settings
     7from django.core import signing
     8
     9from django.contrib.sessions.backends.base import SessionBase
     10
     11
     12class PickleSerializer(object):
     13    """
     14    Simple wrapper around pickle to be used in signing.dumps and
     15    signing.loads.
     16    """
     17    def dumps(self, obj):
     18        return pickle.dumps(obj, pickle.HIGHEST_PROTOCOL)
     19
     20    def loads(self, data):
     21        return pickle.loads(data)
     22
     23
     24class SessionStore(SessionBase):
     25
     26    def load(self):
     27        """
     28        We load the data from the key itself instead of fetching from
     29        some external data store. Opposite of _get_session_key(),
     30        raises BadSignature if signature fails.
     31        """
     32        try:
     33            return signing.loads(self._session_key,
     34                serializer=PickleSerializer,
     35                max_age=settings.SESSION_COOKIE_AGE,
     36                salt='django.contrib.sessions.backends.cookies')
     37        except (signing.BadSignature, ValueError):
     38            self.create()
     39        return {}
     40
     41    def create(self):
     42        """
     43        To create a new key, we simply make sure that the modified flag is set
     44        so that the cookie is set on the client for the current request.
     45        """
     46        self.modified = True
     47
     48    def save(self, must_create=False):
     49        """
     50        To save, we get the session key as a securely signed string and then
     51        set the modified flag so that the cookie is set on the client for the
     52        current request.
     53        """
     54        self._session_key = self._get_session_key()
     55        self.modified = True
     56
     57    def exists(self, session_key=None):
     58        """
     59        This method makes sense when you're talking to a shared resource, but
     60        it doesn't matter when you're storing the information in the client's
     61        cookie.
     62        """
     63        return False
     64
     65    def delete(self, session_key=None):
     66        """
     67        To delete, we clear the session key and the underlying data structure
     68        and set the modified flag so that the cookie is set on the client for
     69        the current request.
     70        """
     71        self._session_key = ''
     72        self._session_cache = {}
     73        self.modified = True
     74
     75    def cycle_key(self):
     76        """
     77        Keeps the same data but with a new key.  To do this, we just have to
     78        call ``save()`` and it will automatically save a cookie with a new key
     79        at the end of the request.
     80        """
     81        self.save()
     82
     83    def _get_session_key(self):
     84        """
     85        Most session backends don't need to override this method, but we do,
     86        because instead of generating a random string, we want to actually
     87        generate a secure url-safe Base64-encoded string of data as our
     88        session key.
     89        """
     90        session_cache = getattr(self, '_session_cache', {})
     91        return signing.dumps(session_cache, compress=True,
     92            salt='django.contrib.sessions.backends.cookies',
     93            serializer=PickleSerializer)
  • django/contrib/sessions/tests.py

    diff --git a/django/contrib/sessions/tests.py b/django/contrib/sessions/tests.py
    index 2eb43f3..2556275 100644
    a b from django.contrib.sessions.backends.db import SessionStore as DatabaseSession  
    77from django.contrib.sessions.backends.cache import SessionStore as CacheSession
    88from django.contrib.sessions.backends.cached_db import SessionStore as CacheDBSession
    99from django.contrib.sessions.backends.file import SessionStore as FileSession
     10from django.contrib.sessions.backends.cookies import SessionStore as CookieSession
    1011from django.contrib.sessions.models import Session
    1112from django.contrib.sessions.middleware import SessionMiddleware
    1213from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation
    1314from django.http import HttpResponse
    1415from django.test import TestCase, RequestFactory
     16from django.test.utils import override_settings
    1517from django.utils import unittest
    1618
    1719
    class SessionTestsMixin(object):  
    213215    def test_get_expire_at_browser_close(self):
    214216        # Tests get_expire_at_browser_close with different settings and different
    215217        # set_expiry calls
    216         try:
    217             try:
    218                 original_expire_at_browser_close = settings.SESSION_EXPIRE_AT_BROWSER_CLOSE
    219                 settings.SESSION_EXPIRE_AT_BROWSER_CLOSE = False
    220 
    221                 self.session.set_expiry(10)
    222                 self.assertFalse(self.session.get_expire_at_browser_close())
    223 
    224                 self.session.set_expiry(0)
    225                 self.assertTrue(self.session.get_expire_at_browser_close())
     218        with override_settings(SESSION_EXPIRE_AT_BROWSER_CLOSE=False):
     219            self.session.set_expiry(10)
     220            self.assertFalse(self.session.get_expire_at_browser_close())
    226221
    227                 self.session.set_expiry(None)
    228                 self.assertFalse(self.session.get_expire_at_browser_close())
     222            self.session.set_expiry(0)
     223            self.assertTrue(self.session.get_expire_at_browser_close())
    229224
    230                 settings.SESSION_EXPIRE_AT_BROWSER_CLOSE = True
     225            self.session.set_expiry(None)
     226            self.assertFalse(self.session.get_expire_at_browser_close())
    231227
    232                 self.session.set_expiry(10)
    233                 self.assertFalse(self.session.get_expire_at_browser_close())
     228        with override_settings(SESSION_EXPIRE_AT_BROWSER_CLOSE=True):
     229            self.session.set_expiry(10)
     230            self.assertFalse(self.session.get_expire_at_browser_close())
    234231
    235                 self.session.set_expiry(0)
    236                 self.assertTrue(self.session.get_expire_at_browser_close())
     232            self.session.set_expiry(0)
     233            self.assertTrue(self.session.get_expire_at_browser_close())
    237234
    238                 self.session.set_expiry(None)
    239                 self.assertTrue(self.session.get_expire_at_browser_close())
    240 
    241             except:
    242                 raise
    243         finally:
    244             settings.SESSION_EXPIRE_AT_BROWSER_CLOSE = original_expire_at_browser_close
     235            self.session.set_expiry(None)
     236            self.assertTrue(self.session.get_expire_at_browser_close())
    245237
    246238    def test_decode(self):
    247239        # Ensure we can decode what we encode
    class FileSessionTests(SessionTestsMixin, unittest.TestCase):  
    302294        shutil.rmtree(self.temp_session_store)
    303295        super(FileSessionTests, self).tearDown()
    304296
     297    @override_settings(
     298        SESSION_FILE_PATH="/if/this/directory/exists/you/have/a/weird/computer")
    305299    def test_configuration_check(self):
    306300        # Make sure the file backend checks for a good storage dir
    307         settings.SESSION_FILE_PATH = "/if/this/directory/exists/you/have/a/weird/computer"
    308301        self.assertRaises(ImproperlyConfigured, self.backend)
    309302
    310303    def test_invalid_key_backslash(self):
    class CacheSessionTests(SessionTestsMixin, unittest.TestCase):  
    324317
    325318
    326319class SessionMiddlewareTests(unittest.TestCase):
    327     def setUp(self):
    328         self.old_SESSION_COOKIE_SECURE = settings.SESSION_COOKIE_SECURE
    329         self.old_SESSION_COOKIE_HTTPONLY = settings.SESSION_COOKIE_HTTPONLY
    330 
    331     def tearDown(self):
    332         settings.SESSION_COOKIE_SECURE = self.old_SESSION_COOKIE_SECURE
    333         settings.SESSION_COOKIE_HTTPONLY = self.old_SESSION_COOKIE_HTTPONLY
    334320
     321    @override_settings(SESSION_COOKIE_SECURE=True)
    335322    def test_secure_session_cookie(self):
    336         settings.SESSION_COOKIE_SECURE = True
    337 
    338323        request = RequestFactory().get('/')
    339324        response = HttpResponse('Session test')
    340325        middleware = SessionMiddleware()
    class SessionMiddlewareTests(unittest.TestCase):  
    347332        response = middleware.process_response(request, response)
    348333        self.assertTrue(response.cookies[settings.SESSION_COOKIE_NAME]['secure'])
    349334
     335    @override_settings(SESSION_COOKIE_HTTPONLY=True)
    350336    def test_httponly_session_cookie(self):
    351         settings.SESSION_COOKIE_HTTPONLY = True
    352 
    353337        request = RequestFactory().get('/')
    354338        response = HttpResponse('Session test')
    355339        middleware = SessionMiddleware()
    class SessionMiddlewareTests(unittest.TestCase):  
    361345        # Handle the response through the middleware
    362346        response = middleware.process_response(request, response)
    363347        self.assertTrue(response.cookies[settings.SESSION_COOKIE_NAME]['httponly'])
     348
     349
     350class CookieSessionTests(SessionTestsMixin, TestCase):
     351
     352    backend = CookieSession
     353
     354    def test_save(self):
     355        """
     356        This test tested exists() in the other session backends, but that
     357        doesn't make sense for us.
     358        """
     359        pass
     360
     361    def test_cycle(self):
     362        """
     363        This test tested cycle_key() which would create a new session
     364        key for the same session data. But we can't invalidate previously
     365        signed cookies (other than letting them expire naturally) so
     366        testing for this behaviour is meaningless.
     367        """
     368        pass
  • django/core/signing.py

    diff --git a/django/core/signing.py b/django/core/signing.py
    index 054777a..8987d9d 100644
    a b Functions for creating and restoring url-safe signed JSON objects.  
    33
    44The format used looks like this:
    55
    6 >>> signed.dumps("hello")
    7 'ImhlbGxvIg.RjVSUCt6S64WBilMYxG89-l0OA8'
     6>>> signing.dumps("hello")
     7'ImhlbGxvIg:1QaUZC:YIye-ze3TTx7gtSv422nZA4sgmk'
    88
    9 There are two components here, separatad by a '.'. The first component is a
     9There are two components here, separatad by a ':'. The first component is a
    1010URLsafe base64 encoded JSON of the object passed to dumps(). The second
    11 component is a base64 encoded hmac/SHA1 hash of "$first_component.$secret"
     11component is a base64 encoded hmac/SHA1 hash of "$first_component:$secret"
    1212
    13 signed.loads(s) checks the signature and returns the deserialised object.
     13signing.loads(s) checks the signature and returns the deserialised object.
    1414If the signature fails, a BadSignature exception is raised.
    1515
    16 >>> signed.loads("ImhlbGxvIg.RjVSUCt6S64WBilMYxG89-l0OA8")
     16>>> signing.loads("ImhlbGxvIg:1QaUZC:YIye-ze3TTx7gtSv422nZA4sgmk")
    1717u'hello'
    18 >>> signed.loads("ImhlbGxvIg.RjVSUCt6S64WBilMYxG89-l0OA8-modified")
     18>>> signing.loads("ImhlbGxvIg:1QaUZC:YIye-ze3TTx7gtSv422nZA4sgmk-modified")
    1919...
    20 BadSignature: Signature failed: RjVSUCt6S64WBilMYxG89-l0OA8-modified
     20BadSignature: Signature failed: ImhlbGxvIg:1QaUZC:YIye-ze3TTx7gtSv422nZA4sgmk-modified
    2121
    2222You can optionally compress the JSON prior to base64 encoding it to save
    2323space, using the compress=True argument. This checks if compression actually
    2424helps and only applies compression if the result is a shorter string:
    2525
    26 >>> signed.dumps(range(1, 20), compress=True)
    27 '.eJwFwcERACAIwLCF-rCiILN47r-GyZVJsNgkxaFxoDgxcOHGxMKD_T7vhAml.oFq6lAAEbkHXBHfGnVX7Qx6NlZ8'
     26>>> signing.dumps(range(1, 20), compress=True)
     27'.eJwFwcERACAIwLCF-rCiILN47r-GyZVJsNgkxaFxoDgxcOHGxMKD_T7vhAml:1QaUaL:BA0thEZrp4FQVXIXuOvYJtLJSrQ'
    2828
    2929The fact that the string is compressed is signalled by the prefixed '.' at the
    3030start of the base64 JSON.
    3131
    32 There are 65 url-safe characters: the 64 used by url-safe base64 and the '.'.
     32There are 65 url-safe characters: the 64 used by url-safe base64 and the ':'.
    3333These functions make use of all of them.
    3434"""
    3535import base64
    def get_cookie_signer(salt='django.core.signing.get_cookie_signer'):  
    8787    return Signer('django.http.cookies' + settings.SECRET_KEY, salt=salt)
    8888
    8989
    90 def dumps(obj, key=None, salt='django.core.signing', compress=False):
     90class JSONSerializer(object):
     91    """
     92    Simple wrapper around simplejson to be used in signing.dumps and
     93    signing.loads.
     94    """
     95    def dumps(self, obj):
     96        return simplejson.dumps(obj, separators=(',', ':'))
     97
     98    def loads(self, data):
     99        return simplejson.loads(data)
     100
     101
     102def dumps(obj, key=None, salt='django.core.signing', serializer=JSONSerializer, compress=False):
    91103    """
    92104    Returns URL-safe, sha1 signed base64 compressed JSON string. If key is
    93105    None, settings.SECRET_KEY is used instead.
    def dumps(obj, key=None, salt='django.core.signing', compress=False):  
    99111    Salt can be used to further salt the hash, in case you're worried
    100112    that the NSA might try to brute-force your SHA-1 protected secret.
    101113    """
    102     json = simplejson.dumps(obj, separators=(',', ':'))
     114    data = serializer().dumps(obj)
    103115
    104116    # Flag for if it's been compressed or not
    105117    is_compressed = False
    106118
    107119    if compress:
    108120        # Avoid zlib dependency unless compress is being used
    109         compressed = zlib.compress(json)
    110         if len(compressed) < (len(json) - 1):
    111             json = compressed
     121        compressed = zlib.compress(data)
     122        if len(compressed) < (len(data) - 1):
     123            data = compressed
    112124            is_compressed = True
    113     base64d = b64_encode(json)
     125    base64d = b64_encode(data)
    114126    if is_compressed:
    115127        base64d = '.' + base64d
    116128    return TimestampSigner(key, salt=salt).sign(base64d)
    117129
    118130
    119 def loads(s, key=None, salt='django.core.signing', max_age=None):
     131def loads(s, key=None, salt='django.core.signing', serializer=JSONSerializer, max_age=None):
    120132    """
    121133    Reverse of dumps(), raises BadSignature if signature fails
    122134    """
    def loads(s, key=None, salt='django.core.signing', max_age=None):  
    127139        # It's compressed; uncompress it first
    128140        base64d = base64d[1:]
    129141        decompress = True
    130     json = b64_decode(base64d)
     142    data = b64_decode(base64d)
    131143    if decompress:
    132         json = zlib.decompress(json)
    133     return simplejson.loads(json)
     144        data = zlib.decompress(data)
     145    return serializer().loads(data)
    134146
    135147
    136148class Signer(object):
    class Signer(object):  
    158170
    159171
    160172class TimestampSigner(Signer):
     173
    161174    def timestamp(self):
    162175        return baseconv.base62.encode(int(time.time()))
    163176
  • docs/releases/1.4.txt

    diff --git a/docs/releases/1.4.txt b/docs/releases/1.4.txt
    index 0635ece..0ab5c05 100644
    a b signing in Web applications.  
    8989
    9090See :doc:`cryptographic signing </topics/signing>` docs for more information.
    9191
     92Cookie-based session backend
     93~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     94
     95Django 1.4 introduces a new cookie based backend for the session framework
     96which uses the tools for :doc:`cryptographic signing </topics/signing>` to
     97store the session data in the client's browser.
     98
     99See the :ref:`cookie-based backend <cookie-session-backend>` docs for
     100more information.
     101
    92102New form wizard
    93103~~~~~~~~~~~~~~~
    94104
  • docs/topics/http/sessions.txt

    diff --git a/docs/topics/http/sessions.txt b/docs/topics/http/sessions.txt
    index 8529f53..19b1e0d 100644
    a b How to use sessions  
    55.. module:: django.contrib.sessions
    66   :synopsis: Provides session management for Django projects.
    77
    8 Django provides full support for anonymous sessions. The session framework lets
    9 you store and retrieve arbitrary data on a per-site-visitor basis. It stores
    10 data on the server side and abstracts the sending and receiving of cookies.
    11 Cookies contain a session ID -- not the data itself.
     8Django provides full support for anonymous sessions. The session framework
     9lets you store and retrieve arbitrary data on a per-site-visitor basis. It
     10stores data on the server side and abstracts the sending and receiving of
     11cookies. Cookies contain a session ID -- not the data itself (unless you're
     12using the :ref:`cookie based backend<cookie-session-backend>`).
    1213
    1314Enabling sessions
    1415=================
    defaults to output from ``tempfile.gettempdir()``, most likely ``/tmp``) to  
    9596control where Django stores session files. Be sure to check that your Web
    9697server has permissions to read and write to this location.
    9798
     99.. _cookie-session-backend:
     100
     101Using cookie-based sessions
     102---------------------------
     103
     104.. versionadded:: 1.4
     105
     106To use cookies-based sessions, set the :setting:`SESSION_ENGINE` setting to
     107``"django.contrib.sessions.backends.cookies"``. The session data will be
     108stored using Django's tools for :doc:`cryptographic signing </topics/signing>`
     109and the :setting:`SECRET_KEY` setting. It's recommended to set the
     110:setting:`SESSION_COOKIE_HTTPONLY` to ``True`` to prevent tampering from
     111JavaScript.
     112
     113.. warning::
     114
     115    **The session data is signed but not encrypted!**
     116
     117    When using the cookies backend the session data can be read out
     118    and will be invalidated when being tampered with. The same invalidation
     119    happens if the client storing the cookie (e.g. your user's browser)
     120    can't store all of the session cookie and drops data. Even though
     121    Django compresses the data, it's still entirely possible to exceed
     122    the `common limit of 4096 bytes`_ per cookie.
     123
     124    Also, the size of a cookie can have an impact on the `speed of your site`_.
     125
     126.. _`common limit of 4096 bytes`: http://tools.ietf.org/html/rfc2965#section-5.3
     127.. _`speed of your site`: http://yuiblog.com/blog/2007/03/01/performance-research-part-3/
    98128
    99129Using sessions in views
    100130=======================
    Controls where Django stores session data. Valid values are:  
    420450    * ``'django.contrib.sessions.backends.file'``
    421451    * ``'django.contrib.sessions.backends.cache'``
    422452    * ``'django.contrib.sessions.backends.cached_db'``
     453    * ``'django.contrib.sessions.backends.signed_cookies'``
    423454
    424455See `configuring the session engine`_ for more details.
    425456
Back to Top