How-To: Easy CGI Session Handling with pso.session


Version:$Revision: 1.7 $
Last Changed: 3-13-2002
Author: thanos vassilakis
Company: Script Foundry Inc.
Document URL: http://sourceforge.net/docman/display_doc.php?docid=10175&group_id=49265
Contact: thanos@0x01.com
Feed-back: pso-development@lists.sourceforge.net
Copyright: thanos vassliakis 2001,2002
Contributions: Ming Huang and Vadim Shaykevich.
$Id: session-cgi.html,v 1.7 2003/01/24 15:21:47 thanos Exp $



  1. Introduction
  2. System Requirements
  3. Real Quick Example - 95% of the Time
  4. What's Going On
  5. More Control - 5% of the Time

  6. Session classes
  7. Installation
  8. Installation Example

  1. Introduction:

    This document shows you how to use pso.session to add easy session handling to a CGI script. Python Service Objects is a open source Internet service development system. The pso system allows you to develop platform and server independent cgi scripts that can later be promoted to NSAPI or modpython request handlers, often without any changes.

  2. System Requirements

  3. Real Quick Example - 95% of the Time

    Let's start with a really simple CGI:
    Its source is as follows:
    #!/usr/bin python2.2
    
    def testHandler():
    	print "content-type: text/html"
    	print
    	print "hello world"
    
    if __name__ == '__main__':	
    	testHandler()
    
    
    Now let's change it to handle sessions:
    
    
    
    
    #!/usr/bin/env python2.2
    from pso.service import ServiceHandler
    
    def testHandler(serviceRequest):
    	serviceRequest.pso().send_http_headers()
    	try:
    		 serviceRequest.pso().session['reloads'] +=1
    	except:
    		 serviceRequest.pso().session['reloads'] =1
    	print "Hello World!  ~ You reloaded: %(reloads)d times ~ Try Reload !" %  serviceRequest.pso().session
    
    if __name__ == '__main__':
    	ServiceHandler().run(testHandler)
    
    The above example was very quick. A lot of default values were used and many assumption were made, yet it shows you how painless it is to add and use session handling.

    Test: http://www.0x01.com/~thanos/pso/tests/session/cgi/quick.py

  4. What's Going On

    ServiceHandler is a boot strap. It creates a ServiceRequest that is the bridge between the pso service interface and the actual underlining server handler. The session object is created, and added to the service request object. This is then passed to testHandler(serviceRequest) as serviceRequest. serviceRequest.pso() is a proxy call to this service request. serviceRequest.pso().send_http_headers() prints the content-type, and, just as important, the cookie header set with the session id. The algorithm for getting the session is carried out by pso.session.ServiceHandler.getSession and is essentially this:
    1. Get the service id from os.environ. Default to the value the request handler module's name.
    2. Get the HTTP_COOKIE entry in the os.environ.
    3. Extract from the cookie the value keyed on the service id and use it as the session id.
    4. If no session id was found generate a new one (step 6).
    5. otherwise load and unpickle the session file whose name is based on the service id and session id.
    6. If this fails
      1. create a new session
      2. add to serviceRequest.pos().headers_out['cookie-set'] the value serviceId=sessionId, for example: serviceRequest.pso().setCookie('MI6Service','007').
      3. Save session when serviceRequest.pso() is finalized.

  5. More Control - That Last 5%

    Setting the service id:
    add the line
    SetEnv PSOServiceId 'Your_Service_Id'
    to the .htaccess file or to the relevant apache httpd.conf <directory> block.

    Forcing the session id:
    add the line
    SetEnv 'Your_Service_Id' 'Your_Session_Id'
    to the .htaccess file or to the relevant apache httpd.conf <directory> block.

    Setting when a session expires
    By default sessions will expire when the user closes their browser. You can preset the life span of a session for the service in question by adding in the .htaccess file the following line:
    SetEnv PSOSessionExpire 900 # 15 minutes
    You can use simple expressions
    SetEnv PSOSessionExpire "15*60" # 15 minutes
    You can also set the sessions to expire on a specific date:
    SetEnv PSOSessionExpire "Mon, 15 Apr 2001 19:00:00 GMT" # 15 minutes
    For more on this format see RFC2068 section 3.3.1 [also RCF822 and RCF1123] If a date is not give all values are seconds relative to the present. You can also control the session's life span in your code:
    • When the browser closes (the default):
      serviceRequest.session.expire()
    • Right away:
      serviceRequest.session.expire(0) 
      

      test: http://www.0x01.com/~thanos/pso/tests/session/cgi/expire.py

    • In a given time:
      serviceRequest.session.expire(15*60) # or SetEnv PSOSessionExpire "60*15" etc.
      twoDays = 60*60*48
      serviceRequest.session.expire(date = time()+twoDays)
    • On a given day
      import time
      serviceRequest.session.expire("Mon, 15 APR 2001 19:00:00", "%a, %d %b %Y %H:%M:%S")

    Reverting the current session to its previous state:
    serviceRequest.session.revert()

    Explicitly saving the current session:
    serviceRequest.session.save()

    Storing the session files in another path:
    There are several ways of doing this:
    • The easy way is to add the line
      SetEnv PSOSessionFileLoader_Path /tmp/path0/  # remember the trailing '/'
      to the .htaccess file or to the relevant apache httpd.conf <directory> block.

      test: http://www.0x01.com/~thanos/pso/tests/session/cgi/path0.py

    • For the oo fans you can subclass pso.session.CookeFileImpl overloading the attribute CookieFileImpl.PATH and then pass it to the service handler.
      # file: path1.py
      #
      #!/usr/bin/env python2.2
      
      from pso.service import ServiceHandler
      
      from pso import session
      class MyFileImpl(session.CookieFileImpl): 
      	PATH="/tmp/path1/"
      
      .
      .
      .
      if __name__ == '__main__':
      # Now change  from:
      #	ServiceHandler().run(test)
      #to:
      	ServiceHandler()Run(test, sessionImpl=MyFileImpl)
      

      test: http://www.0x01.com/~thanos/pso/tests/session/cgi/oopath/path1.py

    • For more control you can also overload the method CookieFileImpl.getPath
      # file: path2.py
      #
      #!/usr/bin/env python2.2
      
      from pso.service import ServiceHandler
      from pso import session 
      
      
      
      class MyFileImpl(session.CookieFileImpl): 
      	def getPath(self, reqHandler):
      		"""
      		you must return the full session file path excluding name.
      		It must be  valid, and the handler must have the 
      		authority to create or write to the given directory.
      		"""
      		return	"/tmp/path2/"
      
      .
      .
      .
      # Again remember __main__ to change:
      if __name__ == '__main__':
      	ServiceHandler()Run(testHandler, sessionImpl=MyFileImpl)
      

      test: http://www.0x01.com/~thanos/pso/tests/session/cgi/oopath/path2.py

    Storing the session in a database
    This is yet another OO example. Just create your own implementation class and mix it in with SessionImpl. The code below is a bit simplistic, but illustrates the basic idea, and it works!
    #file: mysql/mysqltest.py
    #
    #!/usr/bin/env python2.2
    
    from pso.service import ServiceHandler
    from pso import session
    
    
    import pickle 
    import base64
    from time import time
    import MySQL
    
    
    class MySQLLoader:
    	dbconn = MySQL.connect("localhost", "test")
    	dbconn.query('use test')
    	def load(self, reqHandler):
    		" must return the saved session obj or None. "
    		result = self.dbconn.query("SELECT sessionObj FROM session WHERE sessionId='%s';" %  self.getSessionId(reqHandler))
    		rows = result.fetchrows()
    		if rows:
    			sessionData = rows[0][0]
    			if sessionData:
    				return pickle.loads(base64.decodestring(sessionData))
    				return session
    
    	def save(self, reqHandler, session):
    		serviceId = self.getServiceId(reqHandler)
    		sessionId = self.getSessionId(reqHandler)
    		sessionStr = base64.encodestring(pickle.dumps(session,1))
    		res = self.dbconn.query("UPDATE session SET sessionObj='%s', serviceId='%s' WHERE sessionId='%s';" % (sessionStr,  serviceId, sessionId))
    		if res.affectedrows() == 0:
    			self.dbconn.query("INSERT INTO session (serviceId,sessionId, sessionObj) values ('%s','%s','%s')" % (serviceId, sessionId, sessionStr))
    
    	def newSessionId(self, reqHandler):
    			"returns a new id"
    			
    			try:
    				self.dbconn.query("LOCK TABLES session")
    				result = self.dbconn.query("SELECT count(sessionId) FROM session;")
    				rows = result.fetchrows()
    				if rows:
    					int(rows[0][0])
    			finally:
    				self.dbconn.query("UNLOCK TABLES session")
    				
    			return 0
    		
                             
    class MySQLImpl(session.CookieSession, MySQLLoader, session.SessionImpl): pass
                             
    .
    .
    
    .		
    if __name__ == '__main__':
    	ServiceHandler()Run(testHandler, sessionImpl=MySQLImpl)
    
    
    

    test: http://www.0x01.com/~thanos/pso/tests/session/cgi/oopath/mysql.py

    Doing things without cookies
    By default the sessionId is stored as a cookie. To match a request with a visitor you need to send an id to the visitor's browser and have it return on the next request. Cookies accomplish this. Another method is to embed in all the urls that require sessions a session id. The advantage is that if a visitor has blocked cookie acceptance, you can still operate session data between requests. The problem with this approach is that the session dies between visits. Probably the best is to use both approaches. Many sites including Amazon and hotmail do this, and it gives them the power to give you a pretty good service, with shopping cart and all, even when you have blocked their cookies. To make things easier pso.service.ServiceRequest.mkurl has been implemented.

    I want to set the session cookie's path and domain
    The easiest way is to set them (using SetEnv) in the the .htaccess or httpd.conf files. The following directives are recognized by the standard session implementation class:
    • PSOCookieSession_Comment is equivalent to Comment
    • PSOCookieSession_Domain is equivalent to Domain
    • PSOCookieSession_Max-Age is equivalent to Max-Age
    • PSOCookieSession_Path is equivalent to Path
    • PSOCookieSession_Secure is equivalent to Secure
    • PSOCookieSession_Version is equivalent to Version
    • PSOCookieSession_expires is equivalent to expires?... used by netscape et al.
    The above cookie attributes are defined in RCF2109, except for expires. expire takes a string date such as "Mon, 12 Nov 2002 13:04:56 GMT", as defines in RFC2068 section 3.3.1 [also RCF822 and RCF1123]

    What else can I do with pso
    • Using the pso system you can write one request handler that can be used as a cgi, as a script or invoked as a mod_python handler without modification.
    • serviceRequest.pso() returns a bridge to the various request handler implementations. It tries to simplify many typical web service actions, and abstract them from the server content delivery system. Methods are offered covering the following areas:
      • getting form input
      • browser HTTP header information
      • setting http headers
      • setting http cookies
      • file uploads
      • redirecting
      • setting return status
      • output
      • logging
      For more on this see the pso documentation.

      If we take our original quick example:

      #!/usr/bin/env python2.2
      from pso.service import ServiceHandler
      
      def testHandler(serviceRequest):
      	try:
      		serviceRequest.pso().session['reloads'] +=1
      	except:
      		serviceRequest.pso().session['reloads'] =1
      	print "Hello World!  ~ You reloaded: %(reloads)d times ~ Try Reload !" %  serviceRequest.pso().session
      
      if __name__ == '__main__':
      	ServiceHandler()Run(testHandler)
      

      Some simple changes can make it run as mod_python or as a cgi

      
      
      #!/usr/bin python2.2
      
      from pso.service import ServiceHandler, OK
      	
      
      def handler(serviceRequest):
      	serviceRequest.pso().send_http_header()
      	try:
      		serviceRequest.pso().session['visits'] +=1
      	except:
      		serviceRequest.pso().session['visits'] =1
      	print "Hello World!  ~ Your visit number: %(visits)d ~ Try Reload !" %  serviceRequest.pso()Session)
      	print "session: %s" %  serviceRequest.pso().session.__dict__
      	return OK
      if __name__ =='__main__':
      	ServiceHandler()Run(handler)
      
      The real differences are:
      1. With mod_python __main__ is not run.
      2. With mod_python the handler must return a result code, such as OK.
      3. With mode_python direct access to sys.stdout is not available. This is abstarcted by serviceRequest.pso().write
    For more on this see the mini HOW-TO: Writing one request handler for both CGI or mod_python. and its sister document Easy mod_python Session handling with pso.session.

  6. pso.session classes -- see http://www.scriptfoundry.com/modules/pso/

  7. Installation:

    pso should be installed using Distutils which comes standard with python 1.6. and up

    1. Download the distribution file at sourceforge.net
    2. Uncompress the distribution file
    3. Change into the directory created during the uncompression of distribution file
    4. execute the command:
      python setup.py install

  8. Installation Example
    1. create your project directory:
      $ mkdir ~/public_html/psotest
    2. Download and un-tar the latest pso package:
      $ wget  http://www.0x01.com/~thanos/pso/dist/current.tgz
      or for MS lovers: [ http://www.0x01.com/~thanos/pso/dist/current.exe]
      $ tar zvfx current.tgz
      	
    3. cd into the pso directory and then install it locally into your psotest directory.
      $ cd  pso-XX/
      $ python2.1 setup.py install --install-purelib=~/public_html/testpso
      
    4. Create a .htaccess file with the following:
      #.htaccess for testing pso using CGI
      #
      # Options ExecCGI directs apache to allow cgi's to be run from this directory.
      # AddHandler cgi-script  .py  treat any .py file as a script
      #SetEnv PSOServiceId MyPSOTest  names  session cookie as MyPSOtest
      
      Options ExecCGI Indexes 
      AddHandler cgi-script  .cgi 	
      SetEnv PSOServiceId MyPSOTest	
      
    5. Now we will create a test handler and check that it works, we will call the file test.py , but creat a link to it test.cgi. [We are doing this link, and setting the Options Indexes and AddHandler cgi-script .cgi for only one reason: to allow you to view and browser the source code from the web.]
      #!/usr/bin/env python2.1
      #
      from pso.service import ServiceHandler
      def testHandler(serviceRequest):
      	print "hello world"
      
      if __name__ == '__main__':
      	ServiceHandler().run(testHandler)
      
      Now we will set the file permissions, and do the link:
      $ chmod a+x  test.py
      $ ln test.py test.cgi
      
      and try it from the command line, should give you this:
      python:~/public_html/testpso# ./test.cgi
      set-cookie: SESSION_ID=@60123.0SESSION_ID;
      content-type: text/html
      
      hello world
      python:~/public_html/testpso# 
      
      Now lets try it using a browser: http://www.yourhost.com/~yourid/testpso/test.cgi

    see Greg Ward's http://www.python.org/doc/current/inst/inst.html If all else fails. Just copy the pso directory somewhere in the python PATH or to the directory of your request handler.

SourceForge Logo