# HG changeset patch # User paulb # Date 1094399504 0 # Node ID 509f867851353a2ab9c4a9446c5bfe88a82c52cc # Parent fcf7098ad0d2588aa32978e4c91dd78f7a8a3991 [project @ 2004-09-05 15:51:34 by paulb] Added session support for certain frameworks. diff -r fcf7098ad0d2 -r 509f86785135 README.txt --- a/README.txt Sun Sep 05 11:12:32 2004 +0000 +++ b/README.txt Sun Sep 05 15:51:44 2004 +0000 @@ -41,6 +41,7 @@ Fixed field/parameter retrieval so that path and body fields are distinct, regardless of the framework employed. Introduced Zope 2 support. +Session support has been added. New in WebStack 0.6 (Changes Since WebStack 0.5) ------------------------------------------------ @@ -96,6 +97,9 @@ This should be reviewed at a later date when proper standardisation has taken place. +Session support, especially through WebStack.Helpers.Session, should be +reviewed. + Release Procedures ------------------ diff -r fcf7098ad0d2 -r 509f86785135 WebStack/BaseHTTPRequestHandler.py --- a/WebStack/BaseHTTPRequestHandler.py Sun Sep 05 11:12:32 2004 +0000 +++ b/WebStack/BaseHTTPRequestHandler.py Sun Sep 05 15:51:44 2004 +0000 @@ -8,6 +8,7 @@ from Helpers.Request import MessageBodyStream, get_body_fields, get_storage_items from Helpers.Response import ConvertingStream from Helpers.Auth import UserInfo +from Helpers.Session import SessionStore from cgi import parse_qs, FieldStorage import Cookie from StringIO import StringIO @@ -44,6 +45,10 @@ self.storage_body = None + # Special objects retained throughout the transaction. + + self.session_store = None + def commit(self): """ @@ -51,6 +56,13 @@ objects. """ + # Close the session store. + + if self.session_store is not None: + self.session_store.close() + + # Prepare the response. + self.trans.send_response(self.response_code) if self.content_type is not None: self.trans.send_header("Content-Type", str(self.content_type)) @@ -370,6 +382,42 @@ self.cookies_out[cookie_name]["expires"] = 0 self.cookies_out[cookie_name]["max-age"] = 0 + # Session-related methods. + + def get_session(self, create=1): + + """ + Gets a session corresponding to an identifier supplied in the + transaction. + + If no session has yet been established according to information + provided in the transaction then the optional 'create' parameter + determines whether a new session will be established. + + Where no session has been established and where 'create' is set to 0 + then None is returned. In all other cases, a session object is created + (where appropriate) and returned. + """ + + # NOTE: Requires configuration. + + if self.session_store is None: + self.session_store = SessionStore(self, "WebStack-sessions") + return self.session_store.get_session(create) + + def expire_session(self): + + """ + Expires any session established according to information provided in the + transaction. + """ + + # NOTE: Requires configuration. + + if self.session_store is None: + self.session_store = SessionStore(self, "WebStack-sessions") + self.session_store.expire_session() + # Application-specific methods. def set_user(self, username): diff -r fcf7098ad0d2 -r 509f86785135 WebStack/CGI.py --- a/WebStack/CGI.py Sun Sep 05 11:12:32 2004 +0000 +++ b/WebStack/CGI.py Sun Sep 05 15:51:44 2004 +0000 @@ -9,6 +9,7 @@ from Helpers.Request import MessageBodyStream, get_body_fields, get_storage_items from Helpers.Response import ConvertingStream from Helpers.Auth import UserInfo +from Helpers.Session import SessionStore from Helpers import Environment from cgi import parse_qs, FieldStorage import Cookie @@ -49,6 +50,10 @@ self.storage_body = None + # Special objects retained throughout the transaction. + + self.session_store = None + def commit(self): """ @@ -58,6 +63,11 @@ See draft-coar-cgi-v11-03, section 7. """ + # Close the session store. + + if self.session_store is not None: + self.session_store.close() + # NOTE: Provide sensible messages. self.output.write("Status: %s %s\n" % (self.response_code, "WebStack status")) @@ -359,6 +369,42 @@ self.cookies_out[cookie_name]["expires"] = 0 self.cookies_out[cookie_name]["max-age"] = 0 + # Session-related methods. + + def get_session(self, create=1): + + """ + Gets a session corresponding to an identifier supplied in the + transaction. + + If no session has yet been established according to information + provided in the transaction then the optional 'create' parameter + determines whether a new session will be established. + + Where no session has been established and where 'create' is set to 0 + then None is returned. In all other cases, a session object is created + (where appropriate) and returned. + """ + + # NOTE: Requires configuration. + + if self.session_store is None: + self.session_store = SessionStore(self, "WebStack-sessions") + return self.session_store.get_session(create) + + def expire_session(self): + + """ + Expires any session established according to information provided in the + transaction. + """ + + # NOTE: Requires configuration. + + if self.session_store is None: + self.session_store = SessionStore(self, "WebStack-sessions") + self.session_store.expire_session() + # Application-specific methods. def set_user(self, username): diff -r fcf7098ad0d2 -r 509f86785135 WebStack/Helpers/Session.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebStack/Helpers/Session.py Sun Sep 05 15:51:44 2004 +0000 @@ -0,0 +1,188 @@ +#!/usr/bin/env python + +""" +Session helper functions. +""" + +import shelve +import os +import glob +import time +import random +import sys + +class SessionStore: + + "A class representing a session store." + + def __init__(self, trans, session_directory, session_cookie_name="SID", concurrent=1, delay=1): + + """ + Initialise the session store, specifying the transaction 'trans' within + which all session access will occur, a base 'session_directory', the + optional 'session_cookie_name' where the session identifier is held for + each user, and specifying using the optional 'concurrent' parameter + whether concurrent access within the framework might occur (1) or + whether the framework queues accesses at some other level (0). The + optional 'delay' argument specifies the time in seconds between each + poll of the session file when that file is found to be locked for + editing. + """ + + self.trans = trans + self.session_directory = session_directory + self.session_cookie_name = session_cookie_name + self.concurrent = concurrent + self.delay = delay + + # Internal state. + + self.store = None + self.store_filename, self.edit_filename = None, None + self.to_expire = None + + def close(self): + + "Close the store, tidying up files and filenames." + + if self.store is not None: + self.store.close() + self.store = None + if self.edit_filename is not None: + try: + os.rename(self.edit_filename, self.store_filename) + except OSError: + pass + self.edit_filename, self.store_filename = None, None + + # Handle expiry appropriately. + + if self.to_expire is not None: + self._expire_session(self.to_expire) + self.trans.delete_cookie(self.session_cookie_name) + + def expire_session(self): + + """ + Expire the session in the given transaction. + """ + + # Perform expiry. + + cookie = self.trans.get_cookie(self.session_cookie_name) + if cookie: + self.to_expire = cookie.value + + def _expire_session(self, session_id): + + """ + Expire the session with the given 'session_id'. Note that in concurrent + session stores, this operation will block if another execution context + is editing the session. + """ + + filename = os.path.join(self.session_directory, session_id) + if self.concurrent: + while 1: + try: + os.unlink(filename) + except OSError: + time.sleep(self.delay) + else: + break + else: + try: + os.unlink(filename) + except OSError: + pass + + def get_session(self, create): + + """ + Get the session for the given transaction, creating a new session if + 'create' is set to 1 (rather than 0). Where new sessions are created, an + appropriate session identifier cookie will be created. + Returns a session object or None if no session exists and none is then + created. + """ + + cookie = self.trans.get_cookie(self.session_cookie_name) + if cookie: + return self._get_session(cookie.value, create) + elif create: + session_id = self._get_session_identifier() + self.trans.set_cookie_value(self.session_cookie_name, session_id) + return self._get_session(session_id, create) + else: + return None + + def _get_session(self, session_id, create): + + """ + Get a session with the given 'session_id' and whether new sessions + should be created ('create' set to 1). + Returns a dictionary-like object representing the session. + """ + + filename = os.path.join(self.session_directory, session_id) + + # Enforce locking. + + if self.concurrent: + + # Where the session is present (possibly being edited)... + + if glob.glob(filename + "*"): + while 1: + try: + os.rename(filename, filename + ".edit") + except OSError: + time.sleep(self.delay) + else: + break + + # Where no session is present and none should be created, return. + + elif not create: + return None + + self.store_filename = filename + filename = filename + ".edit" + self.edit_filename = filename + + # For non-concurrent situations, return if no session exists and none + # should be created. + + elif not os.path.exists(filename) and not create: + return None + + self.store = shelve.open(filename) + return Wrapper(self.store) + + def _get_session_identifier(self): + + "Return a session identifier as a string." + + g = random.Random() + return str(g.randint(0, sys.maxint - 1)) + +class Wrapper: + + "A wrapper around shelf objects." + + def __init__(self, store): + self.store = store + + def __getattr__(self, name): + if hasattr(self.store, name): + return getattr(self.store, name) + else: + raise AttributeError, name + + def items(self): + l = [] + for key in self.store.keys(): + l.append((key, self.store[key])) + return l + +# vim: tabstop=4 expandtab shiftwidth=4 diff -r fcf7098ad0d2 -r 509f86785135 WebStack/ModPython.py --- a/WebStack/ModPython.py Sun Sep 05 11:12:32 2004 +0000 +++ b/WebStack/ModPython.py Sun Sep 05 15:51:44 2004 +0000 @@ -10,12 +10,17 @@ from mod_python.util import parse_qs, FieldStorage from mod_python import apache -# NOTE: Should provide alternative implementations. +# NOTE: Provide an alternative implementation for the cookie support. +# NOTE: The alternative session support requires cookie support. try: from mod_python import Cookie except ImportError: Cookie = None -try: from mod_python import Session -except ImportError: Session = None +try: + from mod_python import Session +except ImportError: + from Helpers.Session import SessionStore + import os + Session = None class Transaction(Generic.Transaction): @@ -36,6 +41,22 @@ self.storage_body = None + # Special objects retained throughout the transaction. + + self.session_store = None + + def commit(self): + + """ + A special method, synchronising the transaction with framework-specific + objects. + """ + + # Close the session store. + + if self.session_store is not None: + self.session_store.close() + # Request-related methods. def get_request_stream(self): @@ -356,7 +377,11 @@ # NOTE: Not exposing all functionality. return Session.Session(self.trans) else: - return None + # NOTE: Requires configuration. + + if self.session_store is None: + self.session_store = SessionStore(self, os.path.join(apache.server_root(), "WebStack-sessions")) + return self.session_store.get_session(create) def expire_session(self): @@ -365,9 +390,16 @@ transaction. """ - session = self.get_session(create=0) - if session: - session.invalidate() + if Session: + session = self.get_session(create=0) + if session: + session.invalidate() + else: + # NOTE: Requires configuration. + + if self.session_store is None: + self.session_store = SessionStore(self, os.path.join(apache.server_root(), "WebStack-sessions")) + self.session_store.expire_session() # Application-specific methods. diff -r fcf7098ad0d2 -r 509f86785135 WebStack/Twisted.py --- a/WebStack/Twisted.py Sun Sep 05 11:12:32 2004 +0000 +++ b/WebStack/Twisted.py Sun Sep 05 15:51:44 2004 +0000 @@ -8,6 +8,7 @@ from Helpers.Auth import UserInfo from Helpers.Request import Cookie, get_body_field from Helpers.Response import ConvertingStream +from Helpers.Session import SessionStore from cgi import parse_qs class Transaction(Generic.Transaction): @@ -24,6 +25,22 @@ self.user = None self.content_type = None + # Special objects retained throughout the transaction. + + self.session_store = None + + def commit(self): + + """ + A special method, synchronising the transaction with framework-specific + objects. + """ + + # Close the session store. + + if self.session_store is not None: + self.session_store.close() + # Request-related methods. def get_request_stream(self): @@ -341,6 +358,42 @@ self.trans.addCookie(cookie_name, "", expires=0, path="/", max_age=0) + # Session-related methods. + + def get_session(self, create=1): + + """ + Gets a session corresponding to an identifier supplied in the + transaction. + + If no session has yet been established according to information + provided in the transaction then the optional 'create' parameter + determines whether a new session will be established. + + Where no session has been established and where 'create' is set to 0 + then None is returned. In all other cases, a session object is created + (where appropriate) and returned. + """ + + # NOTE: Requires configuration. + + if self.session_store is None: + self.session_store = SessionStore(self, "WebStack-sessions") + return self.session_store.get_session(create) + + def expire_session(self): + + """ + Expires any session established according to information provided in the + transaction. + """ + + # NOTE: Requires configuration. + + if self.session_store is None: + self.session_store = SessionStore(self, "WebStack-sessions") + self.session_store.expire_session() + # Application-specific methods. def set_user(self, username): diff -r fcf7098ad0d2 -r 509f86785135 docs/SESSION.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/SESSION.txt Sun Sep 05 15:51:44 2004 +0000 @@ -0,0 +1,19 @@ +Session Support in WebStack +--------------------------- + +Various frameworks do not support sessions. In order to provide primitive +support for sessions within WebStack upon such frameworks, the +WebStack.Helpers.Session module is used to provide a simple file-based session +store. It is necessary to create a directory called WebStack-sessions in a +particular location for the session store to function, and the location depends +on the framework as summarised in the following table. + +Framework Location +--------- -------- +BaseHTTPRequestHandler The directory where the server is run. +CGI The directory where the handler resides. +mod_python The server root (eg. /usr/local/apache2). + +Note that the WebStack-sessions directory must have the appropriate ownership +and privileges necessary for the server/framework to write session files into +it.