[Zope3-checkins]
SVN: Zope3/branches/jim-session/src/zope/app/session/
Begin rearranging session api and tests
Jim Fulton
jim at zope.com
Sun Jun 6 11:49:42 EDT 2004
Log message for revision 25278:
Begin rearranging session api and tests
-=-
Added: Zope3/branches/jim-session/src/zope/app/session/README.txt
===================================================================
--- Zope3/branches/jim-session/src/zope/app/session/README.txt 2004-06-06 15:27:08 UTC (rev 25277)
+++ Zope3/branches/jim-session/src/zope/app/session/README.txt 2004-06-06 15:49:41 UTC (rev 25278)
@@ -0,0 +1,148 @@
+Sessions
+========
+
+Sessions provide a way to temporarily associate information with a
+client without requiring the authentication of a principal. We
+associate an identifier with a particular client. Whenever we get a
+request from that client, we compute the identifier and use the
+identifier to look up associated information, which is stored on the
+server.
+
+A major disadvantage of sessions is that they require management of
+information on the server. This can have major implications for
+scalability. It is possible for a framework to make use of session
+data very easy for the developer. This is great if scalability is not
+an issue, otherwise, it is a booby trap.
+
+Design Issues
+-------------
+
+Sessions introduce a number of issues to be considered:
+
+- Clients have to be identified. A number of approaches are possible,
+ including:
+
+ o Using HTTP cookies. The application assigns a client identifier,
+ which is stored in a cookie. This technique is the most
+ straightforward, but can be defeated if the client does not
+ support HTTP cookies (usually because the feature has been
+ disabled).
+
+ o Using URLs. The application assigns a client identifier, which is
+ stored in the URL. This makes URLs a bit uglier and requires some
+ care. If people copy URLs and send them to others, then you could
+ end up with multiple clients with the same session
+ identifier. There are a number of ways to reduce the risk of
+ accidental reuse of session identifiers:
+
+ - Embed the client IP address in the identifier
+
+ - Expire the identifier
+
+ o Use hidden form variables. This complicates applications. It
+ requires all requests to be POST requests and requires the
+ maintenance of the hidden variables.
+
+ o Use the client IP address
+
+ This doesn't work very well, because an IP address may be shared by
+ many clients.
+
+- Data storage
+
+ Data can be simply stored in the object database. This provides lots
+ of flexibility. You can store pretty much anything you want as long
+ as it is persistent. You get the full benefit of the object database,
+ such as transactions, transparency, clustering, and so on. Using
+ the object database is especially useful when:
+
+ - Writes are infrequent
+
+ - Data are complex
+
+ If writes are frequent, then the object database introduces
+ scalability problems. Really, any transactional database is likely
+ to introduce problems with frequent writes. If you are tempted to
+ update session data on every request, think very hard about it. You
+ are creating a scalability problem.
+
+ If you know that scalability is not (and never will be) an issue,
+ you can just use the object database.
+
+ If you have client data that needs to be updated often (as in every
+ request), consider storing the data on the client. (Like all data
+ received from a client, it may be tainted and, in most instances,
+ should not be trusted. Sensitive information that the user should
+ not see should likewise not be stored on the client, unless
+ encrypted with a key the client has no access to.) If you can't
+ store it on the client, then consider some other storage mechanism,
+ like a fast database, possibly without transaction support.
+
+ You may be tempted to store session data in memory for speed. This
+ doesn't turn out to work very well. If you need scalability, then
+ you need to be able to use an application-server cluster and storage
+ of session data in memory defeats that. You can use
+ "server-affinity" to assure that requests from a client always go
+ back to the same server, but not all load balancers support server
+ affinity, and, for those that do, enabling server affinity tends to
+ defeat load balancing.
+
+- Session expiration
+
+ You may wish to ensure that sessions terminate after some period of
+ time. This may be for security reasons, or to avoid accidental
+ sharing of a session among multiple clients. The policy might be
+ expressed in terms of total session time, or maximum inactive time,
+ or some combination.
+
+ There are a number of ways to approach this. You can expire client
+ ids. You can expire session data.
+
+- Data expiration
+
+ Because HTTP is a stateless protocol, you can't tell whether a user
+ is thinking about a task or has simply stopped working on it. Some
+ means is needed to free server session storage that is no-longer needed.
+
+ The simplest strategy is to never remove data. This strategy has
+ some obvious disadvantages. Other strategies can be viewed as
+ optimizations of the basic strategy. It is important to realize that
+ a data expiration strategy can be informed by, but need not be
+ constrained by a session-expiration strategy.
+
+Application programming interface
+---------------------------------
+
+Application code will merely adapt request objects to a session data
+interface. Initially, we will define the session data interface
+`IPersistentSessionData'. `IPersistentSessionData` provides a mapping
+interface. Keys in the mapping are application identifiers. Values are
+persistent mapping objects.
+
+Application code that wants to get object session data for a request
+adapts the request to `IPersistentSessionData`::
+
+ data = IPersistentSessionData(request)[appkey]
+
+where `appkey` is a dotted name that identifies the application, such
+as ``zope.app.actionplan``. Given the session data, the application
+can then store data in it::
+
+ data['original'] = original_actions
+ data['new'] = new_actions
+
+From ZPT, you can access session data using adapter syntax::
+
+ request*PersistentSession
+
+So, for example, to access the `old` key for the session data for the
+sample key above:
+
+ request*PersistentSession/zope.app.actionplan/old
+
+In this example, the data for an aplication key was a mapping object.
+The semantics of a session data for a particular application key are
+determined by the session data type interface.
+`IPersistentSessionData` defines the application data to be a mapping
+object by efault. Other data interfaces could specify different
+bahavior.
Property changes on: Zope3/branches/jim-session/src/zope/app/session/README.txt
___________________________________________________________________
Name: svn:eol-style
+ native
Modified: Zope3/branches/jim-session/src/zope/app/session/__init__.py
===================================================================
--- Zope3/branches/jim-session/src/zope/app/session/__init__.py 2004-06-06 15:27:08 UTC (rev 25277)
+++ Zope3/branches/jim-session/src/zope/app/session/__init__.py 2004-06-06 15:49:41 UTC (rev 25278)
@@ -11,249 +11,7 @@
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
-"""
-Session implementation using cookies
+"""Session support
$Id$
"""
-import sha, time, string, random, hmac, warnings, thread
-from UserDict import IterableUserDict
-from heapq import heapify, heappop
-
-from persistent import Persistent
-from zope.server.http.http_date import build_http_date
-from zope.interface import implements
-from zope.component import ComponentLookupError
-from zope.app.zapi import getUtility
-from BTrees.OOBTree import OOBTree
-from zope.app.utility.interfaces import ILocalUtility
-from zope.app.annotation.interfaces import IAttributeAnnotatable
-
-from interfaces import \
- IBrowserIdManager, IBrowserId, ICookieBrowserIdManager, \
- ISessionDataContainer, ISession, ISessionProductData, ISessionData
-
-import ZODB
-import ZODB.MappingStorage
-
-cookieSafeTrans = string.maketrans("+/", "-.")
-
-def digestEncode(s):
- """Encode SHA digest for cookie."""
- return s.encode("base64")[:-2].translate(cookieSafeTrans)
-
-
-class BrowserId(str):
- """See zope.app.interfaces.utilities.session.IBrowserId"""
- implements(IBrowserId)
-
- def __new__(cls, request):
- return str.__new__(
- cls, getUtility(IBrowserIdManager).getBrowserId(request)
- )
-
-
-class CookieBrowserIdManager(Persistent):
- """Session service implemented using cookies."""
-
- implements(IBrowserIdManager, ICookieBrowserIdManager,
- ILocalUtility, IAttributeAnnotatable,
- )
-
- __parent__ = __name__ = None
-
- def __init__(self):
- self.namespace = "zope3_cs_%x" % (int(time.time()) - 1000000000)
- self.secret = "%.20f" % random.random()
- self.cookieLifetime = None
-
- def generateUniqueId(self):
- """Generate a new, random, unique id."""
- data = "%.20f%.20f%.20f" % (random.random(), time.time(), time.clock())
- digest = sha.sha(data).digest()
- s = digestEncode(digest)
- # we store a HMAC of the random value together with it, which makes
- # our session ids unforgeable.
- mac = hmac.new(s, self.secret, digestmod=sha).digest()
- return s + digestEncode(mac)
-
- def getRequestId(self, request):
- """Return the browser id encoded in request as a string,
- or None if it's non-existent."""
- # If there is an id set on the response, use that but don't trust it.
- # We need to check the response in case there has already been a new
- # session created during the course of this request.
- response_cookie = request.response.getCookie(self.namespace)
- if response_cookie:
- sid = response_cookie['value']
- else:
- sid = request.cookies.get(self.namespace)
- if sid is None or len(sid) != 54:
- return None
- s, mac = sid[:27], sid[27:]
- if (digestEncode(hmac.new(s, self.secret, digestmod=sha).digest())
- != mac):
- return None
- else:
- return sid
-
- def setRequestId(self, request, id):
- """Set cookie with id on request."""
- # XXX Currently, the path is the ApplicationURL. This is reasonable,
- # and will be adequate for most purposes.
- # A better path to use would be that of the folder that contains
- # the service-manager this service is registered within. However,
- # that would be expensive to look up on each request, and would
- # have to be altered to take virtual hosting into account.
- # Seeing as this service instance has a unique namespace for its
- # cookie, using ApplicationURL shouldn't be a problem.
-
- if self.cookieLifetime is not None:
- if self.cookieLifetime:
- expires = build_http_date(time.time() + self.cookieLifetime)
- else:
- expires = 'Tue, 19 Jan 2038 00:00:00 GMT'
- request.response.setCookie(
- self.namespace, id, expires=expires,
- path=request.getApplicationURL(path_only=True)
- )
- else:
- request.response.setCookie(
- self.namespace, id,
- path=request.getApplicationURL(path_only=True)
- )
-
- def getBrowserId(self, request):
- """See zope.app.session.interfaces.IBrowserIdManager"""
- sid = self.getRequestId(request)
- if sid is None:
- sid = self.generateUniqueId()
- self.setRequestId(request, sid)
- return sid
-
-
-class PersistentSessionDataContainer(Persistent, IterableUserDict):
- ''' A SessionDataContainer that stores data in the ZODB '''
- __parent__ = __name__ = None
-
- implements(ISessionDataContainer, ILocalUtility, IAttributeAnnotatable)
-
- def __init__(self):
- self.data = OOBTree()
- self.sweepInterval = 5*60
-
- def __getitem__(self, product_id):
- rv = IterableUserDict.__getitem__(self, product_id)
- now = time.time()
- # Only update lastAccessTime once every few minutes, rather than
- # every hit, to avoid ZODB bloat and conflicts
- if rv.lastAccessTime + self.sweepInterval < now:
- rv.lastAccessTime = int(now)
- # XXX: When scheduler exists, this method should just schedule
- # a sweep later since we are currently busy handling a request
- # and may end up doing simultaneous sweeps
- self.sweep()
- return rv
-
- def __setitem__(self, product_id, session_data):
- session_data.lastAccessTime = int(time.time())
- return IterableUserDict.__setitem__(self, product_id, session_data)
-
- def sweep(self):
- ''' Clean out stale data '''
- expire_time = time.time() - self.sweepInterval
- heap = [(v.lastAccessTime, k) for k,v in self.data.items()]
- heapify(heap)
- while heap:
- lastAccessTime, key = heappop(heap)
- if lastAccessTime < expire_time:
- del self.data[key]
- else:
- return
-
-
-class RAMSessionDataContainer(PersistentSessionDataContainer):
- ''' A SessionDataContainer that stores data in RAM. Currently session
- data is not shared between Zope clients, so server affinity will
- need to be maintained to use this in a ZEO cluster.
- '''
- def __init__(self):
- self.sweepInterval = 5*60
- self.key = sha.new(str(time.time() + random.random())).hexdigest()
-
- _ram_storage = ZODB.MappingStorage.MappingStorage()
- _ram_db = ZODB.DB(_ram_storage)
- _conns = {}
-
- def _getData(self):
-
- # Open a connection to _ram_storage per thread
- tid = thread.get_ident()
- if not self._conns.has_key(tid):
- self._conns[tid] = self._ram_db.open()
-
- root = self._conns[tid].root()
- if not root.has_key(self.key):
- root[self.key] = OOBTree()
- return root[self.key]
-
- data = property(_getData, None)
-
- def sweep(self):
- super(RAMSessionDataContainer, self).sweep()
- self._ram_db.pack(time.time())
-
-
-class Session:
- """See zope.app.session.interfaces.ISession"""
- implements(ISession)
- __slots__ = ('browser_id',)
- def __init__(self, request):
- self.browser_id = str(IBrowserId(request))
-
- def __getitem__(self, product_id):
- """See zope.app.session.interfaces.ISession"""
-
- # First locate the ISessionDataContainer by looking up
- # the named Utility, and falling back to the unnamed one.
- try:
- sdc = getUtility(ISessionDataContainer, product_id)
- except ComponentLookupError:
- # XXX: Do we want this?
- warnings.warn(
- 'Unable to find ISessionDataContainer named %s. '
- 'Using default' % repr(product_id),
- RuntimeWarning
- )
- sdc = getUtility(ISessionDataContainer)
-
- # The ISessionDataContainer contains two levels:
- # ISessionDataContainer[product_id] == ISessionProductData
- # ISessionDataContainer[product_id][browser_id] == ISessionData
- try:
- spd = sdc[product_id]
- except KeyError:
- sdc[product_id] = SessionProductData()
- spd = sdc[product_id]
-
- try:
- return spd[self.browser_id]
- except KeyError:
- spd[self.browser_id] = SessionData()
- return spd[self.browser_id]
-
-
-class SessionProductData(Persistent, IterableUserDict):
- """See zope.app.session.interfaces.ISessionProductData"""
- implements(ISessionProductData)
- lastAccessTime = 0
- def __init__(self):
- self.data = OOBTree()
-
-
-class SessionData(Persistent, IterableUserDict):
- """See zope.app.session.interfaces.ISessionData"""
- implements(ISessionData)
- def __init__(self):
- self.data = OOBTree()
-
Copied: Zope3/branches/jim-session/src/zope/app/session/http.py (from rev 25125, Zope3/trunk/src/zope/app/session/__init__.py)
===================================================================
--- Zope3/trunk/src/zope/app/session/__init__.py 2004-05-29 13:20:46 UTC (rev 25125)
+++ Zope3/branches/jim-session/src/zope/app/session/http.py 2004-06-06 15:49:41 UTC (rev 25278)
@@ -0,0 +1,266 @@
+##############################################################################
+#
+# Copyright (c) 2004 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""
+Session implementation using cookies
+
+$Id$
+"""
+
+from persistent import Persistent
+from zope.app.annotation.interfaces import IAttributeAnnotatable
+from zope.app.i18n import ZopeMessageIDFactory as _
+from zope.app import zapi
+from zope.app.session.interfaces import IClientIdManager
+from zope.app.utility.interfaces import ILocalUtility
+from zope import schema
+from zope.interface import implements
+from zope.server.http.http_date import build_http_date
+import hmac
+import random
+import re
+import sha
+import string
+import time
+
+cookieSafeTrans = string.maketrans("+/", "-.")
+
+def digestEncode(s):
+ """Encode SHA digest for cookie."""
+ return s.encode("base64")[:-2].translate(cookieSafeTrans)
+
+class ICookieClientIdManager(IClientIdManager):
+ """Manages sessions using a cookie"""
+
+ namespace = schema.TextLine(
+ title=_('Cookie Name'),
+ description=_(
+ "Name of cookie used to maintain state. "
+ "Must be unique to the site domain name, and only contain "
+ "ASCII letters, digits and '_'"
+ ),
+ required=True,
+ min_length=1,
+ max_length=30,
+ constraint=re.compile("^[\d\w_]+$").search,
+ )
+
+ cookieLifetime = schema.Int(
+ title=_('Cookie Lifetime'),
+ description=_(
+ "Number of seconds until the browser expires the cookie. "
+ "Leave blank expire the cookie when the browser is quit. "
+ "Set to 0 to never expire. "
+ ),
+ min=0,
+ required=False,
+ default=None,
+ missing_value=None,
+ )
+
+class CookieBrowserIdManager(Persistent):
+ """Session service implemented using cookies."""
+
+ implements(IClientIdManager, ICookieClientIdManager,
+ ILocalUtility, IAttributeAnnotatable,
+ )
+
+ __parent__ = __name__ = None
+
+ def __init__(self):
+ self.namespace = "zope3_cs_%x" % (int(time.time()) - 1000000000)
+ self.secret = "%.20f" % random.random()
+ self.cookieLifetime = None
+
+ def generateUniqueId(self):
+ """Generate a new, random, unique id.
+
+ >>> bim = CookieBrowserIdManager()
+ >>> id1 = bim.generateUniqueId()
+ >>> id2 = bim.generateUniqueId()
+ >>> id1 != id2
+ True
+
+ """
+ data = "%.20f%.20f%.20f" % (random.random(), time.time(), time.clock())
+ digest = sha.sha(data).digest()
+ s = digestEncode(digest)
+ # we store a HMAC of the random value together with it, which makes
+ # our session ids unforgeable.
+ mac = hmac.new(s, self.secret, digestmod=sha).digest()
+ return s + digestEncode(mac)
+
+ def getRequestId(self, request):
+ """Return the browser id encoded in request as a string
+
+ Return None if an id is not set.
+
+ For example:
+
+ >>> from zope.publisher.http import HTTPRequest
+ >>> request = HTTPRequest(None, None, {}, None)
+ >>> bim = CookieBrowserIdManager()
+
+ Because no cookie has been set, we get no id:
+
+ >>> bim.getRequestId(request) is None
+ True
+ >>> id1 = bim.generateUniqueId()
+
+ We can set an id:
+
+ >>> bim.setRequestId(request, id1)
+
+ And get it back:
+
+ >>> bim.getRequestId(request) == id1
+ True
+
+ When we set the request id, we also set a response cookie. We
+ can simulate getting this cookie back in a subsequent request:
+
+ >>> request2 = HTTPRequest(None, None, {}, None)
+ >>> request2._cookies = dict(
+ ... [(name, cookie['value'])
+ ... for (name, cookie) in request.response._cookies.items()
+ ... ])
+
+ And we get the same id back from the new request:
+
+ >>> bim.getRequestId(request) == bim.getRequestId(request2)
+ True
+
+ """
+
+ # If there is an id set on the response, use that but don't trust it.
+ # We need to check the response in case there has already been a new
+ # session created during the course of this request.
+ response_cookie = request.response.getCookie(self.namespace)
+ if response_cookie:
+ sid = response_cookie['value']
+ else:
+ sid = request.cookies.get(self.namespace)
+ if sid is None or len(sid) != 54:
+ return None
+ s, mac = sid[:27], sid[27:]
+ if (digestEncode(hmac.new(s, self.secret, digestmod=sha).digest())
+ != mac):
+ return None
+ else:
+ return sid
+
+ def setRequestId(self, request, id):
+ """Set cookie with id on request.
+
+ This sets the response cookie:
+
+ See the examples in getRequestId.
+
+ Note that the id is checkec for validity. Setting an
+ invalid value is silently ignored:
+
+ >>> from zope.publisher.http import HTTPRequest
+ >>> request = HTTPRequest(None, None, {}, None)
+ >>> bim = CookieBrowserIdManager()
+ >>> bim.getRequestId(request)
+ >>> bim.setRequestId(request, 'invalid id')
+ >>> bim.getRequestId(request)
+
+ For now, the cookie path is the application URL:
+
+ >>> cookie = request.response.getCookie(bim.namespace)
+ >>> cookie['path'] == request.getApplicationURL(path_only=True)
+ True
+
+ In the future, it should be the site containing the
+ CookieBrowserIdManager
+
+ By default, session cookies don't expire:
+
+ >>> cookie.has_key('expires')
+ False
+
+ Expiry time of 0 means never (well - close enough)
+
+ >>> bim.cookieLifetime = 0
+ >>> request = HTTPRequest(None, None, {}, None)
+ >>> bid = bim.getClientId(request)
+ >>> cookie = request.response.getCookie(bim.namespace)
+ >>> cookie['expires']
+ 'Tue, 19 Jan 2038 00:00:00 GMT'
+
+ A non-zero value means to expire after than number of seconds:
+
+ >>> bim.cookieLifetime = 3600
+ >>> request = HTTPRequest(None, None, {}, None)
+ >>> bid = bim.getClientId(request)
+ >>> cookie = request.response.getCookie(bim.namespace)
+ >>> import rfc822
+ >>> expires = time.mktime(rfc822.parsedate(cookie['expires']))
+ >>> expires > time.mktime(time.gmtime()) + 55*60
+ True
+ """
+ # XXX Currently, the path is the ApplicationURL. This is reasonable,
+ # and will be adequate for most purposes.
+ # A better path to use would be that of the folder that contains
+ # the service-manager this service is registered within. However,
+ # that would be expensive to look up on each request, and would
+ # have to be altered to take virtual hosting into account.
+ # Seeing as this service instance has a unique namespace for its
+ # cookie, using ApplicationURL shouldn't be a problem.
+
+ if self.cookieLifetime is not None:
+ if self.cookieLifetime:
+ expires = build_http_date(time.time() + self.cookieLifetime)
+ else:
+ expires = 'Tue, 19 Jan 2038 00:00:00 GMT'
+ request.response.setCookie(
+ self.namespace, id, expires=expires,
+ path=request.getApplicationURL(path_only=True)
+ )
+ else:
+ request.response.setCookie(
+ self.namespace, id,
+ path=request.getApplicationURL(path_only=True)
+ )
+
+ def getClientId(self, request):
+ """Get the client id
+
+ This creates one if necessary:
+
+ >>> from zope.publisher.http import HTTPRequest
+ >>> request = HTTPRequest(None, None, {}, None)
+ >>> bim = CookieBrowserIdManager()
+ >>> id = bim.getClientId(request)
+ >>> id == bim.getClientId(request)
+ True
+
+ The id is retained accross requests:
+
+ >>> request2 = HTTPRequest(None, None, {}, None)
+ >>> request2._cookies = dict(
+ ... [(name, cookie['value'])
+ ... for (name, cookie) in request.response._cookies.items()
+ ... ])
+ >>> id == bim.getClientId(request2)
+ True
+ >>> bool(id)
+ True
+
+ """
+ sid = self.getRequestId(request)
+ if sid is None:
+ sid = self.generateUniqueId()
+ self.setRequestId(request, sid)
+ return sid
Modified: Zope3/branches/jim-session/src/zope/app/session/interfaces.py
===================================================================
--- Zope3/branches/jim-session/src/zope/app/session/interfaces.py 2004-06-06 15:27:08 UTC (rev 25277)
+++ Zope3/branches/jim-session/src/zope/app/session/interfaces.py 2004-06-06 15:49:41 UTC (rev 25278)
@@ -17,17 +17,14 @@
"""
import re
from zope.interface import Interface
-from zope.interface.common.mapping import IMapping, IReadMapping, IWriteMapping
-from zope import schema
-from zope.app.container.interfaces import IContainer
-from zope.app.i18n import ZopeMessageIDFactory as _
+from zope.interface.common.mapping import IMapping
-class IBrowserIdManager(Interface):
- """Manages sessions - fake state over multiple browser requests."""
+class IClientIdManager(Interface):
+ """Manages sessions - fake state over multiple client requests."""
- def getBrowserId(request):
- """Return the browser id for the given request as a string.
+ def getClientId(request):
+ """Return the client id for the given request as a string.
If the request doesn't have an attached sessionId a new one will be
generated.
@@ -40,123 +37,37 @@
""" XXX: Want this
- def invalidate(browser_id):
- ''' Expire the browser_id, and remove any matching ISessionData data
+ def invalidate(client_id):
+ ''' Expire the client_id, and remove any matching ISessionData data
'''
"""
-
-class ICookieBrowserIdManager(IBrowserIdManager):
- """Manages sessions using a cookie"""
-
- namespace = schema.TextLine(
- title=_('Cookie Name'),
- description=_(
- "Name of cookie used to maintain state. "
- "Must be unique to the site domain name, and only contain "
- "ASCII letters, digits and '_'"
- ),
- required=True,
- min_length=1,
- max_length=30,
- constraint=re.compile("^[\d\w_]+$").search,
- )
-
- cookieLifetime = schema.Int(
- title=_('Cookie Lifetime'),
- description=_(
- "Number of seconds until the browser expires the cookie. "
- "Leave blank expire the cookie when the browser is quit. "
- "Set to 0 to never expire. "
- ),
- min=0,
- required=False,
- default=None,
- missing_value=None,
- )
-
-
-class IBrowserId(Interface):
- """A unique ID representing a session"""
-
- def __str__():
- """As a unique ASCII string"""
-
-
-class ISessionDataContainer(IMapping):
- """Stores data objects for sessions.
-
- The object implementing this interface is responsible for expiring data as
- it feels appropriate.
-
- Usage::
-
- session_data_container[product_id][browser_id][key] = value
-
- Attempting to access a key that does not exist will raise a KeyError.
+class IPersistentSessionDataManager(IMapping):
+ """Manage IPersistentSessionData objects by client id
"""
- timeout = schema.Int(
- title=_(u"Timeout"),
- description=_(
- "Number of seconds before data becomes stale and may "
- "be removed"),
- default=3600,
- required=True,
- min=1,
- )
- sweepInterval = schema.Int(
- title=_(u"Purge Interval"),
- description=_(
- "How often stale data is purged in seconds. "
- "Higer values improve performance."
- ),
- default=5*60,
- required=True,
- min=1,
- )
+ def __getitem__(self, client_id):
+ """Returns data for a client id
- def __getitem__(self, product_id):
- """Return an ISessionProductData"""
+ If data hasn't been stored previously, a new IPersistentSessionData
+ will be created and returned.
+
+ """
+
+class IPersistentSessionData(IMapping):
+ """Provide persistent session storage
- def __setitem__(self, product_id, value):
- """Store an ISessionProductData"""
+ Data are stored persistently and transactionally. Data stored must
+ be persistent or picklable.
-
-class ISession(Interface):
- """This object allows retrieval of the correct ISessionData
- for a particular product id
-
- >>> session = ISession(request)[product_id]
- >>> session['color'] = 'red'
+ Note that this type of sessionstoreage should not be written to
+ very frequently.
"""
- def __getitem__(product_id):
- """Locate the correct ISessionDataContainer for the given product id
- and return that product id's ISessionData"""
+ def __getitem__(self, application_id):
+ """Returns data for an application id
-
-class ISessionProductData(IReadMapping, IWriteMapping):
- """Storage for a particular product id's session data, containing
- 0 or more ISessionData instances"""
-
- lastAccessTime = schema.Int(
- title=_("Last Access Time"),
- description=_(
- "Approximate epoch time this ISessionData was last retrieved "
- "from its ISessionDataContainer"
- ),
- default=0,
- required=True,
- )
-
- def __getitem__(self, browser_id):
- """Return an ISessionData"""
-
- def __setitem__(self, browser_id, session_data):
- """Store an ISessionData"""
-
-class ISessionData(IMapping):
- """Storage for a particular product id and browser id's session data"""
-
-
+ If data hasn't been stored previously, a new mapping object
+ will be created and returned.
+
+ """
Copied: Zope3/branches/jim-session/src/zope/app/session/persist.py (from rev 25125, Zope3/trunk/src/zope/app/session/__init__.py)
===================================================================
--- Zope3/trunk/src/zope/app/session/__init__.py 2004-05-29 13:20:46 UTC (rev 25125)
+++ Zope3/branches/jim-session/src/zope/app/session/persist.py 2004-06-06 15:49:41 UTC (rev 25278)
@@ -0,0 +1,254 @@
+##############################################################################
+#
+# Copyright (c) 2004 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""
+Session implementation using cookies
+
+$Id$
+"""
+
+from BTrees.OOBTree import OOBTree
+from heapq import heapify, heappop
+from persistent import Persistent
+from UserDict import DictMixin
+from zope.app.annotation.interfaces import IAttributeAnnotatable
+from zope.app.container.contained import Contained
+from zope.app.i18n import ZopeMessageIDFactory as _
+from zope.app import zapi
+from zope.app.session.interfaces import IClientIdManager
+from zope.app.session.interfaces import IPersistentSessionData
+from zope.app.session.interfaces import IPersistentSessionDataManager
+from zope.app.utility.interfaces import ILocalUtility
+from zope import schema
+import random
+import sha
+import thread
+import time
+import ZODB.MappingStorage
+import zope.cachedescriptors.property
+import zope.interface
+
+
+class ISessionDataConfig(zope.interface.Interface):
+ """Configure session data storage"""
+
+ sweepInterval = schema.Int(
+ title=_(u"Purge Interval"),
+ description=_(
+ "How often stale data is purged in seconds. "
+ "Higer values improve performance."
+ ),
+ default=5*60,
+ required=True,
+ min=1,
+ )
+
+class PersistentSessionDataContainer(Persistent, Contained):
+ ''' A SessionDataContainer that stores data in the ZODB '''
+
+ zope.interface.implements(
+ IPersistentSessionDataManager,
+ ISessionDataConfig,
+ ILocalUtility,
+ IAttributeAnnotatable,
+ )
+
+ def __init__(self):
+ self.data = OOBTree()
+ self.sweepInterval = 5*60
+
+ _v_data = zope.cachedescriptors.property.Volatile('_v_data', dict)
+
+ def __getitem__(self, client_id):
+ """Return data for a client id
+
+ If no data has been stored for the client id, then return
+ a new PersistentSessionData object, but don't modify the
+ PersistentSessionDataContainer.
+
+ Let's look at an example:
+
+ >>> container = PersistentSessionDataContainer()
+ >>> session = container['id']
+ >>> session['appid1'] = 42
+
+ Of course, if we ask for the session data again. we'll get
+ the same object:
+
+ >>> container['id'] is session
+ True
+
+ This applies to new data, even when the container hasn't
+ changed:
+
+ >>> newsession = container['newid']
+ >>> newsession is session
+ False
+ >>> newsession is container['newid']
+ True
+
+ The container has a sweep interval, in seconds.
+ Data older than that interval is removed. To see how this
+ works, we'll "age" our session object by setting it's last
+ access time to a time in the distant past:
+
+ >>> from time import time
+ >>> session.lastAccessTime = (
+ ... int(time()) - container.sweepInterval * 2)
+
+ Now, if we sk for a session, we'll get a different one:
+
+ >>> container['id'] is session
+ False
+
+ And the data we stored before will be gone:
+
+ >>> 'appid1' in container['id']
+ False
+ """
+ now = time.time()
+
+ rv = self.data.get(client_id)
+ if rv is None:
+ rv = self._v_data.get(client_id)
+ if rv is None:
+ rv = PersistentSessionData(self, client_id)
+ rv.lastAccessTime = int(now)
+ self._v_data[client_id] = rv
+
+ # Only update lastAccessTime once every few minutes, rather than
+ # every hit, to avoid ZODB bloat and conflicts
+ if rv.lastAccessTime + self.sweepInterval < now:
+ # XXX: When scheduler exists, this method should just schedule
+ # a sweep later since we are currently busy handling a request
+ # and may end up doing simultaneous sweeps
+ self.sweep()
+ rv = PersistentSessionData(self, client_id)
+ rv.lastAccessTime = int(now)
+ self._v_data[client_id] = rv
+
+ return rv
+
+ def _setitem(self, client_id, session_data):
+ # This method is for use by PersistentSessionData only
+ if client_id in self._v_data:
+ del self._v_data[client_id]
+ self.data[client_id] = session_data
+
+ def sweep(self):
+ ''' Clean out stale data '''
+ expire_time = time.time() - self.sweepInterval
+ heap = [(v.lastAccessTime, k) for k,v in self.data.items()]
+ heapify(heap)
+ while heap:
+ lastAccessTime, key = heappop(heap)
+ if lastAccessTime < expire_time:
+ del self.data[key]
+ else:
+ return
+
+
+class RAMSessionDataContainer(PersistentSessionDataContainer):
+ ''' A SessionDataContainer that stores data in RAM. Currently session
+ data is not shared between Zope clients, so server affinity will
+ need to be maintained to use this in a ZEO cluster.
+ '''
+ def __init__(self):
+ self.sweepInterval = 5*60
+ self.key = sha.new(str(time.time() + random.random())).hexdigest()
+
+ _ram_storage = ZODB.MappingStorage.MappingStorage()
+ _ram_db = ZODB.DB(_ram_storage)
+ _conns = {}
+
+ def _getData(self):
+
+ # Open a connection to _ram_storage per thread
+ tid = thread.get_ident()
+ if not self._conns.has_key(tid):
+ self._conns[tid] = self._ram_db.open()
+
+ root = self._conns[tid].root()
+ if not root.has_key(self.key):
+ root[self.key] = OOBTree()
+ return root[self.key]
+
+ data = property(_getData, None)
+
+ def sweep(self):
+ super(RAMSessionDataContainer, self).sweep()
+ self._ram_db.pack(time.time())
+
+
+class PersistentSessionData(Persistent, DictMixin):
+
+ zope.interface.implements(IPersistentSessionData)
+
+ def __init__(self, manager, client_id):
+ self.manager, self.client_id = manager, client_id
+ self.data = {}
+
+ _v_data = zope.cachedescriptors.property.Volatile('_v_data', dict)
+
+ def __getitem__(self, application_key):
+ r = self.data.get(application_key)
+ if r is None:
+ r = self._v_data.get(application_key)
+ if r is None:
+ r = PersistentApplicationData(self, application_key)
+ return r
+
+ def __setitem__(self, application_key, application_data):
+ if self.manager is not None:
+ self.manager._setitem(self.client_id, self)
+ self.manager = None
+ if application_key in self._v_data:
+ del self._v_data[application_key]
+ self.data[application_key] = application_data
+ self._p_changed = 1
+
+ def __delitem__(self, application_key):
+ if application_key in self.data:
+ del self.data[application_key]
+ if application_key in self._v_data:
+ del self._v_data[application_key]
+ self._p_changed = 1
+
+ def keys(self):
+ return self.data.keys
+
+
+class PersistentApplicationData(DictMixin):
+ def __init__(self, manager, application_key):
+ self.manager = manager
+ self.application_key = application_key
+ self.data = {}
+
+ def __getitem__(self, key):
+ return self.data[key]
+
+ def __setitem__(self, key, value):
+ self.manager[self.application_key] = self
+ self.data[key] = value
+
+ def __delitem__(self, key):
+ del self.data[key]
+
+ def keys(self):
+ return self.data.keys()
+
+def accessPersistentSessionData(request):
+ ids = zapi.getUtility(IClientIdManager)
+ client_id = ids.getClientId(request)
+ manager = zapi.getUtility(IPersistentSessionDataManager)
+ return manager[client_id]
Deleted: Zope3/branches/jim-session/src/zope/app/session/session.stx
===================================================================
--- Zope3/branches/jim-session/src/zope/app/session/session.stx 2004-06-06 15:27:08 UTC (rev 25277)
+++ Zope3/branches/jim-session/src/zope/app/session/session.stx 2004-06-06 15:49:41 UTC (rev 25278)
@@ -1,55 +0,0 @@
-Session Support
----------------
-
-Sessions allow us to fake state over a stateless protocol - HTTP. We do this
-by having a unique identifier stored across multiple HTTP requests, be it
-a cookie or some id mangled into the URL.
-
-The IBrowserIdManager Utility provides this unique id. It is responsible
-for propagating this id so that future requests from the browser get
-the same id (eg. by setting an HTTP cookie)
-
-ISessionDataContainer Utilities store session data. The ISessionDataContainer
-is responsible for expiring data.
-
-ISessionDataContainer[product_id] returns ISessionProductData
-ISessionDataContainer[product_id][browser_id] returns ISessionData
-
-ISession(request)[product_id] returns ISessionData
-
-An ISession determines what ISessionDataContainer to use by looking
-up an ISessionDataContainer using the product_id as the name, and
-falling back to the unnamed ISessionDataContainer utility. This allows
-site administrators to select which ISessionDataContainer a particular
-product stores its session data in by registering the utility with
-the relevant name(s).
-
-Python example::
-
- >>> browser_id = IBrowserId(request)
-
- >>> session_data = ISession(request)['zopeproducts.fooprod']
- >>> session_data['color'] = 'red'
-
- or for the adventurous....
-
- >>> explicit_dc = getUtility(ISessionDataContainer, 'zopeproducts.fooprod')
- >>> session_data = explicit_dc['zopeproducts.fooprod'][str(browser_id)]
- >>> session_data = Session(explicit_dc, browser_id)['zopeproducts.fooprod']
- >>> session_data['color'] = 'red'
-
-
-Page Template example::
-
- XXX: Needs update when TALES adapter syntax decided
-
- <tal:x condition="exists:session/zopeproducts.fooprod/count">
- <tal:x condition="python:
- session['zopeproducts.fooprod']['count'] += 1" />
- </tal:x>
- <tal:x condition="not:exists:session/zopeprodicts.fooprod/count">
- <tal:x condition="python:
- session['zopeproducts.fooprod']['count'] = 1 />
- </tal:x>
- <span content="session/zopeproducts.fooprod/count">6</span>
-
Modified: Zope3/branches/jim-session/src/zope/app/session/tests.py
===================================================================
--- Zope3/branches/jim-session/src/zope/app/session/tests.py 2004-06-06 15:27:08 UTC (rev 25277)
+++ Zope3/branches/jim-session/src/zope/app/session/tests.py 2004-06-06 15:49:41 UTC (rev 25278)
@@ -1,4 +1,3 @@
-# -*- coding: ascii -*-
##############################################################################
#
# Copyright (c) 2004 Zope Corporation and Contributors.
@@ -12,313 +11,14 @@
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
-'''
+"""XXX short summary goes here.
+
$Id$
-'''
-import unittest, doctest, time, rfc822
-from zope.app import zapi
-from zope.app.tests import ztapi
-from zope.app.tests import setup
-import zope.interface
-from zope.app.utility.interfaces import ILocalUtility
-from zope.app.utility import LocalUtilityService
-from zope.app.servicenames import Utilities
-from zope.app.annotation.interfaces import IAttributeAnnotatable
+"""
+import unittest
+from zope.testing.doctestunit import DocTestSuite
-from zope.app.session.interfaces import \
- IBrowserId, IBrowserIdManager, \
- ISession, ISessionDataContainer, ISessionData, ISessionProductData
-from zope.app.session import \
- BrowserId, CookieBrowserIdManager, \
- PersistentSessionDataContainer, RAMSessionDataContainer, \
- Session, SessionData, SessionProductData
-
-from zope.publisher.interfaces import IRequest
-from zope.publisher.interfaces.http import IHTTPRequest
-from zope.publisher.http import HTTPRequest
-
-def setUp(session_data_container_class):
-
- # Placeful setup
- root = setup.placefulSetUp(site=True)
- setup.createStandardServices(root)
- sm = setup.createServiceManager(root, True)
- setup.addService(sm, Utilities, LocalUtilityService())
-
- # Add a CookieBrowserIdManager Utility
- setup.addUtility(sm, '', IBrowserIdManager, CookieBrowserIdManager())
-
- # Add an ISessionDataContainer, registered under a number of names
- sdc = session_data_container_class()
- for product_id in ('', 'products.foo', 'products.bar', 'products.baz'):
- setup.addUtility(sm, product_id, ISessionDataContainer, sdc)
-
- # Register our adapters
- ztapi.provideAdapter(IRequest, IBrowserId, BrowserId)
- ztapi.provideAdapter(IRequest, ISession, Session)
-
- # Return a request
- request = HTTPRequest(None, None, {}, None)
- return request
-
-def tearDown():
- setup.placefulTearDown()
-
-
-def test_CookieBrowserIdManager():
- """
- CookieBrowserIdManager.generateUniqueId should generate a unique
- IBrowserId each time it is called
-
- >>> bim = CookieBrowserIdManager()
- >>> id1 = bim.generateUniqueId()
- >>> id2 = bim.generateUniqueId()
- >>> id1 != id2
- True
-
- CookieBrowserIdManager.getRequestId pulls the browser id from an
- IHTTPRequest, or returns None if there isn't one stored in it.
- Because cookies cannnot be trusted, we confirm that they are not forged,
- returning None if we have a corrupt or forged browser id.
-
- >>> request = HTTPRequest(None, None, {}, None)
- >>> bim.getRequestId(request) is None
- True
- >>> bim.setRequestId(request, id1)
- >>> bim.getRequestId(request) == id1
- True
- >>> bim.setRequestId(request, 'invalid_id')
- >>> bim.getRequestId(request) is None
- True
-
- Make sure that the same browser id is extracted from a cookie in
- request (sent from the browser) and a cookie in request.response
- (set during this transaction)
-
- >>> request2 = HTTPRequest(None, None, {}, None)
- >>> request2._cookies = request.response._cookies
- >>> bim.getRequestId(request) == bim.getRequestId(request2)
- True
-
- CookieBrowserIdManager.getBrowserId pulls the browser id from an
- IHTTPRequest, or generates a new one and returns it after storing
- it in the request.
-
- >>> id3 = bim.getBrowserId(request)
- >>> id4 = bim.getBrowserId(request)
- >>> id3 == id4
- True
- >>> id3 == id4
- True
- >>> bool(id3)
- True
-
- Confirm the path of the cookie is correct. The value being tested
- for here will eventually change - it should be the URL to the
- site containing the CookieBrowserIdManager
-
- >>> cookie = request.response.getCookie(bim.namespace)
- >>> cookie['path'] == request.getApplicationURL(path_only=True)
- True
-
- Confirm the expiry time of the cookie is correct.
- Default is no expires.
-
- >>> cookie.has_key('expires')
- False
-
- Expiry time of 0 means never (well - close enough)
-
- >>> bim.cookieLifetime = 0
- >>> request = HTTPRequest(None, None, {}, None)
- >>> bid = bim.getBrowserId(request)
- >>> cookie = request.response.getCookie(bim.namespace)
- >>> cookie['expires']
- 'Tue, 19 Jan 2038 00:00:00 GMT'
-
- >>> bim.cookieLifetime = 3600
- >>> request = HTTPRequest(None, None, {}, None)
- >>> bid = bim.getBrowserId(request)
- >>> cookie = request.response.getCookie(bim.namespace)
- >>> expires = time.mktime(rfc822.parsedate(cookie['expires']))
- >>> expires > time.mktime(time.gmtime()) + 55*60
- True
- """
-
-
-def test_BrowserId():
- """
- >>> request = setUp(PersistentSessionDataContainer)
-
- >>> id1 = BrowserId(request)
- >>> id2 = BrowserId(request)
- >>> id1 == id2
- True
-
- >>> tearDown()
- """
-
-
-def test_PersistentSessionDataContainer():
- """
- Ensure mapping interface is working as expected
-
- >>> sdc = PersistentSessionDataContainer()
- >>> sdc['a']
- Traceback (most recent call last):
- File "<stdin>", line 1, in ?
- File "/usr/python-2.3.3/lib/python2.3/UserDict.py", line 19, in __getitem__
- def __getitem__(self, key): return self.data[key]
- KeyError: 'a'
- >>> sdc['a'] = SessionData()
- >>> pdict = SessionData()
- >>> sdc['a'] = pdict
- >>> id(pdict) == id(sdc['a'])
- True
- >>> del sdc['a']
- >>> sdc['a']
- Traceback (most recent call last):
- File "<stdin>", line 1, in ?
- File "/usr/python-2.3.3/lib/python2.3/UserDict.py", line 19, in __getitem__
- def __getitem__(self, key): return self.data[key]
- KeyError: 'a'
- >>> del sdc['a']
- Traceback (most recent call last):
- File "<stdin>", line 1, in ?
- File "/usr/python-2.3.3/lib/python2.3/UserDict.py", line 21, in __delitem__
- def __delitem__(self, key): del self.data[key]
- KeyError: 'a'
-
- Make sure stale data is removed
-
- >>> sdc.sweepInterval = 60
- >>> sdc[1], sdc[2] = sd1, sd2 = SessionData(), SessionData()
- >>> ignore = sdc[1], sdc[2]
- >>> sd1.lastAccessTime = sd1.lastAccessTime - 62
- >>> sd2.lastAccessTime = sd2.lastAccessTime - 62
- >>> ignore = sdc[1]
- >>> sdc.get(2, 'stale')
- 'stale'
-
- Ensure lastAccessTime on the ISessionData is being updated
- occasionally. The ISessionDataContainer maintains this whenever
- the ISessionData is retrieved.
-
- >>> sd = SessionData()
- >>> sdc['product_id'] = sd
- >>> sd.lastAccessTime > 0
- True
- >>> last1 = sd.lastAccessTime - 62
- >>> sd.lastAccessTime = last1 # Wind back the clock
- >>> last1 < sdc['product_id'].lastAccessTime
- True
- """
-
-
-def test_RAMSessionDataContainer(self):
- pass
-test_RAMSessionDataContainer.__doc__ = \
- test_PersistentSessionDataContainer.__doc__.replace(
- 'PersistentSessionDataContainer', 'RAMSessionDataContainer'
- )
-
-
-def test_SessionProductData():
- """
- >>> session = SessionProductData()
- >>> ISessionProductData.providedBy(session)
- True
- """
-
-
-def test_SessionData():
- """
- >>> session = SessionData()
-
- Is the interface defined?
-
- >>> ISessionData.providedBy(session)
- True
-
- Make sure it actually works
-
- >>> session['color']
- Traceback (most recent call last):
- File "<stdin>", line 1, in ?
- File "zope/app/utilities/session.py", line 157, in __getitem__
- return self._data[key]
- KeyError: 'color'
- >>> session['color'] = 'red'
- >>> session['color']
- 'red'
-
- Test the rest of the dictionary interface...
-
- >>> 'foo' in session
- False
- >>> 'color' in session
- True
- >>> session.get('size', 'missing')
- 'missing'
- >>> session.get('color', 'missing')
- 'red'
- >>> list(session.keys())
- ['color']
- >>> list(session.values())
- ['red']
- >>> list(session.items())
- [('color', 'red')]
- >>> len(session)
- 1
- >>> [x for x in session]
- ['color']
- >>> del session['color']
- >>> session.get('color') is None
- True
- """
-
-def test_Session():
- """
- >>> request = setUp(PersistentSessionDataContainer)
- >>> request2 = HTTPRequest(None, None, {}, None)
-
- >>> ISession.providedBy(Session(request))
- True
-
- >>> session1 = Session(request)['products.foo']
- >>> session2 = Session(request)['products.bar']
- >>> session3 = Session(request)['products.bar'] # dupe
- >>> session4 = Session(request2)['products.bar'] # not dupe
-
- Make sure it returned sane values
-
- >>> ISessionData.providedBy(session1)
- True
- >>> ISessionData.providedBy(session2)
- True
- >>> session2 == session3
- True
- >>> ISessionData.providedBy(session4)
- True
-
- Make sure that product_ids don't share a namespace, except when they should
-
- >>> session1['color'] = 'red'
- >>> session2['color'] = 'blue'
- >>> session4['color'] = 'vomit'
- >>> session1['color']
- 'red'
- >>> session2['color']
- 'blue'
- >>> session3['color']
- 'blue'
- >>> session4['color']
- 'vomit'
-
- >>> tearDown()
- """
-
from zope.app.appsetup.tests import TestBootstrapSubscriberBase, EventStub
class TestBootstrapInstance(TestBootstrapSubscriberBase):
@@ -345,13 +45,11 @@
def test_suite():
return unittest.TestSuite((
- doctest.DocTestSuite(),
+ DocTestSuite('zope.app.session.http'),
+ DocTestSuite('zope.app.session.persist'),
unittest.makeSuite(TestBootstrapInstance),
))
if __name__ == '__main__':
- unittest.main()
+ unittest.main(defaultTest='test_suite')
-# vim: set filetype=python ts=4 sw=4 et si
-
-
More information about the Zope3-Checkins
mailing list