Commits

Ben Bangert committed 22e1b1a Merge

Merge

Comments (0)

Files changed (6)

 container_file
 container_dbm
 tests/*/container_*
+glob:*.komodoproject

beaker/docs/sessions.rst

 Using
 =====
 
-The session object provided by Beaker's 
+The session object provided by Beaker's
 :class:`~beaker.middleware.SessionMiddleware` implements a dict-style interface
 with a few additional object methods. Once the SessionMiddleware is in place,
 a session object will be made available as ``beaker.session`` in the WSGI
 This is not usually the case however, as a session generally should not be
 saved should something catastrophic happen during a request.
 
-.. note::
+**Order Matters**: When using the Beaker middleware, you **must call save before
+the headers are sent to the client**. Since Beaker's middleware watches for when
+the ``start_response`` function is called to know that it should add its cookie
+header, the session must be saved before its called.
 
-    When using the Beaker middleware, you **must call save before the headers
-    are sent to the client**. Since Beaker's middleware watches for when the
-    ``start_response`` function is called to know that it should add its
-    cookie header, the session must be saved before its called.
+Keep in mind that Response objects in popular frameworks (WebOb, Werkzeug,
+etc.) call start_response immediately, so if you are using one of those
+objects to handle your Response, you must call .save() before the Response
+object is called::
+
+    # this would apply to WebOb and possibly others too
+    from werkzeug.wrappers import Response
+
+    # this will work
+    def sessions_work(environ, start_response):
+        environ['beaker.session']['count'] += 1
+        resp = Response('hello')
+        environ['beaker.session'].save()
+        return resp(environ, start_response)
+
+    # this will not work
+    def sessions_broken(environ, start_response):
+        environ['beaker.session']['count'] += 1
+        resp = Response('hello')
+        retval = resp(environ, start_response)
+        environ['beaker.session'].save()
+        return retval
+
 
 
 Auto-save
 Cookie Domain and Path
 ======================
 
-In addition to setting a default cookie domain with the 
+In addition to setting a default cookie domain with the
 :ref:`cookie domain setting <cookie_domain_config>`, the cookie's domain and
 path can be set dynamically for a session with the domain and path properties.
 

beaker/ext/memcached.py

+from __future__ import with_statement
 from beaker.container import NamespaceManager, Container
 from beaker.exceptions import InvalidCacheBackendError, MissingCacheParameter
 from beaker.synchronization import file_synchronizer, null_synchronizer
         else:
             return object.__new__(MemcachedNamespaceManager)
 
-    def __init__(self, namespace, url, 
-                        memcache_module='auto', 
-                        data_dir=None, lock_dir=None, 
+    def __init__(self, namespace, url,
+                        memcache_module='auto',
+                        data_dir=None, lock_dir=None,
                         **kw):
         NamespaceManager.__init__(self, namespace)
 
         _memcache_module = _client_libs[memcache_module]
 
         if not url:
-            raise MissingCacheParameter("url is required") 
+            raise MissingCacheParameter("url is required")
 
         if lock_dir:
             self.lock_dir = lock_dir
             verify_directory(self.lock_dir)
 
         self.mc = MemcachedNamespaceManager.clients.get(
-                        (memcache_module, url), 
-                        _memcache_module.Client, 
+                        (memcache_module, url),
+                        _memcache_module.Client,
                         url.split(';'))
 
     def get_creation_lock(self, key):
         return file_synchronizer(
-            identifier="memcachedcontainer/funclock/%s" % 
+            identifier="memcachedcontainer/funclock/%s" %
                     self.namespace,lock_dir = self.lock_dir)
 
     def _format_key(self, key):

beaker/session.py

     ``key``
         The name the cookie should be set to.
     ``timeout``
-        How long session data is considered valid. This is used 
+        How long session data is considered valid. This is used
         regardless of the cookie being present or not to determine
         whether session data is still valid.
     ``cookie_domain``
         Domain to use for the cookie.
     ``secure``
         Whether or not the cookie should only be sent over SSL.
+    ``httponly``
+        Whether or not the cookie should only be accessible by the browser
+        not by JavaScript.
     """
     def __init__(self, request, id=None, invalidate_corrupt=False,
                  use_cookies=True, type=None, data_dir=None,
                  key='beaker.session.id', timeout=None, cookie_expires=True,
                  cookie_domain=None, secret=None, secure=False,
-                 namespace_class=None, **namespace_args):
+                 namespace_class=None, httponly=False, **namespace_args):
         if not type:
             if data_dir:
                 self.type = 'file'
         self.was_invalidated = False
         self.secret = secret
         self.secure = secure
+        self.httponly = httponly
         self.id = id
         self.accessed_dict = {}
 
                 else:
                     raise
 
-    def _create_id(self):
-        id_str = "%f%s%f%s" % (
-                    time.time(), 
-                    id({}), 
-                    random.random(),
-                    getpid()
-                ) 
-        if util.py3k:
-            self.id = md5(
-                            md5(
-                                id_str.encode('ascii')
-                            ).hexdigest().encode('ascii')
-                        ).hexdigest()
-        else:
-            self.id = md5(md5(id_str).hexdigest()).hexdigest()
+    def _set_cookie_values(self, expires=None):
+        self.cookie[self.key] = self.id
+        if self._domain:
+            self.cookie[self.key]['domain'] = self._domain
+        if self.secure:
+            self.cookie[self.key]['secure'] = True
+        self._set_cookie_http_only()
+        self.cookie[self.key]['path'] = self._path
 
-        self.is_new = True
-        self.last_accessed = None
-        if self.use_cookies:
-            self.cookie[self.key] = self.id
-            if self._domain:
-                self.cookie[self.key]['domain'] = self._domain
-            if self.secure:
-                self.cookie[self.key]['secure'] = True
-            self.cookie[self.key]['path'] = self._path
+        self._set_cookie_expires(expires)
+
+    def _set_cookie_expires(self, expires):
+        if expires is None:
             if self.cookie_expires is not True:
                 if self.cookie_expires is False:
                     expires = datetime.fromtimestamp( 0x7FFFFFFF )
                 else:
                     raise ValueError("Invalid argument for cookie_expires: %s"
                                      % repr(self.cookie_expires))
-                self.cookie[self.key]['expires'] = \
-                    expires.strftime("%a, %d-%b-%Y %H:%M:%S GMT" )
-            self.request['cookie_out'] = self.cookie[self.key].output(header='')
-            self.request['set_cookie'] = False
+            else:
+                expires = None
+        if expires is not None:
+            self.cookie[self.key]['expires'] = \
+                expires.strftime("%a, %d-%b-%Y %H:%M:%S GMT" )
+        return expires
+
+    def _update_cookie_out(self, set_cookie=True):
+        self.request['cookie_out'] = self.cookie[self.key].output(header='')
+        self.request['set_cookie'] = set_cookie
+
+    def _set_cookie_http_only(self):
+        try:
+            if self.httponly:
+                self.cookie[self.key]['httponly'] = True
+        except Cookie.CookieError, e:
+            if 'Invalid Attribute httponly' not in str(e):
+                raise
+            util.warn('Python 2.6+ is required to use httponly')
+
+    def _create_id(self, set_new=True):
+        id_str = "%f%s%f%s" % (
+                    time.time(),
+                    id({}),
+                    random.random(),
+                    getpid()
+                )
+        if util.py3k:
+            self.id = md5(
+                            md5(
+                                id_str.encode('ascii')
+                            ).hexdigest().encode('ascii')
+                        ).hexdigest()
+        else:
+            self.id = md5(md5(id_str).hexdigest()).hexdigest()
+
+        if set_new:
+            self.is_new = True
+            self.last_accessed = None
+        if self.use_cookies:
+            self._set_cookie_values()
+            sc = set_new == False
+            self._update_cookie_out(set_cookie=sc)
 
     def created(self):
         return self['_creation_time']
     def _set_domain(self, domain):
         self['_domain'] = domain
         self.cookie[self.key]['domain'] = domain
-        self.request['cookie_out'] = self.cookie[self.key].output(header='')
-        self.request['set_cookie'] = True
+        self._update_cookie_out()
 
     def _get_domain(self):
         return self._domain
     def _set_path(self, path):
         self['_path'] = path
         self.cookie[self.key]['path'] = path
-        self.request['cookie_out'] = self.cookie[self.key].output(header='')
-        self.request['set_cookie'] = True
+        self._update_cookie_out()
 
     def _get_path(self):
         return self._path
 
     def _delete_cookie(self):
         self.request['set_cookie'] = True
-        self.cookie[self.key] = self.id
-        if self._domain:
-            self.cookie[self.key]['domain'] = self._domain
-        if self.secure:
-            self.cookie[self.key]['secure'] = True
-        self.cookie[self.key]['path'] = '/'
         expires = datetime.utcnow().replace(year=2003)
-        self.cookie[self.key]['expires'] = \
-            expires.strftime("%a, %d-%b-%Y %H:%M:%S GMT" )
-        self.request['cookie_out'] = self.cookie[self.key].output(header='')
-        self.request['set_cookie'] = True
+        self._set_cookie_values(expires)
+        self._update_cookie_out()
 
     def delete(self):
         """Deletes the session from the persistent storage, and sends
     def load(self):
         "Loads the data from this session from persistent storage"
         self.namespace = self.namespace_class(self.id,
-            data_dir=self.data_dir, 
+            data_dir=self.data_dir,
             digest_filenames=False,
             **self.namespace_args)
         now = time.time()
         if accessed_only and self.is_new:
             return None
 
-        if not hasattr(self, 'namespace'):
+        # this session might not have a namespace yet or the session id
+        # might have been regenerated
+        if not hasattr(self, 'namespace') or self.namespace.namespace != self.id:
             self.namespace = self.namespace_class(
-                                    self.id, 
+                                    self.id,
                                     data_dir=self.data_dir,
-                                    digest_filenames=False, 
+                                    digest_filenames=False,
                                     **self.namespace_args)
 
         self.namespace.acquire_write_lock(replace=True)
         self.clear()
         self.update(self.accessed_dict)
 
+    def regenerate_id(self):
+        """
+            creates a new session id, retains all session data
+
+            Its a good security practice to regnerate the id after a client
+            elevates priviliges.
+
+        """
+        self._create_id(set_new=False)
+
     # TODO: I think both these methods should be removed.  They're from
     # the original mod_python code i was ripping off but they really
     # have no use here.
     ``key``
         The name the cookie should be set to.
     ``timeout``
-        How long session data is considered valid. This is used 
+        How long session data is considered valid. This is used
         regardless of the cookie being present or not to determine
         whether session data is still valid.
     ``encrypt_key``
         Domain to use for the cookie.
     ``secure``
         Whether or not the cookie should only be sent over SSL.
+    ``httponly``
+        Whether or not the cookie should only be accessible by the browser
+        not by JavaScript.
 
     """
     def __init__(self, request, key='beaker.session.id', timeout=None,
                  cookie_expires=True, cookie_domain=None, encrypt_key=None,
-                 validate_key=None, secure=False, **kwargs):
+                 validate_key=None, secure=False, httponly=False, **kwargs):
 
         if not crypto.has_aes and encrypt_key:
             raise InvalidCryptoBackendError("No AES library is installed, can't generate "
         self.validate_key = validate_key
         self.request['set_cookie'] = False
         self.secure = secure
+        self.httponly = httponly
         self._domain = cookie_domain
         self._path = '/'
 
             self['_id'] = self._make_id()
         self['_accessed_time'] = time.time()
 
-        if self.cookie_expires is not True:
-            if self.cookie_expires is False:
-                expires = datetime.fromtimestamp( 0x7FFFFFFF )
-            elif isinstance(self.cookie_expires, timedelta):
-                expires = datetime.utcnow() + self.cookie_expires
-            elif isinstance(self.cookie_expires, datetime):
-                expires = self.cookie_expires
-            else:
-                raise ValueError("Invalid argument for cookie_expires: %s"
-                                 % repr(self.cookie_expires))
-            self['_expires'] = expires
-        elif '_expires' in self:
+        if '_expires' in self:
             expires = self['_expires']
         else:
             expires = None
+        expires = self._set_cookie_expires(expires)
+        if expires is not None:
+            self['_expires'] = expires
 
         val = self._encrypt_data()
         if len(val) > 4064:
             self.cookie[self.key]['domain'] = self._domain
         if self.secure:
             self.cookie[self.key]['secure'] = True
+        self._set_cookie_http_only()
 
         self.cookie[self.key]['path'] = self.get('_path', '/')
 
-        if expires:
-            self.cookie[self.key]['expires'] = \
-                expires.strftime("%a, %d-%b-%Y %H:%M:%S GMT" )
         self.request['cookie_out'] = self.cookie[self.key].output(header='')
         self.request['set_cookie'] = True
 
 About
 =====
 
-Beaker is a web session and general caching library that includes WSGI 
+Beaker is a web session and general caching library that includes WSGI
 middleware for use in web applications.
 
 As a general caching library, Beaker can handle storing for various times
-any Python object that can be pickled with optional back-ends on a 
+any Python object that can be pickled with optional back-ends on a
 fine-grained basis.
 
 Beaker was built largely on the code from MyghtyUtils, then refactored and
 extended with database support.
 
 Beaker includes Cache and Session WSGI middleware to ease integration with
-WSGI capable frameworks, and is automatically used by `Pylons 
+WSGI capable frameworks, and is automatically used by `Pylons
 <http://pylonshq.com/>`_.
 
 
 ========
 
 * Fast, robust performance
-* Multiple reader/single writer lock system to avoid duplicate simultaneous 
+* Multiple reader/single writer lock system to avoid duplicate simultaneous
   cache creation
 * Cache back-ends include dbm, file, memory, memcached, and database (Using
   SQLAlchemy for multiple-db vendor support)
 * Signed cookie's to prevent session hijacking/spoofing
-* Cookie-only sessions to remove the need for a db or file backend (ideal 
+* Cookie-only sessions to remove the need for a db or file backend (ideal
   for clustered systems)
 * Extensible Container object to support new back-ends
-* Cache's can be divided into namespaces (to represent templates, objects, 
+* Cache's can be divided into namespaces (to represent templates, objects,
   etc.) then keyed for different copies
 * Create functions for automatic call-backs to create new cache copies after
   expiration

tests/test_session.py

 # -*- coding: utf-8 -*-
+import sys
 import time
+import warnings
 
 from beaker.session import Session
 from beaker import util
     assert u'Deutchland' not in session
 
 
+def test_regenerate_id():
+    """Test :meth:`Session.regenerate_id`"""
+    # new session & save
+    session = get_session()
+    orig_id = session.id
+    session[u'foo'] = u'bar'
+    session.save()
+
+    # load session
+    session = get_session(id=session.id)
+    # data should still be there
+    assert session[u'foo'] == u'bar'
+
+    # regenerate the id
+    session.regenerate_id()
+
+    assert session.id != orig_id
+
+    # data is still there
+    assert session[u'foo'] == u'bar'
+
+    # should be the new id
+    assert 'beaker.session.id=%s' % session.id in session.request['cookie_out']
+
+    # get a new session before calling save
+    bunk_sess = get_session(id=session.id)
+    assert u'foo' not in bunk_sess
+
+    # save it
+    session.save()
+
+    # make sure we get the data back
+    session = get_session(id=session.id)
+    assert session[u'foo'] == u'bar'
+
+
 def test_timeout():
     """Test if the session times out properly"""
     session = get_session(timeout=2)
     assert 'beaker.session.id=%s' % session.id in session.request['cookie_out']
     assert 'expires=' in session.request['cookie_out']
 
+    # test for secure
+    session = get_session(use_cookies=True, secure=True)
+    assert 'secure' in session.request['cookie_out']
+
+    # test for httponly
+    class ShowWarning(object):
+        def __init__(self):
+            self.msg = None
+        def __call__(self, message, category, filename, lineno, file=None, line=None):
+            self.msg = str(message)
+    orig_sw = warnings.showwarning
+    sw = ShowWarning()
+    warnings.showwarning = sw
+    session = get_session(use_cookies=True, httponly=True)
+    if sys.version_info < (2, 6):
+        assert sw.msg == 'Python 2.6+ is required to use httponly'
+    else:
+        assert 'httponly' in session.request['cookie_out']
+    warnings.showwarning = orig_sw
 
 def test_cookies_disabled():
     """
 
 
 def test_file_based_replace_optimization():
-    """Test the file-based backend with session, 
+    """Test the file-based backend with session,
     which includes the 'replace' optimization.
 
     """
 
-    session = get_session(use_cookies=False, type='file', 
+    session = get_session(use_cookies=False, type='file',
                             data_dir='./cache')
 
     session['foo'] = 'foo'
     session['bar'] = 'bar'
     session.save()
 
-    session = get_session(use_cookies=False, type='file', 
+    session = get_session(use_cookies=False, type='file',
                             data_dir='./cache', id=session.id)
     assert session['foo'] == 'foo'
     assert session['bar'] == 'bar'
     session.namespace['test'] = 'some test'
     session.namespace.do_close()
 
-    session = get_session(use_cookies=False, type='file', 
+    session = get_session(use_cookies=False, type='file',
                             data_dir='./cache', id=session.id)
 
     session.namespace.do_open('r', False)
 
 
 def test_invalidate_corrupt():
-    session = get_session(use_cookies=False, type='file', 
+    session = get_session(use_cookies=False, type='file',
                             data_dir='./cache')
     session['foo'] = 'bar'
     session.save()
     util.assert_raises(
         util.pickle.UnpicklingError,
         get_session,
-        use_cookies=False, type='file', 
+        use_cookies=False, type='file',
                 data_dir='./cache', id=session.id
     )
 
-    session = get_session(use_cookies=False, type='file', 
+    session = get_session(use_cookies=False, type='file',
                             invalidate_corrupt=True,
                             data_dir='./cache', id=session.id)
     assert "foo" not in dict(session)
-