You are viewing a plain text version of this content. The canonical link for it is here.
Posted to rivet-dev@tcl.apache.org by ka...@apache.org on 2004/01/05 22:08:27 UTC

cvs commit: tcl-rivet/rivet/packages/session README.txt pkgIndex.tcl session-class.tcl session-create.sql session-demo.rvt session-drop.sql session-httpd.conf

karl        2004/01/05 13:08:27

  Added:       rivet/packages/session README.txt pkgIndex.tcl
                        session-class.tcl session-create.sql
                        session-demo.rvt session-drop.sql
                        session-httpd.conf
  Log:
  Commit of session management package.
  
  Revision  Changes    Path
  1.1                  tcl-rivet/rivet/packages/session/README.txt
  
  Index: README.txt
  ===================================================================
  
  $Id: README.txt,v 1.1 2004/01/05 21:08:26 karl Exp $
  
  INTRODUCTION
  
  This is session management code.  It provides an interface to allow you to
  generate and track a browser's visit as a "session", giving you a unique
  session ID and an interface for storing and retrieving data for that session
  on the server.
  
  This is an alpha/beta release -- documentation is not in final form, but
  everything you need should be in this file.
  
  Using sessions and their included ability to store and retrieve 
  session-related data on the server, programmers can generate more
  secure and higher-performance websites.  For example, hidden fields
  do not have to be included in forms (and the risk of them being manipulated
  by the user mitigated) since data that would be stored in hidden fields can 
  now be stored in the session cache on the server.  Forms are then
  faster since no hidden data is transmitted -- hidden fields must be
  sent twice, once in the form to the broswer and once in the response from it.
  
  Robust login systems, etc, can be built on top of this code.
  
  REQUIREMENTS
  
  Rivet.  Currently has only been tested with Postgresql.  All DB interfacing
  is done through DIO, though, so it should be relatively easy to add support 
  for other databases.
  
  PREPARING TO USE IT
  
  Create the tables in your SQL server.  With Postgres, do a "psql www" or
  whatever DB you connect as, then a backslash-i on session-create.sql
  
  (If you need to delete the tables, use session-drop.sql)
  
  The session code by default requires a DIO handle called DIO (the name of 
  which can be overridden).  We get it by doing a
  
      RivetServerConf ChildInitScript "package require DIO"
      RivetServerConf ChildInitScript "::DIO::handle Postgresql DIO -user www"
  
  EXAMPLE USAGE
  
  In your httpd.conf, add:
  
      RivetServerConf ChildInitScript "package require Session; Session SESSION"
  
  This tells Rivet you want to create a session object named SESSION in every
  child process Apache creates.
  
  You can configure the session at this point using numerous key-value pairs 
  (which are defined later in this doc).  Here's a quick example:
  
      RivetServerConf ChildInitScript "package require Session; Session SESSION -cookieLifetime 120 -debugMode 1"
  
  Turn debugging on (-debugMode 1) to figure out what's going on -- it's really 
  useful, if verbose.
  
  In your .rvt file, when you're generating the <HEAD> section:
  
      SESSION activate
  
  Activate handles everything for you with respect to creating new sessions, and
  for locating, validating, and updating existing sessions.  Activate will 
  either locate an existing session, or create a new one.  Sessions will 
  automatically be refreshed (their lifetimes extended) as additional requests 
  are received during the session, all under the control of the key-value pairs 
  controlling the session object.
  
  USING SESSIONS FROM YOUR CODE
  
  The main methods your code will use are:
  
      SESSION id - After doing a "SESSION activate", this will return a 32-byte 
  	ASCII-encoded random hexadecimal string.  Every time this browser 
  	comes to us with a request within the timeout period, this same string 
  	will be returned (assuming they have cookies enabled).
  
      SESSION is_new_session - returns 1 if it's a new session or 0 if it has
         previously existed (i.e. it's a zero if this request represents a 
         "return" or subsequent visit to a current session.)
  
      SESSION new_session_reason - this will return why this request is the first
  	request of a new session, either "no_cookie" saying the browser didn't 
  	give us a session cookie, "no_session" indicating we got a cookie but 
  	couldn't find it in our session table, or "timeout" where they had a 
  	cookie and we found the matching session but the session has timed out.
  
      SESSION store packageName key data - given the name of a package, a key,
  	and some data.  Stores the data in the rivet session cache table.
  
      SESSION fetch packageName key - given a package name and a key, return 
  	the data stored by the store method, or an empty string if none was 
  	set.  (Status is set to the DIO error that occurred, it can be fetched 
  	using the status method.)
  
  
  SESSION CONFIGURATION OPTIONS
  
  The following key-value pairs can be specified when a session object (like
  SESSION above) is created:
  
  sessionLifetime - how many seconds the session will live for.  7200 == 2 hours
  
  sessionRefreshInterval - if a request is processed for a browser that currently
  has a session and this long has elapsed since the session update time was
  last updated, update it.  900 == 15 minutes.  so if at least 15 minutes has
  elapsed and we've gotten a new request for a page, update the session update
  time, extending the session lifetime (sessions that are in use keep getting
  extended).
  
  cookieName - name of the cookie stored on the user's web browser
      default rivetSession
  
  dioObject - the name of the DIO object we'll use to access the database
  (default DIO)
  
  gcProbability - the probability that garbage collection will occur in percent.
  (default 1%, 1) (not coded yet)
  
  gcMaxLifetime - the number of seconds after which data will be seen as 
  "garbage" and cleaned up -- defaults to 1 day (86400) (not coded yet)
  
  refererCheck - the substring you want to check each HTTP referer for.  If 
  the referer was sent by the browser and the substring is not found, the 
  session will be deleted. (not coded yet)
  
  entropyFile - the name of a file that random binary data can be read from.
  ("/dev/urandom")  Data will be used from this file to help generate a
  super-hard-to-guess session ID.
  
  entropyLength - The number of bytes which will be read from the entropy file.
  If 0, the entropy file will not be read (default 0)
  
  scrambleCode -  Set the scramble code to something unique for the site or 
  your app or whatever, to slightly increase the unguessability of session ids
  (default "some random string")
  
  cookieLifetime - The lifetime of the cookie in minutes.  0 means until the 
  browser is closed (I think). (default 0)
  
  cookiePath - The webserver subpath that the session cookie applies to 
  (defaults to /)
  
  cookieDomain - The domain to set in the session cookie (not coded yet)
  
  cookieSecure - specifies whether the cookie should only be sent over secure 
  connections, 0 = any, 1 = secure connections only (default 0)
  
  sessionTable - the name of the table that session info will be stored in
  (default "rivet_session")
  
  sessionCacheTable - the name of the table that contains cached session data
  (default "rivet_session_cache")
  
  debugMode - Set debug mode to 1 to trace through and see the session object do 
  its thing (default 0)
  
  debugFile - the file handle that debugging messages will be written to
  (default stdout)
  
  
  SESSION METHODS
  
  The following methods can be invoked to find out stuff about the current
  session, store and fetch server data identified with this session, etc:
  
  SESSION status - return the status of the last operation
  
  SESSION id - get the session ID of the current browser.  Returns an
  empty string if there's no session (will not happen is SESSION activate
  has been issued.)
  
  SESSION new_session_reason - Returns the reason why there wasn't a previous
  session, either "no_cookie" saying the browser didn't give us a session
  cookie, "no_session" indicating we got a cookie but couldn't find it in
  the session table, or "timeout" when we had a cookie and a session but
  the session had timed out.
  
  SESSION store packageName key data
  
  Given a package name, a key string, and a data string, store the data in the 
  rivet session cache.
  
  SESSION fetch packageName key
  
  Given a package name and a key, return the data stored by the store method,
  or an empty string if none was set.  Status is set to the DIO error that
  occurred, it can be fetched using the status method.
  
  
  SESSION delete - given a user ID and looking at their IP address we inherited
  from the environment (thanks, Apache), remove them from the session
  table.  (the session table is how the server remembers stuff about sessions).
  
  If the session ID was not specified the current session is deleted.
  
  
  SESSION activate
  
  Find and validate the session ID if they have one.  If they don't have one or
  it isn't valid (timed out, etc), create a session and drop a cookie on them.
  
  
  GETTING ADDITIONAL RANDOMNESS FROM THE ENTROPY FILE
  
      RivetServerConf ChildInitScript "Session SESSION -entropyFile /dev/urandom -entropyLength 10 -debugMode 1"
  
  This options say we want to get randomness from an entropy file (random data 
  pseudo-device) of /dev/urandom, to get ten bytes of random data from that 
  entropy device, and to turn on debug mode, which will cause the SESSION object 
  to output all manner of debugging information as it does stuff.  This has
  been tested on FreeBSD and appears to work.
  
  
  
  
  1.1                  tcl-rivet/rivet/packages/session/pkgIndex.tcl
  
  Index: pkgIndex.tcl
  ===================================================================
  # Tcl package index file, version 1.1
  # This file is generated by the "pkg_mkIndex" command
  # and sourced either when an application starts up or
  # by a "package unknown" script.  It invokes the
  # "package ifneeded" command to set up package-related
  # information so that packages will be loaded automatically
  # in response to "package require" commands.  When this
  # script is sourced, the variable $dir must contain the
  # full path name of this file's directory.
  
  package ifneeded Session 1.0 [list source [file join $dir session-class.tcl]]
  
  
  
  1.1                  tcl-rivet/rivet/packages/session/session-class.tcl
  
  Index: session-class.tcl
  ===================================================================
  #
  # Session - Itcl object for web session management for Rivet
  #
  # $Id: session-class.tcl,v 1.1 2004/01/05 21:08:26 karl Exp $
  #
  
  package provide Session 1.0
  package require Itcl
  
  ::itcl::class Session {
      # true if the page being processed didn't have a previous session
      public variable isNewSession 1
  
      # contains the reason why this session is a new session, or "" if it isn't
      public variable newSessionReason ""
  
      # the routine that will handle saving data, could use DIO, could use
      # flatfiles, etc.
      public variable saveHandler ""
  
      # the name of the DIO object that we'll use to access the database
      public variable dioObject "DIO"
  
      # the name of the cookie used to set the session ID
      public variable cookieName "rivetSession"
  
      # the probability that garbage collection will occur in percent.
      public variable gcProbability 1
  
      # the number of seconds after which data will be seen as "garbage"
      # and cleaned up -- defaults to 1 day
      public variable gcMaxLifetime 86400
  
      # the substring you want to check each HTTP referer for.  If the
      # referer was sent by the browser and the substring is not found,
      # the session will be deleted.
      public variable refererCheck ""
  
      public variable entropyFile "/dev/urandom"
  
      # the number of bytes which will be read from the entropy file
      public variable entropyLength 0
  
      # set the scramble code to something unique for the site or the
      # app or whatever, to slightly increase the unguessability of
      # session ids
      public variable scrambleCode "some random string"
  
      # the lifetime of the cookie in minutes.  0 means until the browser
      # is closed.
      public variable cookieLifetime 0
  
      # the lifetime of the session in seconds.  this will be updated if
      # additional pages are fetched while the session is still alive.
      public variable sessionLifetime 7200
  
      # if a request is being processed, a session is active, and this many
      # seconds have elapsed since the session was created or the session
      # update time was last updated, the session update time will be updated
      # (the session being in use extends the session lifetime)
      public variable sessionRefreshInterval 900
  
      # the webserver subpath that the session cookie applies to -- defaults
      # to /
      public variable cookiePath "/"
  
      # the domain to set in the session cookie
      public variable cookieDomain ""
  
      # the status of the last operation, "" if ok
      public variable status
  
      # specifies whether cookies should only be sent over secure connections
      public variable cookieSecure 0
  
      # the name of the table that session info will be stored in
      public variable sessionTable "rivet_session"
  
      # the name of the table that contains cached session data
      public variable sessionCacheTable "rivet_session_cache"
  
      # the file that debug messages will be written to
      public variable debugFile stdout
  
      # set debug mode to 1 to trace through and see the session object
      # do its thing
      public variable debugMode 1
  
      constructor {args} {
  	eval configure $args
      }
  
      method status {args} {
  	if {$args == ""} {
  	    return $status
  	}
  	set status $args
      }
  
      # get_entropy_bytes - read entropyLength bytes from a random data
      # device, such as /dev/random or /dev/urandom, available on some
      # systems as a way to generate random data
      #
      # if entropyLength is 0 (the default) or entropyFile isn't defined
      # or doesn't open successfully, returns an empty string
      #
      method get_entropy_bytes {} {
  	if {$entropyLength == 0 || $entropyFile == ""} {
  	    return ""
  	}
  	if {[catch {open $entropyFile} fp] == 1} {
  	    return ""
  	}
  
  	set entropyBytes [read $fp $entropyLength]
  	close $fp
  	if {[binary scan $entropyBytes h* data]} {
  	    debug "get_entropy_bytes: returning '$data'"
  	    return $data
  	}
  	error "software bug - binary scan behaved unexpectedly"
      }
  
      #
      # gen_session_id - generate a session ID by md5'ing as many things
      # as we can get our hands on.
      #
      method gen_session_id {args} {
  	package require md5
  
  	# if the Apache unique ID module is installed, the environment
  	# variable UNIQUE_ID will have been set.  If not, we'll get an
  	# empty string, which won't hurt anything.
  	set uniqueID [env UNIQUE_ID]
  
  	set sessionIdKey "$uniqueID[clock clicks][pid]$args[clock seconds]$scrambleCode[get_entropy_bytes]"
  	debug "gen_session_id - feeding this to md5: '$sessionIdKey'"
  	return [::md5::md5 $sessionIdKey]
      }
  
      #
      # set_session_cookie - set a session cookie to the specified value --
      #  other cookie attributes are controlled by variables defined in the
      #  object
      #
      method set_session_cookie {value} {
  	cookie set $cookieName $value \
  	    -path $cookiePath \
  	    -minutes $cookieLifetime \
  	    -secure $cookieSecure
      }
  
      #
      # id - get the session ID of the current browser
      #
      # returns a session ID if their session cookie matches a current session.
      # returns an empty string if they do not have a session.
      #
      # status will be set to an empty string if all is ok, "timeout" if
      # the session had timed out, "no_cookie" if no cookie was previously
      # defined (session id could still be valid though -- first visit)
      #
      # ...caches the results in the info array to avoid calls to the database
      # in subsequent requests for the user ID from the same page, a common
      # occurrence.
      #
      method id {} {
  	::request::global sessionInfo
  
  	status ""
  
  	# if we already know the session ID, we're done.
  	# (i.e. we've already validated them earlier in the 
  	# handling of the current page.)
  
  	if {[info exists sessionInfo(sessionID)]} { 
  	    debug "id called, returning cached ID '$sessionInfo(sessionID)'"
  	    return $sessionInfo(sessionID) 
  	}
  
  	#
  	# see if they have a session cookie.  if they don't,
  	# set status and return.
  	#
  	set sessionCookie [cookie get $cookieName]
  	if {$sessionCookie == ""} {
  	    # they did not have a cookie set, they are not logged in
  	    status "no_cookie"
  	    debug "id: no session cookie '$cookieName'"
  	    return ""
  	}
  
  	# there is a session Cookie, grab the remote address of the connection,
  	# see if our state table says he has logged into us from this
  	# address within our login timeout window and we've given him
  	# this session
  
  	debug "id: found session cookie '$cookieName' value '$sessionCookie'"
  
  	set a(session_id) $sessionCookie
  	set a(ip_address) [env REMOTE_ADDR]
  
  	# see if there's a record matching the session ID cookie and
  	# IP address
  	set kf [list session_id ip_address]
  	set key [$dioObject makekey a $kf]
  	if {![$dioObject fetch $key a -table $sessionTable -keyfield $kf]} {
  	    debug "id: no entry in the session table for session '$sessionCookie' and address [env REMOTE_ADDR]: [$dioObject errorinfo]"
  	    status "no_session"
  	    return ""
  	}
  
  	## Carve the seconds out of the session_update_time field in the
  	# $sessionTable table.  Trim off the timezone at the end.
  	set secs [clock scan [string range $a(session_update_time) 0 18]]
  
  	# if the session has timed out, delete the session and return -1
  
  	if {[expr $secs + $sessionLifetime] < [clock seconds]} {
  	    $dioObject delete $key -table $sessionTable -keyfield $kf
  	    debug "id: session '$sessionCookie' timed out"
  	    status "timeout"
  	    return ""
  	}
  
  	# Their session is still alive.  If the session refresh 
  	# interval time has expired, update the session update time in the 
  	# database (we don't update every time they request a page for 
  	# performance reasons)  The idea is it's been at least 15 minutes or 
  	# something like that since they've logged in, and they're still 
  	# doing stuff, so reset their session update time to now
  
  	if {[expr $secs + $sessionRefreshInterval] < [clock seconds]} {
  	    debug "session '$sessionCookie' alive, refreshing session update time"
  	    set a(session_update_time) now
  	    if {![$dioObject store a -table $sessionTable -keyfield $kf]} {
  		debug "id: Failed to store $sessionTable: [$dioObject errorinfo]"
  		puts "Failed to store $sessionTable: [$dioObject errorinfo]"
  	    }
  	}
  
  	#
  	# THEY VALIDATED.  Cache the session ID in the sessionInfo array
  	# that will only exist for the handling of this request, set that
  	# this is not a new session (at least one previous request has been
  	# handled with this session ID) and return the session ID
  	#
  	debug "id: active session, '$a(session_id)'"
  	set sessionInfo(sessionID) $a(session_id)
  	set isNewSession 0
  	return $a(session_id)
      }
  
      #
      # store - given a package name, a key string, and a data string,
      #  store the data in the rivet session cache
      #
      method store {packageName key data} {
  	set a(session_id) [id]
  	set a(package) $packageName
  	set a(key) $key
  	set a(data) $data
  
  	debug "store session data, package '$packageName', key '$key', data '$data'"
  	set kf [list session_id package key]
  
  	if {![$dioObject store a -table $sessionCacheTable -keyfield $kf]} {
  	    puts "Failed to store $sessionCacheTable '$kf'"
  	    parray a
  	    error [$dioObject errorinfo]
  	}
      }
  
      #
      # fetch - given a package name and a key, return the data stored
      #   for this session
      #
      method fetch {packageName key} {
  	set kf [list session_id package key]
  
  	set a(session_id) [id]
  	set a(package) $packageName
  	set a(key) $key
  
  	set key [$dioObject makekey a $kf]
  	if {![$dioObject fetch $key a -table $sessionCacheTable -keyfield $kf]} {
  	    status [$dioObject errorinfo]
  	    puts "error: [$dioObject errorinfo]"
  	    debug "fetch session data failed, package '$packageName', key '$key', error '[$dioObject errorinfo]'"
  	    return ""
  	}
  
  	debug "fetch session data succeeded, package '$packageName', key '$key', result '$a(data)'"
  
  	return $a(data)
      }
  
      #
      # delete - given a user ID and looking at their IP address we inherited
      # from the environment (thanks, webserver), remove them from the session
      # table.  (the session table is how the server remembers stuff about
      # sessions)
      #
      method delete_session {{session_id ""}} {
  	variable conf
  
  	set ip_address [env REMOTE_ADDR]
  
  	if {$session_id == ""} {
  	    set session_id [id]
  	}
  
  	debug "delete session $session_id"
  
  	set kf [list session_id ip_address]
  	$dioObject delete [list $session_id $ip_address] -table $sessionTable -keyfield $kf
  
  	## NEED TO delete saved session data here too, from the
  	# $sessionCacheTable structure.
      }
  
      #
      # create_session - Generate a session ID and store the session in the
      #  session table.
      #
      # returns the session_id
      #
      method create_session {} {
  	global conf
  
  	## Create their session by storing their session information in 
  	# the session table.
  	set a(ip_address) [env REMOTE_ADDR]
  	set a(session_start_time) now
  	set a(session_update_time) now
  
  	set a(session_id) [gen_session_id $a(ip_address)]
  
  	set kf [list ip_address session_id]
  	if {![$dioObject store a -table $sessionTable -keyfield $kf]} {
  	    debug "Failed to store $sessionTable: [$dioObject errorinfo]"
  	    puts "Failed to store $sessionTable: [$dioObject errorinfo]"
  	}
  
  	debug "create_session: ip $a(ip_address), id '$a(session_id)'"
  
  	return $a(session_id)
      }
  
      #
      # activate - find the session ID if they have one.  if they don't, create
      # one and drop a cookie on them.
      #
      method activate {} {
  	::request::global sessionInfo
  
  	debug "activate: checking out the situation"
  	set id [id]
  	if {$id != ""} {
  	    debug "activate: returning session id '$id'"
  	    return $id
  	}
  
  	# it's a new session, save the reason for why it's a new session,
  	# set that it's a new session, drop a session cookie on the browser
  	# that issued this request, set the session ID cache variable, and 
  	# return the cookie ID
  	set newSessionReason [status]
  	debug "activate: new session, reason '$newSessionReason'"
  	set id [create_session]
  	set isNewSession 1
  	set_session_cookie $id
  	set sessionInfo(sessionID) $id
  	debug "activate: created session '$id' and set cookie (theoretically)"
  	return $id
      }
  
      #
      # is_new_sesion - return a 1 if it's a new session, else a zero if there
      # were one or more prior pages creating and/or using this session ID
      #
      method is_new_session {} {
  	return $isNewSession
      }
  
      #
      # new_session_reason - return the reason why a session is new, either
      # it didn't have a cookie "no_cookie", there was a cookie but no
      # matching session "no_session", or there was a cookie and a session
      # but the session has timed out "timeout".  if the session isn't new,
      # returns ""
      #
      method new_session_reason {} {
  	return $newSessionReason
      }
  
      #
      # debug - output a debugging message
      #
      method debug {message} {
  	if {$debugMode} {
  	    puts $debugFile "$this (debug) $message<br>"
  	}
      }
  }
  
  
  
  
  1.1                  tcl-rivet/rivet/packages/session/session-create.sql
  
  Index: session-create.sql
  ===================================================================
  --
  -- Define SQL tables for session management code
  --
  -- $Id: session-create.sql,v 1.1 2004/01/05 21:08:26 karl Exp $
  --
  --
  
  create table rivet_session(
      ip_address		inet,
      session_start_time	timestamp,
      session_update_time	timestamp,
      session_id		varchar,
  
      UNIQUE( session_id )
  );
  
  create table rivet_session_cache(
      session_id		varchar REFERENCES rivet_session(session_id) ON DELETE CASCADE,
      package		varchar,
      key                 varchar,
      data                varchar,
  
      UNIQUE( session_id, package, key )
  );
  create index rivet_session_cache_idx ON rivet_session_cache( session_id );
  
  
  
  
  1.1                  tcl-rivet/rivet/packages/session/session-demo.rvt
  
  Index: session-demo.rvt
  ===================================================================
  <h1>Session Demo</h1>
  <?
  SESSION activate
  
  puts "<p>Here's some stuff about your session:"
  puts "<ul>"
  puts "<li>Your session ID is [SESSION id]"
  puts "<li>SESSION is_new_session -> [SESSION is_new_session]"
  puts "<li>SESSION new_session_reason -> [SESSION new_session_reason]"
  puts "</ul>"
  ?>
  <p>Fetching key 'foo' from package 'dummy':
  <?  set data [SESSION fetch dummy foo]
  
  puts "Fetch status is '[SESSION status]'<br>"
  
      if {$data == ""} {
  	puts "No data.  Storing data 'bar' with key 'foo' package 'dummy'.<p>"
  
  	SESSION store dummy foo bar
  
      } else {
  	puts "<b>$data</b>"
      }
  
  ?>
  
  
  
  
  1.1                  tcl-rivet/rivet/packages/session/session-drop.sql
  
  Index: session-drop.sql
  ===================================================================
  --
  -- Drop session management SQL tables
  --
  -- $Id: session-drop.sql,v 1.1 2004/01/05 21:08:26 karl Exp $
  --
  --
  
  drop table rivet_session_cache;
  drop table rivet_session;
  
  
  
  
  1.1                  tcl-rivet/rivet/packages/session/session-httpd.conf
  
  Index: session-httpd.conf
  ===================================================================
  ##
  ## session-httpd.conf -- include in for from an Apache httpd.conf file
  ##  to pick up 
  ##
  ## Include file
  ##
  ## $Id: session-httpd.conf,v 1.1 2004/01/05 21:08:26 karl Exp $
  ##
  
  <IfModule mod_rivet.c>
      RivetServerConf ChildInitScript "source /opt/local/rivet/packages/sc-session/session-class.tcl; Session SESSION -entropyFile /dev/urandom -entropyLength 10 -debugMode 1"
      <Directory "/usr/local/www/data">
          RivetDirConf BeforeScript "SESSION activate"
      </Directory>
  
  </IfModule>
  
  
  
  

---------------------------------------------------------------------
To unsubscribe, e-mail: rivet-cvs-unsubscribe@tcl.apache.org
For additional commands, e-mail: rivet-cvs-help@tcl.apache.org