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