Code

Ticket #16199: 16199.7.diff

File 16199.7.diff, 17.4 KB (added by jezdez, 3 years ago)

Removed code duplication and disabled test.

Line 
1diff --git a/django/contrib/sessions/backends/signed_cookies.py b/django/contrib/sessions/backends/signed_cookies.py
2new file mode 100644
3index 0000000..966cbf6
4--- /dev/null
5+++ b/django/contrib/sessions/backends/signed_cookies.py
6@@ -0,0 +1,93 @@
7+try:
8+    import cPickle as pickle
9+except ImportError:
10+    import pickle
11+
12+from django.conf import settings
13+from django.core import signing
14+
15+from django.contrib.sessions.backends.base import SessionBase
16+
17+
18+class PickleSerializer(object):
19+    """
20+    Simple wrapper around pickle to be used in signing.dumps and
21+    signing.loads.
22+    """
23+    def dumps(self, obj):
24+        return pickle.dumps(obj, pickle.HIGHEST_PROTOCOL)
25+
26+    def loads(self, data):
27+        return pickle.loads(data)
28+
29+
30+class SessionStore(SessionBase):
31+
32+    def load(self):
33+        """
34+        We load the data from the key itself instead of fetching from
35+        some external data store. Opposite of _get_session_key(),
36+        raises BadSignature if signature fails.
37+        """
38+        try:
39+            return signing.loads(self._session_key,
40+                serializer=PickleSerializer,
41+                max_age=settings.SESSION_COOKIE_AGE,
42+                salt='django.contrib.sessions.backends.cookies')
43+        except (signing.BadSignature, ValueError):
44+            self.create()
45+        return {}
46+
47+    def create(self):
48+        """
49+        To create a new key, we simply make sure that the modified flag is set
50+        so that the cookie is set on the client for the current request.
51+        """
52+        self.modified = True
53+
54+    def save(self, must_create=False):
55+        """
56+        To save, we get the session key as a securely signed string and then
57+        set the modified flag so that the cookie is set on the client for the
58+        current request.
59+        """
60+        self._session_key = self._get_session_key()
61+        self.modified = True
62+
63+    def exists(self, session_key=None):
64+        """
65+        This method makes sense when you're talking to a shared resource, but
66+        it doesn't matter when you're storing the information in the client's
67+        cookie.
68+        """
69+        return False
70+
71+    def delete(self, session_key=None):
72+        """
73+        To delete, we clear the session key and the underlying data structure
74+        and set the modified flag so that the cookie is set on the client for
75+        the current request.
76+        """
77+        self._session_key = ''
78+        self._session_cache = {}
79+        self.modified = True
80+
81+    def cycle_key(self):
82+        """
83+        Keeps the same data but with a new key.  To do this, we just have to
84+        call ``save()`` and it will automatically save a cookie with a new key
85+        at the end of the request.
86+        """
87+        self.save()
88+
89+    def _get_session_key(self):
90+        """
91+        Most session backends don't need to override this method, but we do,
92+        because instead of generating a random string, we want to actually
93+        generate a secure url-safe Base64-encoded string of data as our
94+        session key.
95+        """
96+        session_cache = getattr(self, '_session_cache', {})
97+        return signing.dumps(session_cache, compress=True,
98+            salt='django.contrib.sessions.backends.cookies',
99+            serializer=PickleSerializer)
100diff --git a/django/contrib/sessions/tests.py b/django/contrib/sessions/tests.py
101index 2eb43f3..2556275 100644
102--- a/django/contrib/sessions/tests.py
103+++ b/django/contrib/sessions/tests.py
104@@ -7,11 +7,13 @@ from django.contrib.sessions.backends.db import SessionStore as DatabaseSession
105 from django.contrib.sessions.backends.cache import SessionStore as CacheSession
106 from django.contrib.sessions.backends.cached_db import SessionStore as CacheDBSession
107 from django.contrib.sessions.backends.file import SessionStore as FileSession
108+from django.contrib.sessions.backends.cookies import SessionStore as CookieSession
109 from django.contrib.sessions.models import Session
110 from django.contrib.sessions.middleware import SessionMiddleware
111 from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation
112 from django.http import HttpResponse
113 from django.test import TestCase, RequestFactory
114+from django.test.utils import override_settings
115 from django.utils import unittest
116 
117 
118@@ -213,35 +215,25 @@ class SessionTestsMixin(object):
119     def test_get_expire_at_browser_close(self):
120         # Tests get_expire_at_browser_close with different settings and different
121         # set_expiry calls
122-        try:
123-            try:
124-                original_expire_at_browser_close = settings.SESSION_EXPIRE_AT_BROWSER_CLOSE
125-                settings.SESSION_EXPIRE_AT_BROWSER_CLOSE = False
126-
127-                self.session.set_expiry(10)
128-                self.assertFalse(self.session.get_expire_at_browser_close())
129-
130-                self.session.set_expiry(0)
131-                self.assertTrue(self.session.get_expire_at_browser_close())
132+        with override_settings(SESSION_EXPIRE_AT_BROWSER_CLOSE=False):
133+            self.session.set_expiry(10)
134+            self.assertFalse(self.session.get_expire_at_browser_close())
135 
136-                self.session.set_expiry(None)
137-                self.assertFalse(self.session.get_expire_at_browser_close())
138+            self.session.set_expiry(0)
139+            self.assertTrue(self.session.get_expire_at_browser_close())
140 
141-                settings.SESSION_EXPIRE_AT_BROWSER_CLOSE = True
142+            self.session.set_expiry(None)
143+            self.assertFalse(self.session.get_expire_at_browser_close())
144 
145-                self.session.set_expiry(10)
146-                self.assertFalse(self.session.get_expire_at_browser_close())
147+        with override_settings(SESSION_EXPIRE_AT_BROWSER_CLOSE=True):
148+            self.session.set_expiry(10)
149+            self.assertFalse(self.session.get_expire_at_browser_close())
150 
151-                self.session.set_expiry(0)
152-                self.assertTrue(self.session.get_expire_at_browser_close())
153+            self.session.set_expiry(0)
154+            self.assertTrue(self.session.get_expire_at_browser_close())
155 
156-                self.session.set_expiry(None)
157-                self.assertTrue(self.session.get_expire_at_browser_close())
158-
159-            except:
160-                raise
161-        finally:
162-            settings.SESSION_EXPIRE_AT_BROWSER_CLOSE = original_expire_at_browser_close
163+            self.session.set_expiry(None)
164+            self.assertTrue(self.session.get_expire_at_browser_close())
165 
166     def test_decode(self):
167         # Ensure we can decode what we encode
168@@ -302,9 +294,10 @@ class FileSessionTests(SessionTestsMixin, unittest.TestCase):
169         shutil.rmtree(self.temp_session_store)
170         super(FileSessionTests, self).tearDown()
171 
172+    @override_settings(
173+        SESSION_FILE_PATH="/if/this/directory/exists/you/have/a/weird/computer")
174     def test_configuration_check(self):
175         # Make sure the file backend checks for a good storage dir
176-        settings.SESSION_FILE_PATH = "/if/this/directory/exists/you/have/a/weird/computer"
177         self.assertRaises(ImproperlyConfigured, self.backend)
178 
179     def test_invalid_key_backslash(self):
180@@ -324,17 +317,9 @@ class CacheSessionTests(SessionTestsMixin, unittest.TestCase):
181 
182 
183 class SessionMiddlewareTests(unittest.TestCase):
184-    def setUp(self):
185-        self.old_SESSION_COOKIE_SECURE = settings.SESSION_COOKIE_SECURE
186-        self.old_SESSION_COOKIE_HTTPONLY = settings.SESSION_COOKIE_HTTPONLY
187-
188-    def tearDown(self):
189-        settings.SESSION_COOKIE_SECURE = self.old_SESSION_COOKIE_SECURE
190-        settings.SESSION_COOKIE_HTTPONLY = self.old_SESSION_COOKIE_HTTPONLY
191 
192+    @override_settings(SESSION_COOKIE_SECURE=True)
193     def test_secure_session_cookie(self):
194-        settings.SESSION_COOKIE_SECURE = True
195-
196         request = RequestFactory().get('/')
197         response = HttpResponse('Session test')
198         middleware = SessionMiddleware()
199@@ -347,9 +332,8 @@ class SessionMiddlewareTests(unittest.TestCase):
200         response = middleware.process_response(request, response)
201         self.assertTrue(response.cookies[settings.SESSION_COOKIE_NAME]['secure'])
202 
203+    @override_settings(SESSION_COOKIE_HTTPONLY=True)
204     def test_httponly_session_cookie(self):
205-        settings.SESSION_COOKIE_HTTPONLY = True
206-
207         request = RequestFactory().get('/')
208         response = HttpResponse('Session test')
209         middleware = SessionMiddleware()
210@@ -361,3 +345,24 @@ class SessionMiddlewareTests(unittest.TestCase):
211         # Handle the response through the middleware
212         response = middleware.process_response(request, response)
213         self.assertTrue(response.cookies[settings.SESSION_COOKIE_NAME]['httponly'])
214+
215+
216+class CookieSessionTests(SessionTestsMixin, TestCase):
217+
218+    backend = CookieSession
219+
220+    def test_save(self):
221+        """
222+        This test tested exists() in the other session backends, but that
223+        doesn't make sense for us.
224+        """
225+        pass
226+
227+    def test_cycle(self):
228+        """
229+        This test tested cycle_key() which would create a new session
230+        key for the same session data. But we can't invalidate previously
231+        signed cookies (other than letting them expire naturally) so
232+        testing for this behaviour is meaningless.
233+        """
234+        pass
235diff --git a/django/core/signing.py b/django/core/signing.py
236index 054777a..8987d9d 100644
237--- a/django/core/signing.py
238+++ b/django/core/signing.py
239@@ -3,33 +3,33 @@ Functions for creating and restoring url-safe signed JSON objects.
240 
241 The format used looks like this:
242 
243->>> signed.dumps("hello")
244-'ImhlbGxvIg.RjVSUCt6S64WBilMYxG89-l0OA8'
245+>>> signing.dumps("hello")
246+'ImhlbGxvIg:1QaUZC:YIye-ze3TTx7gtSv422nZA4sgmk'
247 
248-There are two components here, separatad by a '.'. The first component is a
249+There are two components here, separatad by a ':'. The first component is a
250 URLsafe base64 encoded JSON of the object passed to dumps(). The second
251-component is a base64 encoded hmac/SHA1 hash of "$first_component.$secret"
252+component is a base64 encoded hmac/SHA1 hash of "$first_component:$secret"
253 
254-signed.loads(s) checks the signature and returns the deserialised object.
255+signing.loads(s) checks the signature and returns the deserialised object.
256 If the signature fails, a BadSignature exception is raised.
257 
258->>> signed.loads("ImhlbGxvIg.RjVSUCt6S64WBilMYxG89-l0OA8")
259+>>> signing.loads("ImhlbGxvIg:1QaUZC:YIye-ze3TTx7gtSv422nZA4sgmk")
260 u'hello'
261->>> signed.loads("ImhlbGxvIg.RjVSUCt6S64WBilMYxG89-l0OA8-modified")
262+>>> signing.loads("ImhlbGxvIg:1QaUZC:YIye-ze3TTx7gtSv422nZA4sgmk-modified")
263 ...
264-BadSignature: Signature failed: RjVSUCt6S64WBilMYxG89-l0OA8-modified
265+BadSignature: Signature failed: ImhlbGxvIg:1QaUZC:YIye-ze3TTx7gtSv422nZA4sgmk-modified
266 
267 You can optionally compress the JSON prior to base64 encoding it to save
268 space, using the compress=True argument. This checks if compression actually
269 helps and only applies compression if the result is a shorter string:
270 
271->>> signed.dumps(range(1, 20), compress=True)
272-'.eJwFwcERACAIwLCF-rCiILN47r-GyZVJsNgkxaFxoDgxcOHGxMKD_T7vhAml.oFq6lAAEbkHXBHfGnVX7Qx6NlZ8'
273+>>> signing.dumps(range(1, 20), compress=True)
274+'.eJwFwcERACAIwLCF-rCiILN47r-GyZVJsNgkxaFxoDgxcOHGxMKD_T7vhAml:1QaUaL:BA0thEZrp4FQVXIXuOvYJtLJSrQ'
275 
276 The fact that the string is compressed is signalled by the prefixed '.' at the
277 start of the base64 JSON.
278 
279-There are 65 url-safe characters: the 64 used by url-safe base64 and the '.'.
280+There are 65 url-safe characters: the 64 used by url-safe base64 and the ':'.
281 These functions make use of all of them.
282 """
283 import base64
284@@ -87,7 +87,19 @@ def get_cookie_signer(salt='django.core.signing.get_cookie_signer'):
285     return Signer('django.http.cookies' + settings.SECRET_KEY, salt=salt)
286 
287 
288-def dumps(obj, key=None, salt='django.core.signing', compress=False):
289+class JSONSerializer(object):
290+    """
291+    Simple wrapper around simplejson to be used in signing.dumps and
292+    signing.loads.
293+    """
294+    def dumps(self, obj):
295+        return simplejson.dumps(obj, separators=(',', ':'))
296+
297+    def loads(self, data):
298+        return simplejson.loads(data)
299+
300+
301+def dumps(obj, key=None, salt='django.core.signing', serializer=JSONSerializer, compress=False):
302     """
303     Returns URL-safe, sha1 signed base64 compressed JSON string. If key is
304     None, settings.SECRET_KEY is used instead.
305@@ -99,24 +111,24 @@ def dumps(obj, key=None, salt='django.core.signing', compress=False):
306     Salt can be used to further salt the hash, in case you're worried
307     that the NSA might try to brute-force your SHA-1 protected secret.
308     """
309-    json = simplejson.dumps(obj, separators=(',', ':'))
310+    data = serializer().dumps(obj)
311 
312     # Flag for if it's been compressed or not
313     is_compressed = False
314 
315     if compress:
316         # Avoid zlib dependency unless compress is being used
317-        compressed = zlib.compress(json)
318-        if len(compressed) < (len(json) - 1):
319-            json = compressed
320+        compressed = zlib.compress(data)
321+        if len(compressed) < (len(data) - 1):
322+            data = compressed
323             is_compressed = True
324-    base64d = b64_encode(json)
325+    base64d = b64_encode(data)
326     if is_compressed:
327         base64d = '.' + base64d
328     return TimestampSigner(key, salt=salt).sign(base64d)
329 
330 
331-def loads(s, key=None, salt='django.core.signing', max_age=None):
332+def loads(s, key=None, salt='django.core.signing', serializer=JSONSerializer, max_age=None):
333     """
334     Reverse of dumps(), raises BadSignature if signature fails
335     """
336@@ -127,10 +139,10 @@ def loads(s, key=None, salt='django.core.signing', max_age=None):
337         # It's compressed; uncompress it first
338         base64d = base64d[1:]
339         decompress = True
340-    json = b64_decode(base64d)
341+    data = b64_decode(base64d)
342     if decompress:
343-        json = zlib.decompress(json)
344-    return simplejson.loads(json)
345+        data = zlib.decompress(data)
346+    return serializer().loads(data)
347 
348 
349 class Signer(object):
350@@ -158,6 +170,7 @@ class Signer(object):
351 
352 
353 class TimestampSigner(Signer):
354+
355     def timestamp(self):
356         return baseconv.base62.encode(int(time.time()))
357 
358diff --git a/docs/releases/1.4.txt b/docs/releases/1.4.txt
359index 0635ece..0ab5c05 100644
360--- a/docs/releases/1.4.txt
361+++ b/docs/releases/1.4.txt
362@@ -89,6 +89,16 @@ signing in Web applications.
363 
364 See :doc:`cryptographic signing </topics/signing>` docs for more information.
365 
366+Cookie-based session backend
367+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
368+
369+Django 1.4 introduces a new cookie based backend for the session framework
370+which uses the tools for :doc:`cryptographic signing </topics/signing>` to
371+store the session data in the client's browser.
372+
373+See the :ref:`cookie-based backend <cookie-session-backend>` docs for
374+more information.
375+
376 New form wizard
377 ~~~~~~~~~~~~~~~
378 
379diff --git a/docs/topics/http/sessions.txt b/docs/topics/http/sessions.txt
380index 8529f53..19b1e0d 100644
381--- a/docs/topics/http/sessions.txt
382+++ b/docs/topics/http/sessions.txt
383@@ -5,10 +5,11 @@ How to use sessions
384 .. module:: django.contrib.sessions
385    :synopsis: Provides session management for Django projects.
386 
387-Django provides full support for anonymous sessions. The session framework lets
388-you store and retrieve arbitrary data on a per-site-visitor basis. It stores
389-data on the server side and abstracts the sending and receiving of cookies.
390-Cookies contain a session ID -- not the data itself.
391+Django provides full support for anonymous sessions. The session framework
392+lets you store and retrieve arbitrary data on a per-site-visitor basis. It
393+stores data on the server side and abstracts the sending and receiving of
394+cookies. Cookies contain a session ID -- not the data itself (unless you're
395+using the :ref:`cookie based backend<cookie-session-backend>`).
396 
397 Enabling sessions
398 =================
399@@ -95,6 +96,35 @@ defaults to output from ``tempfile.gettempdir()``, most likely ``/tmp``) to
400 control where Django stores session files. Be sure to check that your Web
401 server has permissions to read and write to this location.
402 
403+.. _cookie-session-backend:
404+
405+Using cookie-based sessions
406+---------------------------
407+
408+.. versionadded:: 1.4
409+
410+To use cookies-based sessions, set the :setting:`SESSION_ENGINE` setting to
411+``"django.contrib.sessions.backends.cookies"``. The session data will be
412+stored using Django's tools for :doc:`cryptographic signing </topics/signing>`
413+and the :setting:`SECRET_KEY` setting. It's recommended to set the
414+:setting:`SESSION_COOKIE_HTTPONLY` to ``True`` to prevent tampering from
415+JavaScript.
416+
417+.. warning::
418+
419+    **The session data is signed but not encrypted!**
420+
421+    When using the cookies backend the session data can be read out
422+    and will be invalidated when being tampered with. The same invalidation
423+    happens if the client storing the cookie (e.g. your user's browser)
424+    can't store all of the session cookie and drops data. Even though
425+    Django compresses the data, it's still entirely possible to exceed
426+    the `common limit of 4096 bytes`_ per cookie.
427+
428+    Also, the size of a cookie can have an impact on the `speed of your site`_.
429+
430+.. _`common limit of 4096 bytes`: http://tools.ietf.org/html/rfc2965#section-5.3
431+.. _`speed of your site`: http://yuiblog.com/blog/2007/03/01/performance-research-part-3/
432 
433 Using sessions in views
434 =======================
435@@ -420,6 +450,7 @@ Controls where Django stores session data. Valid values are:
436     * ``'django.contrib.sessions.backends.file'``
437     * ``'django.contrib.sessions.backends.cache'``
438     * ``'django.contrib.sessions.backends.cached_db'``
439+    * ``'django.contrib.sessions.backends.signed_cookies'``
440 
441 See `configuring the session engine`_ for more details.
442