Skip to content
Browse files

Reverted sessions.

--HG--
branch : 1.0
  • Loading branch information...
1 parent 19e1bdd commit d7fd8371cf076acff62a7b46f2996f72c932f3a7 @moraes committed
Showing with 721 additions and 362 deletions.
  1. +441 −48 tests/sessions_test.py
  2. +47 −44 tipfy/appengine/sessions.py
  3. +233 −270 tipfy/sessions.py
View
489 tests/sessions_test.py
@@ -1,56 +1,449 @@
-import datetime
-import functools
-import time
+from __future__ import with_statement
-from tipfy.sessions import SecureCookieSerializer
+import os
+import unittest
+
+from werkzeug import cached_property
+
+from tipfy.app import App, Request, Response
+from tipfy.handler import RequestHandler
+from tipfy.json import json_b64decode
+from tipfy.local import local
+from tipfy.routing import Rule
+from tipfy.sessions import (SecureCookieSession, SecureCookieStore,
+ SessionMiddleware, SessionStore)
+from tipfy.appengine.sessions import (DatastoreSession, MemcacheSession,
+ SessionModel)
import test_utils
-class SessionsTest(test_utils.BaseTestCase):
- def test_secure_cookie_serializer(self):
- def get_timestamp(*args):
- d = datetime.datetime(*args)
- return int(time.mktime(d.timetuple()))
-
- value = ['a', 'b', 'c']
- result = 'WyJhIiwiYiIsImMiXQ==|1293847200|f7f95c30ba7cc2c1d49677e6b0eaf477504ad548'
-
- serializer = SecureCookieSerializer('secret-key')
- serializer.get_timestamp = functools.partial(get_timestamp, 2011, 1,
- 1, 0, 0, 0)
- # ok
- rv = serializer.serialize('foo', value)
- self.assertEqual(rv, result)
-
- # ok
- rv = serializer.deserialize('foo', result)
- self.assertEqual(rv, value)
-
- # no value
- rv = serializer.deserialize('foo', None)
- self.assertEqual(rv, None)
-
- # not 3 parts
- rv = serializer.deserialize('foo', 'a|b')
- self.assertEqual(rv, None)
-
- # bad signature
- rv = serializer.deserialize('foo', result + 'foo')
- self.assertEqual(rv, None)
-
- # too old
- serializer.get_timestamp = functools.partial(get_timestamp, 2011, 1,
- 3, 0, 0, 0)
- rv = serializer.deserialize('foo', result, max_age=86400)
- self.assertEqual(rv, None)
-
- # not correctly encoded
- serializer2 = SecureCookieSerializer('foo')
- serializer2.encode = lambda x: 'foo'
- result2 = serializer2.serialize('foo', value)
- rv2 = serializer2.deserialize('foo', result2)
- self.assertEqual(rv2, None)
+class BaseHandler(RequestHandler):
+ middleware = [SessionMiddleware()]
+
+
+class TestSessionStoreBase(test_utils.BaseTestCase):
+ def _get_app(self):
+ return App(config={
+ 'tipfy.sessions': {
+ 'secret_key': 'secret',
+ }
+ })
+
+ def test_secure_cookie_store(self):
+ with self._get_app().get_test_context() as request:
+ store = request.session_store
+ self.assertEqual(isinstance(store.secure_cookie_store, SecureCookieStore), True)
+
+ def test_secure_cookie_store_no_secret_key(self):
+ with App().get_test_context() as request:
+ store = request.session_store
+ self.assertRaises(KeyError, getattr, store, 'secure_cookie_store')
+
+ def test_get_cookie_args(self):
+ with self._get_app().get_test_context() as request:
+ store = request.session_store
+
+ self.assertEqual(store.get_cookie_args(), {
+ 'max_age': None,
+ 'domain': None,
+ 'path': '/',
+ 'secure': None,
+ 'httponly': False,
+ })
+
+ self.assertEqual(store.get_cookie_args(max_age=86400, domain='.foo.com'), {
+ 'max_age': 86400,
+ 'domain': '.foo.com',
+ 'path': '/',
+ 'secure': None,
+ 'httponly': False,
+ })
+
+ def test_get_save_session(self):
+ with self._get_app().get_test_context() as request:
+ store = request.session_store
+
+ session = store.get_session()
+ self.assertEqual(isinstance(session, SecureCookieSession), True)
+ self.assertEqual(session, {})
+
+ session['foo'] = 'bar'
+
+ response = Response()
+ store.save(response)
+
+ with self._get_app().get_test_context('/', headers={'Cookie': '\n'.join(response.headers.getlist('Set-Cookie'))}) as request:
+ store = request.session_store
+
+ session = store.get_session()
+ self.assertEqual(isinstance(session, SecureCookieSession), True)
+ self.assertEqual(session, {'foo': 'bar'})
+
+ def test_set_delete_cookie(self):
+ with self._get_app().get_test_context() as request:
+ store = request.session_store
+
+ store.set_cookie('foo', 'bar')
+ store.set_cookie('baz', 'ding')
+
+ response = Response()
+ store.save(response)
+
+ headers = {'Cookie': '\n'.join(response.headers.getlist('Set-Cookie'))}
+ with self._get_app().get_test_context('/', headers=headers) as request:
+ store = request.session_store
+
+ self.assertEqual(request.cookies.get('foo'), 'bar')
+ self.assertEqual(request.cookies.get('baz'), 'ding')
+
+ store.delete_cookie('foo')
+ store.save(response)
+
+ headers = {'Cookie': '\n'.join(response.headers.getlist('Set-Cookie'))}
+ with self._get_app().get_test_context('/', headers=headers) as request:
+ self.assertEqual(request.cookies.get('foo', None), '')
+ self.assertEqual(request.cookies['baz'], 'ding')
+
+ def test_set_cookie_encoded(self):
+ with self._get_app().get_test_context() as request:
+ store = request.session_store
+
+ store.set_cookie('foo', 'bar', format='json')
+ store.set_cookie('baz', 'ding', format='json')
+
+ response = Response()
+ store.save(response)
+
+ headers = {'Cookie': '\n'.join(response.headers.getlist('Set-Cookie'))}
+ with self._get_app().get_test_context('/', headers=headers) as request:
+ store = request.session_store
+
+ self.assertEqual(json_b64decode(request.cookies.get('foo')), 'bar')
+ self.assertEqual(json_b64decode(request.cookies.get('baz')), 'ding')
+
+
+class TestSessionStore(test_utils.BaseTestCase):
+ def setUp(self):
+ SessionStore.default_backends.update({
+ 'datastore': DatastoreSession,
+ 'memcache': MemcacheSession,
+ 'securecookie': SecureCookieSession,
+ })
+ test_utils.BaseTestCase.setUp(self)
+
+ def _get_app(self, *args, **kwargs):
+ app = App(config={
+ 'tipfy.sessions': {
+ 'secret_key': 'secret',
+ },
+ })
+ return app
+
+ def test_set_session(self):
+ class MyHandler(BaseHandler):
+ def get(self):
+ res = self.session.get('key')
+ if not res:
+ res = 'undefined'
+ session = SecureCookieSession()
+ session['key'] = 'a session value'
+ self.session_store.set_session(self.session_store.config['cookie_name'], session)
+
+ return Response(res)
+
+ rules = [Rule('/', name='test', handler=MyHandler)]
+
+ app = self._get_app('/')
+ app.router.add(rules)
+ client = app.get_test_client()
+
+ response = client.get('/')
+ self.assertEqual(response.data, 'undefined')
+
+ response = client.get('/', headers={
+ 'Cookie': '\n'.join(response.headers.getlist('Set-Cookie')),
+ })
+ self.assertEqual(response.data, 'a session value')
+
+ def test_set_session_datastore(self):
+ class MyHandler(BaseHandler):
+ def get(self):
+ session = self.session_store.get_session(backend='datastore')
+ res = session.get('key')
+ if not res:
+ res = 'undefined'
+ session = DatastoreSession(None, 'a_random_session_id')
+ session['key'] = 'a session value'
+ self.session_store.set_session(self.session_store.config['cookie_name'], session, backend='datastore')
+
+ return Response(res)
+
+ rules = [Rule('/', name='test', handler=MyHandler)]
+
+ app = self._get_app('/')
+ app.router.add(rules)
+ client = app.get_test_client()
+
+ response = client.get('/')
+ self.assertEqual(response.data, 'undefined')
+
+ response = client.get('/', headers={
+ 'Cookie': '\n'.join(response.headers.getlist('Set-Cookie')),
+ })
+ self.assertEqual(response.data, 'a session value')
+
+ def test_get_memcache_session(self):
+ class MyHandler(BaseHandler):
+ def get(self):
+ session = self.session_store.get_session(backend='memcache')
+ res = session.get('test')
+ if not res:
+ res = 'undefined'
+ session['test'] = 'a memcache session value'
+
+ return Response(res)
+
+ rules = [Rule('/', name='test', handler=MyHandler)]
+
+ app = self._get_app('/')
+ app.router.add(rules)
+ client = app.get_test_client()
+
+ response = client.get('/')
+ self.assertEqual(response.data, 'undefined')
+
+ response = client.get('/', headers={
+ 'Cookie': '\n'.join(response.headers.getlist('Set-Cookie')),
+ })
+ self.assertEqual(response.data, 'a memcache session value')
+
+ def test_get_datastore_session(self):
+ class MyHandler(BaseHandler):
+ def get(self):
+ session = self.session_store.get_session(backend='datastore')
+ res = session.get('test')
+ if not res:
+ res = 'undefined'
+ session['test'] = 'a datastore session value'
+
+ return Response(res)
+
+ rules = [Rule('/', name='test', handler=MyHandler)]
+
+ app = self._get_app('/')
+ app.router.add(rules)
+ client = app.get_test_client()
+
+ response = client.get('/')
+ self.assertEqual(response.data, 'undefined')
+
+ response = client.get('/', headers={
+ 'Cookie': '\n'.join(response.headers.getlist('Set-Cookie')),
+ })
+ self.assertEqual(response.data, 'a datastore session value')
+
+ def test_set_delete_cookie(self):
+ class MyHandler(BaseHandler):
+ def get(self):
+ res = self.request.cookies.get('test')
+ if not res:
+ res = 'undefined'
+ self.session_store.set_cookie('test', 'a cookie value')
+ else:
+ self.session_store.delete_cookie('test')
+
+ return Response(res)
+
+ rules = [Rule('/', name='test', handler=MyHandler)]
+
+ app = self._get_app('/')
+ app.router.add(rules)
+ client = app.get_test_client()
+
+ response = client.get('/')
+ self.assertEqual(response.data, 'undefined')
+
+ response = client.get('/', headers={
+ 'Cookie': '\n'.join(response.headers.getlist('Set-Cookie')),
+ })
+ self.assertEqual(response.data, 'a cookie value')
+
+ response = client.get('/', headers={
+ 'Cookie': '\n'.join(response.headers.getlist('Set-Cookie')),
+ })
+ self.assertEqual(response.data, 'undefined')
+
+ response = client.get('/', headers={
+ 'Cookie': '\n'.join(response.headers.getlist('Set-Cookie')),
+ })
+ self.assertEqual(response.data, 'a cookie value')
+
+ def test_set_unset_cookie(self):
+ class MyHandler(BaseHandler):
+ def get(self):
+ res = self.request.cookies.get('test')
+ if not res:
+ res = 'undefined'
+ self.session_store.set_cookie('test', 'a cookie value')
+
+ self.session_store.unset_cookie('test')
+ return Response(res)
+
+ rules = [Rule('/', name='test', handler=MyHandler)]
+
+ app = self._get_app('/')
+ app.router.add(rules)
+ client = app.get_test_client()
+
+ response = client.get('/')
+ self.assertEqual(response.data, 'undefined')
+
+ response = client.get('/', headers={
+ 'Cookie': '\n'.join(response.headers.getlist('Set-Cookie')),
+ })
+ self.assertEqual(response.data, 'undefined')
+
+ def test_set_get_secure_cookie(self):
+ class MyHandler(BaseHandler):
+ def get(self):
+ response = Response()
+
+ cookie = self.session_store.get_secure_cookie('test') or {}
+ res = cookie.get('test')
+ if not res:
+ res = 'undefined'
+ self.session_store.set_secure_cookie(response, 'test', {'test': 'a secure cookie value'})
+
+ response.data = res
+ return response
+
+ rules = [Rule('/', name='test', handler=MyHandler)]
+
+ app = self._get_app('/')
+ app.router.add(rules)
+ client = app.get_test_client()
+
+ response = client.get('/')
+ self.assertEqual(response.data, 'undefined')
+
+ response = client.get('/', headers={
+ 'Cookie': '\n'.join(response.headers.getlist('Set-Cookie')),
+ })
+ self.assertEqual(response.data, 'a secure cookie value')
+
+ def test_set_get_flashes(self):
+ class MyHandler(BaseHandler):
+ def get(self):
+ res = [msg for msg, level in self.session.get_flashes()]
+ if not res:
+ res = [{'body': 'undefined'}]
+ self.session.flash({'body': 'a flash value'})
+
+ return Response(res[0]['body'])
+
+ rules = [Rule('/', name='test', handler=MyHandler)]
+
+ app = self._get_app('/')
+ app.router.add(rules)
+ client = app.get_test_client()
+
+ response = client.get('/')
+ self.assertEqual(response.data, 'undefined')
+
+ response = client.get('/', headers={
+ 'Cookie': '\n'.join(response.headers.getlist('Set-Cookie')),
+ })
+ self.assertEqual(response.data, 'a flash value')
+
+ def test_set_get_messages(self):
+ class MyHandler(BaseHandler):
+ @cached_property
+ def messages(self):
+ """A list of status messages to be displayed to the user."""
+ messages = []
+ flashes = self.session.get_flashes(key='_messages')
+ for msg, level in flashes:
+ msg['level'] = level
+ messages.append(msg)
+
+ return messages
+
+ def set_message(self, level, body, title=None, life=None, flash=False):
+ """Adds a status message.
+
+ :param level:
+ Message level. Common values are "success", "error", "info" or
+ "alert".
+ :param body:
+ Message contents.
+ :param title:
+ Optional message title.
+ :param life:
+ Message life time in seconds. User interface can implement
+ a mechanism to make the message disappear after the elapsed time.
+ If not set, the message is permanent.
+ :returns:
+ None.
+ """
+ message = {'title': title, 'body': body, 'life': life}
+ if flash is True:
+ self.session.flash(message, level, '_messages')
+ else:
+ self.messages.append(message)
+
+ def get(self):
+ self.set_message('success', 'a normal message value')
+ self.set_message('success', 'a flash message value', flash=True)
+ return Response('|'.join(msg['body'] for msg in self.messages))
+
+ rules = [Rule('/', name='test', handler=MyHandler)]
+
+ app = self._get_app('/')
+ app.router.add(rules)
+ client = app.get_test_client()
+
+ response = client.get('/')
+ self.assertEqual(response.data, 'a normal message value')
+
+ response = client.get('/', headers={
+ 'Cookie': '\n'.join(response.headers.getlist('Set-Cookie')),
+ })
+ self.assertEqual(response.data, 'a flash message value|a normal message value')
+
+
+class TestSessionModel(test_utils.BaseTestCase):
+ def setUp(self):
+ self.app = App()
+ test_utils.BaseTestCase.setUp(self)
+
+ def test_get_by_sid_without_cache(self):
+ sid = 'test'
+ entity = SessionModel.create(sid, {'foo': 'bar', 'baz': 'ding'})
+ entity.put()
+
+ cached_data = SessionModel.get_cache(sid)
+ self.assertNotEqual(cached_data, None)
+
+ entity.delete_cache()
+ cached_data = SessionModel.get_cache(sid)
+ self.assertEqual(cached_data, None)
+
+ entity = SessionModel.get_by_sid(sid)
+ self.assertNotEqual(entity, None)
+
+ # Now will fetch cache.
+ entity = SessionModel.get_by_sid(sid)
+ self.assertNotEqual(entity, None)
+
+ self.assertEqual('foo' in entity.data, True)
+ self.assertEqual('baz' in entity.data, True)
+ self.assertEqual(entity.data['foo'], 'bar')
+ self.assertEqual(entity.data['baz'], 'ding')
+
+ entity.delete()
+ entity = SessionModel.get_by_sid(sid)
+ self.assertEqual(entity, None)
if __name__ == '__main__':
View
91 tipfy/appengine/sessions.py
@@ -5,7 +5,7 @@
App Engine session backends.
- :copyright: 2010 by tipfy.org.
+ :copyright: 2011 by tipfy.org.
:license: BSD, see LICENSE.txt for more details.
"""
import re
@@ -14,8 +14,8 @@
from google.appengine.api import memcache
from google.appengine.ext import db
-from tipfy.config import DEFAULT_VALUE
-from tipfy.sessions import BaseSessionFactory, SessionDict
+from tipfy.sessions import BaseSession
+
from tipfy.appengine.db import (PickleProperty, get_protobuf_from_entity,
get_entity_from_protobuf)
@@ -101,73 +101,76 @@ def delete(self):
db.delete(self)
-class AppEngineSessionFactory(BaseSessionFactory):
- session_class = SessionDict
- sid = None
+class AppEngineBaseSession(BaseSession):
+ __slots__ = BaseSession.__slots__ + ('sid',)
- def get_session(self, max_age=DEFAULT_VALUE):
- if self.session is None:
- data = self.session_store.get_secure_cookie(self.name,
- max_age=max_age)
- if data is not None:
- self.sid = data.get('_sid')
- if _is_valid_key(self.sid):
- self.session = self._get_by_sid(self.sid)
+ def __init__(self, data=None, sid=None, new=False):
+ BaseSession.__init__(self, data, new)
+ if new:
+ self.sid = self.__class__._get_new_sid()
+ elif sid is None:
+ raise ValueError('A session id is required for existing sessions.')
+ else:
+ self.sid = sid
- if self.session is None:
- self.sid = self._get_new_sid()
- self.session = self.session_class(self, new=True)
+ @classmethod
+ def _get_new_sid(cls):
+ # Force a namespace in the key, to not pollute the namespace in case
+ # global namespaces are in use.
+ return cls.__module__ + '.' + cls.__name__ + '.' + uuid.uuid4().hex
- return self.session
+ @classmethod
+ def get_session(cls, store, name=None, **kwargs):
+ if name:
+ cookie = store.get_secure_cookie(name)
+ if cookie is not None:
+ sid = cookie.get('_sid')
+ if sid and _is_valid_key(sid):
+ return cls._get_by_sid(sid, **kwargs)
- def _get_new_sid(self):
- return uuid.uuid4().hex
+ return cls(new=True)
-class DatastoreSessionFactory(AppEngineSessionFactory):
+class DatastoreSession(AppEngineBaseSession):
+ """A session that stores data serialized in the datastore."""
model_class = SessionModel
- def _get_by_sid(self, sid):
+ @classmethod
+ def _get_by_sid(cls, sid, **kwargs):
"""Returns a session given a session id."""
- entity = self.model_class.get_by_sid(sid)
+ entity = cls.model_class.get_by_sid(sid)
if entity is not None:
- return self.session_class(self, data=entity.data)
+ return cls(entity.data, sid)
- self.sid = self._get_new_sid()
- return self.session_class(self, new=True)
+ return cls(new=True)
- def save_session(self, response):
- if self.session is None or not self.session.modified:
+ def save_session(self, response, store, name, **kwargs):
+ if not self.modified:
return
- self.model_class.create(self.sid, dict(self.session)).put()
- self.session_store.set_secure_cookie(
- response, self.name, {'_sid': self.sid}, **self.session_args)
+ self.model_class.create(self.sid, dict(self)).put()
+ store.set_secure_cookie(response, name, {'_sid': self.sid}, **kwargs)
-class MemcacheSessionFactory(AppEngineSessionFactory):
+class MemcacheSession(AppEngineBaseSession):
"""A session that stores data serialized in memcache."""
- def _get_by_sid(self, sid):
+ @classmethod
+ def _get_by_sid(cls, sid, **kwargs):
"""Returns a session given a session id."""
data = memcache.get(sid)
if data is not None:
- return self.session_class(self, data=data)
+ return cls(data, sid)
- self.sid = self._get_new_sid()
- return self.session_class(self, new=True)
+ return cls(new=True)
- def save_session(self, response):
- if self.session is None or not self.session.modified:
+ def save_session(self, response, store, name, **kwargs):
+ if not self.modified:
return
- memcache.set(self.sid, dict(self.session))
- self.session_store.set_secure_cookie(
- response, self.name, {'_sid': self.sid}, **self.session_args)
+ memcache.set(self.sid, dict(self))
+ store.set_secure_cookie(response, name, {'_sid': self.sid}, **kwargs)
def _is_valid_key(key):
"""Check if a session key has the correct format."""
- if not key:
- return False
-
return _UUID_RE.match(key.split('.')[-1]) is not None
View
503 tipfy/sessions.py
@@ -16,9 +16,9 @@
import time
from tipfy import APPENGINE, DEFAULT_VALUE, REQUIRED_VALUE
-from tipfy.json import json_b64encode, json_b64decode
+from tipfy.utils import json_b64encode, json_b64decode
-from werkzeug.utils import cached_property
+from werkzeug import cached_property
from werkzeug.contrib.sessions import ModificationTrackingDict
#: Default configuration values for this module. Keys are:
@@ -75,13 +75,68 @@
}
-class SecureCookieSerializer(object):
- """Serializes and deserializes secure cookie values.
+class BaseSession(ModificationTrackingDict):
+ __slots__ = ModificationTrackingDict.__slots__ + ('new',)
+
+ def __init__(self, data=None, new=False):
+ ModificationTrackingDict.__init__(self, data or ())
+ self.new = new
+
+ def get_flashes(self, key='_flash'):
+ """Returns a flash message. Flash messages are deleted when first read.
+
+ :param key:
+ Name of the flash key stored in the session. Default is '_flash'.
+ :returns:
+ The data stored in the flash, or an empty list.
+ """
+ if key not in self:
+ # Avoid popping if the key doesn't exist to not modify the session.
+ return []
+
+ return self.pop(key, [])
+
+ def add_flash(self, value, level=None, key='_flash'):
+ """Adds a flash message. Flash messages are deleted when first read.
+
+ :param value:
+ Value to be saved in the flash message.
+ :param level:
+ An optional level to set with the message. Default is `None`.
+ :param key:
+ Name of the flash key stored in the session. Default is '_flash'.
+ """
+ self.setdefault(key, []).append((value, level))
+
+ #: Alias, Flask-like interface.
+ flash = add_flash
+
+
+class SecureCookieSession(BaseSession):
+ """A session that stores data serialized in a signed cookie."""
+ @classmethod
+ def get_session(cls, store, name=None, **kwargs):
+ if name:
+ data = store.get_secure_cookie(name)
+ if data is not None:
+ return cls(data)
+
+ return cls(new=True)
+
+ def save_session(self, response, store, name, **kwargs):
+ if not self.modified:
+ return
+
+ store.set_secure_cookie(response, name, dict(self), **kwargs)
+
+
+class SecureCookieStore(object):
+ """Encapsulates getting and setting secure cookies.
Extracted from `Tornado`_ and modified.
"""
def __init__(self, secret_key):
- """Initiliazes the serializer/deserializer.
+ """Initilizes this secure cookie store.
:param secret_key:
A long, random sequence of bytes to be used as the HMAC secret
@@ -89,34 +144,19 @@ def __init__(self, secret_key):
"""
self.secret_key = secret_key
- def serialize(self, name, value):
- """Serializes a signed cookie value.
+ def get_cookie(self, request, name, max_age=None):
+ """Returns the given signed cookie if it validates, or None.
+ :param request:
+ A :class:`tipfy.app.Request` object.
:param name:
Cookie name.
- :param value:
- Cookie value to be serialized.
- :returns:
- A serialized value ready to be stored in a cookie.
- """
- timestamp = str(self.get_timestamp())
- value = self.encode(value)
- signature = self._get_signature(name, value, timestamp)
- return '|'.join([value, timestamp, signature])
-
- def deserialize(self, name, value, max_age=None):
- """Deserializes a signed cookie value.
-
- :param name:
- Cookie name.
- :param value:
- A cookie value to be deserialized.
:param max_age:
Maximum age in seconds for a valid cookie. If the cookie is older
than this, returns None.
- :returns:
- The deserialized secure cookie, or None if it is not valid.
"""
+ value = request.cookies.get(name)
+
if not value:
return
@@ -130,33 +170,55 @@ def deserialize(self, name, value, max_age=None):
logging.warning('Invalid cookie signature %r', value)
return
- if max_age is not None:
- if int(parts[1]) < self.get_timestamp() - max_age:
- logging.warning('Expired cookie %r', value)
- return
+ if max_age is not None and (int(parts[1]) < time.time() - max_age):
+ logging.warning('Expired cookie %r', value)
+ return
try:
- return self.decode(parts[0])
- except Exception, e:
+ return json_b64decode(parts[0])
+ except:
logging.warning('Cookie value failed to be decoded: %r', parts[0])
+ return
- def encode(self, value):
- return json_b64encode(value)
+ def set_cookie(self, response, name, value, **kwargs):
+ """Signs and timestamps a cookie so it cannot be forged.
- def decode(self, value):
- return json_b64decode(value)
+ To read a cookie set with this method, use get_cookie().
+
+ :param response:
+ A :class:`tipfy.app.Response` instance.
+ :param name:
+ Cookie name.
+ :param value:
+ Cookie value.
+ :param kwargs:
+ Options to save the cookie. See :meth:`SessionStore.get_session`.
+ """
+ response.set_cookie(name, self.get_signed_value(name, value), **kwargs)
- def get_timestamp(self):
- return int(time.time())
+ def get_signed_value(self, name, value):
+ """Returns a signed value for a cookie.
+
+ :param name:
+ Cookie name.
+ :param value:
+ Cookie value.
+ :returns:
+ An signed value using HMAC.
+ """
+ timestamp = str(int(time.time()))
+ value = json_b64encode(value)
+ signature = self._get_signature(name, value, timestamp)
+ return '|'.join([value, timestamp, signature])
def _get_signature(self, *parts):
- """Generates an HMAC signature."""
- signature = hmac.new(self.secret_key, digestmod=hashlib.sha1)
- signature.update('|'.join(parts))
- return signature.hexdigest()
+ """Generated an HMAC signatures."""
+ hash = hmac.new(self.secret_key, digestmod=hashlib.sha1)
+ hash.update('|'.join(parts))
+ return hash.hexdigest()
def _check_signature(self, a, b):
- """Checks if an HMAC signature is valid."""
+ """Checks if an HMAC signatures is valid."""
if len(a) != len(b):
return False
@@ -167,125 +229,106 @@ def _check_signature(self, a, b):
return result == 0
-class SessionDict(ModificationTrackingDict):
- __slots__ = ModificationTrackingDict.__slots__ + ('new',)
-
- def __init__(self, data=None, new=False):
- ModificationTrackingDict.__init__(self, data or ())
- self.new = new
-
- def get_flashes(self, key='_flash'):
- """Returns a flash message. Flash messages are deleted when first read.
-
- :param key:
- Name of the flash key stored in the session. Default is '_flash'.
- :returns:
- The data stored in the flash, or an empty list.
- """
- if key not in self:
- # Avoid popping if the key doesn't exist to not modify the session.
- return []
-
- return self.pop(key, [])
-
- def add_flash(self, value, level=None, key='_flash'):
- """Adds a flash message. Flash messages are deleted when first read.
-
- :param value:
- Value to be saved in the flash message.
- :param level:
- An optional level to set with the message. Default is `None`.
- :param key:
- Name of the flash key stored in the session. Default is '_flash'.
- """
- self.setdefault(key, []).append((value, level))
-
- #: Alias, Flask-like interface.
- flash = add_flash
-
-
-class BaseSessionFactory(object):
- def __init__(self, name, session_store):
- self.name = name
- self.session_store = session_store
- self.session_args = session_store.config['cookie_args'].copy()
- self.session = None
-
-
-class CookieSessionFactory(BaseSessionFactory):
- """A session that stores data serialized in a ordinary cookie."""
- def save_session(self, response):
- if self.session is None:
- path = self.session_args.get('path', '/')
- domain = self.session_args.get('domain', None)
- response.delete_cookie(self.name, path=path, domain=domain)
- else:
- response.set_cookie(self.name, self.session, **self.session_args)
-
-
-class SecureCookieSessionFactory(BaseSessionFactory):
- """A session that stores data serialized in a signed cookie."""
- session_class = SessionDict
-
- def get_session(self, max_age=DEFAULT_VALUE):
- if self.session is None:
- data = self.session_store.get_secure_cookie(self.name,
- max_age=max_age)
- new = data is None
- self.session = self.session_class(self, data=data, new=new)
-
- return self.session
-
- def save_session(self, response):
- if self.session is None or not self.session.modified:
- return
-
- self.session_store.save_secure_cookie(
- response, self.name, dict(self.session), **self.session_args)
-
-
class SessionStore(object):
- def __init__(self, request):
+ #: A dictionary with the default supported backends.
+ default_backends = {
+ 'securecookie': SecureCookieSession,
+ }
+
+ def __init__(self, request, backends=None):
self.request = request
# Base configuration.
self.config = request.app.config[__name__]
+ # A dictionary of support backend classes.
+ self.backends = backends or self.default_backends
+ # The default backend to use when none is provided.
+ self.default_backend = self.config['default_backend']
# Tracked sessions.
- self.sessions = {}
- # Serializer and deserializer for signed cookies.
- self.cookie_serializer = SecureCookieSerializer(
- self.config['secret_key'])
-
- # Backend based sessions --------------------------------------------------
+ self._sessions = {}
+ # Tracked cookies.
+ self._cookies = {}
- def _get_session_container(self, name, factory):
- if name not in self.sessions:
- self.sessions[name] = factory(name, self)
+ @cached_property
+ def secure_cookie_store(self):
+ """Factory for secure cookies.
- return self.sessions[name]
+ :returns:
+ A :class:`SecureCookieStore` instance.
+ """
+ return SecureCookieStore(self.config['secret_key'])
- def get_session(self, name=None, max_age=DEFAULT_VALUE,
- factory=SecureCookieSessionFactory):
- """Returns a session for a given name. If the session doesn't exist, a
+ def get_session(self, key=None, backend=None, **kwargs):
+ """Returns a session for a given key. If the session doesn't exist, a
new session is returned.
- :param name:
+ :param key:
Cookie name. If not provided, uses the ``cookie_name``
value configured for this module.
+ :param backend:
+ Name of the session backend to be used. If not set, uses the
+ default backend.
+ :param kwargs:
+ Options to set the session cookie. Keys are the same that can be
+ passed to ``Response.set_cookie``, and override the ``cookie_args``
+ values configured for this module. If not set, use the configured
+ values.
:returns:
A dictionary-like session object.
"""
- name = name or self.config['cookie_name']
+ key = key or self.config['cookie_name']
+ backend = backend or self.default_backend
+ sessions = self._sessions.setdefault(backend, {})
- if max_age is DEFAULT_VALUE:
- max_age = self.config['session_max_age']
+ if key not in sessions:
+ kwargs = self.get_cookie_args(**kwargs)
+ value = self.backends[backend].get_session(self, key, **kwargs)
+ sessions[key] = (value, kwargs)
+
+ return sessions[key][0]
+
+ def set_session(self, key, value, backend=None, **kwargs):
+ """Sets a session value. If a session with the same key exists, it
+ will be overriden with the new value.
- container = self._get_session_container(name, factory)
- return container.get_session(max_age=max_age)
+ :param key:
+ Cookie name. See :meth:`get_session`.
+ :param value:
+ A dictionary of session values.
+ :param backend:
+ Name of the session backend. See :meth:`get_session`.
+ :param kwargs:
+ Options to save the cookie. See :meth:`get_session`.
+ """
+ assert isinstance(value, dict), 'Session value must be a dict.'
+ backend = backend or self.default_backend
+ sessions = self._sessions.setdefault(backend, {})
+ session = self.backends[backend].get_session(self, **kwargs)
+ session.update(value)
+ kwargs = self.get_cookie_args(**kwargs)
+ sessions[key] = (session, kwargs)
+
+ def update_session_args(self, key, backend=None, **kwargs):
+ """Updates the cookie options for a session.
- # Signed cookies ----------------------------------------------------------
+ :param key:
+ Cookie name. See :meth:`get_session`.
+ :param backend:
+ Name of the session backend. See :meth:`get_session`.
+ :param kwargs:
+ Options to save the cookie. See :meth:`get_session`.
+ :returns:
+ True if the session was updated, False otherwise.
+ """
+ backend = backend or self.default_backend
+ sessions = self._sessions.setdefault(backend, {})
+ if key in sessions:
+ sessions[key][1].update(kwargs)
+ return True
+
+ return False
def get_secure_cookie(self, name, max_age=DEFAULT_VALUE):
- """Returns a deserialized secure cookie value.
+ """Returns a secure cookie from the request.
:param name:
Cookie name.
@@ -298,14 +341,14 @@ def get_secure_cookie(self, name, max_age=DEFAULT_VALUE):
if max_age is DEFAULT_VALUE:
max_age = self.config['session_max_age']
- value = self.request.cookies.get(name)
- if value:
- return self.cookie_serializer.deserialize(name, value,
- max_age=max_age)
+ return self.secure_cookie_store.get_cookie(self.request, name,
+ max_age=max_age)
- def set_secure_cookie(self, name, value, **kwargs):
- """Sets a secure cookie to be saved.
+ def set_secure_cookie(self, response, name, value, **kwargs):
+ """Sets a secure cookie in the response.
+ :param response:
+ A :class:`tipfy.app.Response` object.
:param name:
Cookie name.
:param value:
@@ -313,148 +356,65 @@ def set_secure_cookie(self, name, value, **kwargs):
:param kwargs:
Options to save the cookie. See :meth:`get_session`.
"""
- container = self._get_session_container(name,
- SecureCookieSessionFactory)
- container.session = value
- container.session_args.update(kwargs)
-
- # Ordinary cookies --------------------------------------------------------
-
- def get_cookie(self, name, decoder=json_b64decode):
- """Returns a cookie from the request, decoding it.
-
- :param name:
- Cookie name.
- :param decoder:
- An decoder for the cookie value. Default is
- func:`tipfy.json.json_b64decode`.
- :returns:
- A decoded cookie value, or None if a cookie with this name is not
- set or decoding failed.
- """
- value = self.request.cookies.get(name)
- if value is not None and decoder:
- try:
- value = decoder(value)
- except Exception, e:
- return
-
- return value
+ assert isinstance(value, dict), 'Secure cookie value must be a dict.'
+ kwargs = self.get_cookie_args(**kwargs)
+ self.secure_cookie_store.set_cookie(response, name, value, **kwargs)
- def set_cookie(self, name, value, format=None, encoder=json_b64encode,
- **kwargs):
- """Registers a cookie to be saved or deleted.
+ def set_cookie(self, key, value, format=None, **kwargs):
+ """Registers a cookie or secure cookie to be saved or deleted.
- :param name:
+ :param key:
Cookie name.
:param value:
Cookie value.
:param format:
If set to 'json', the value is serialized to JSON and encoded
to base64.
-
- ..warning: Deprecated. Pass an encoder instead.
- :param encoder:
- An encoder for the cookie value. Default is
- func:`tipfy.json.json_b64encode`.
:param kwargs:
Options to save the cookie. See :meth:`get_session`.
"""
- if format is not None:
- from warnings import warn
- warn(DeprecationWarning("SessionStore.set_cookie(): the "
- "'format' argument is deprecated. Use 'encoder' instead to "
- "pass an encoder callable."))
-
- if format == 'json':
- value = json_b64encode(value)
- elif encoder:
- value = encoder(value)
-
- container = self._get_session_container(name, CookieSessionFactory)
- container.session = value
- container.session_args.update(kwargs)
-
- def delete_cookie(self, name, **kwargs):
- """Registers a cookie or secure cookie to be deleted.
+ if format == 'json':
+ value = json_b64encode(value)
- :param name:
- Cookie name.
- :param kwargs:
- Options to delete the cookie. See :meth:`get_session`.
- """
- self.set_cookie(name, None, **kwargs)
+ self._cookies[key] = (value, self.get_cookie_args(**kwargs))
- def unset_cookie(self, name):
+ def unset_cookie(self, key):
"""Unsets a cookie previously set. This won't delete the cookie, it
just won't be saved.
- :param name:
+ :param key:
Cookie name.
"""
- self.sessions.pop(name, None)
-
- # Saving to a response object ---------------------------------------------
-
- def save_sessions(self, response):
- """Saves all cookies and sessions to a response object.
-
- :param response:
- A ``tipfy.app.Response`` object.
- """
- for session in self.sessions.values():
- session.save_session(response)
- # Old name
- save = save_sessions
+ self._cookies.pop(key, None)
- def save_secure_cookie(self, response, name, value, **kwargs):
- value = self.cookie_serializer.serialize(name, value)
- response.set_cookie(name, value, **kwargs)
-
- # Deprecated methods ------------------------------------------------------
-
- def set_session(self, name, value, backend=None, **kwargs):
- """Sets a session value. If a session with the same name exists, it
- will be overriden with the new value.
+ def delete_cookie(self, key, **kwargs):
+ """Registers a cookie or secure cookie to be deleted.
- :param name:
- Cookie name. See :meth:`get_session`.
- :param value:
- A dictionary of session values.
- :param backend:
- Name of the session backend. See :meth:`get_session`.
+ :param key:
+ Cookie name.
:param kwargs:
- Options to save the cookie. See :meth:`get_session`.
+ Options to delete the cookie. See :meth:`get_session`.
"""
- from warnings import warn
- warn(DeprecationWarning("SessionStore.set_session(): this "
- "method is deprecated. Cookie arguments can be set directly in "
- "a session."))
-
- self.set_secure_cookie(name, value, **kwargs)
+ self._cookies[key] = (None, self.get_cookie_args(**kwargs))
- def update_session_args(self, name, backend=None, **kwargs):
- """Updates the cookie options for a session.
+ def save(self, response):
+ """Saves all cookies and sessions to a response object.
- :param name:
- Cookie name. See :meth:`get_session`.
- :param backend:
- Name of the session backend. See :meth:`get_session`.
- :param kwargs:
- Options to save the cookie. See :meth:`get_session`.
- :returns:
- True if the session was updated, False otherwise.
+ :param response:
+ A ``tipfy.Response`` object.
"""
- from warnings import warn
- warn(DeprecationWarning("SessionStore.update_session_args(): this "
- "method is deprecated. Cookie arguments can be set directly in "
- "a session."))
-
- if name in self.sessions:
- self.sessions[name].session_args.update(kwargs)
- return True
-
- return False
+ if self._cookies:
+ for key, (value, kwargs) in self._cookies.iteritems():
+ if value is None:
+ response.delete_cookie(key, path=kwargs.get('path', '/'),
+ domain=kwargs.get('domain', None))
+ else:
+ response.set_cookie(key, value, **kwargs)
+
+ if self._sessions:
+ for sessions in self._sessions.values():
+ for key, (value, kwargs) in sessions.iteritems():
+ value.save_session(response, self, key, **kwargs)
def get_cookie_args(self, **kwargs):
"""Returns a copy of the default cookie configuration updated with the
@@ -465,11 +425,6 @@ def get_cookie_args(self, **kwargs):
:returns:
A dictionary with arguments for the session cookie.
"""
- from warnings import warn
- warn(DeprecationWarning("SessionStore.get_cookie_args(): this "
- "method is deprecated. Cookie arguments can be set directly in "
- "a session."))
-
_kwargs = self.config['cookie_args'].copy()
_kwargs.update(kwargs)
return _kwargs
@@ -489,3 +444,11 @@ def after_dispatch(self, handler, response):
"""
handler.session_store.save(response)
return response
+
+
+if APPENGINE:
+ from tipfy.appengine.sessions import DatastoreSession, MemcacheSession
+ SessionStore.default_backends.update({
+ 'datastore': DatastoreSession,
+ 'memcache': MemcacheSession,
+ })

0 comments on commit d7fd837

Please sign in to comment.
Something went wrong with that request. Please try again.