| # |
| # Session - Itcl object for web session management for Rivet |
| # |
| # |
| |
| # Copyright 2004 The Apache Software Foundation |
| |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| |
| 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 |
| |
| # specifies whether cookies should only be sent over http connections |
| public variable cookieHttpOnly 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 |
| $dioObject registerSpecialField $sessionTable session_update_time NOW |
| $dioObject registerSpecialField $sessionTable session_start_time NOW |
| } |
| |
| 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 [::rivet::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 -hex -- $sessionIdKey] |
| } |
| |
| # |
| # do_garbage_collection - delete dead sessions from the session table. |
| # corresponding session table cache entries will automatically be |
| # deleted as well (assuming they've been defined with ON DELETE CASCADE) |
| # |
| method do_garbage_collection {} { |
| debug "do_garbage_collection: performing garbage collection" |
| # set result [$dioObject exec "delete from $sessionTable where timestamp 'now' - session_update_time > interval '$gcMaxLifetime seconds';"] |
| set del_cmd "delete from $sessionTable where " |
| append del_cmd [$dioObject makeDBFieldValue $sessionTable session_update_time now SECS] |
| append del_cmd " - [$dioObject makeDBFieldValue $sessionTable session_update_time {} SECS]" |
| append del_cmd " > $gcMaxLifetime" |
| debug "do_garbage_collection: > $del_cmd <" |
| set result [$dioObject exec $del_cmd] |
| $result destroy |
| } |
| |
| # |
| # consider_garbage_collection - perform a garbage collection gcProbability |
| # percent of the time. For example, if gcProbability is 1, about 1 in |
| # every 100 times this routine is called, garbage collection will be |
| # performed. |
| # |
| method consider_garbage_collection {} { |
| if {rand() <= $gcProbability / 100.0} { |
| do_garbage_collection |
| } |
| } |
| |
| # |
| # 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} { |
| ::rivet::cookie set $cookieName $value \ |
| -path $cookiePath \ |
| -minutes $cookieLifetime \ |
| -secure $cookieSecure \ |
| -HttpOnly $cookieHttpOnly |
| } |
| |
| # |
| # 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 [::rivet::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) [::rivet::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 [::rivet::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 |
| |
| regsub -all {\\} $data {\\\\} data |
| 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]} { |
| debug "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] |
| debug "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) |
| } |
| |
| # |
| # stash - given a packagename and a key-value dictionary of data |
| # the procedure calls method store to data into the cache table using |
| # the dictionary keys as column _key value |
| # |
| # |
| |
| method stash {packageName keyvalue_d} { |
| dict for {key value} $keyvalue_d { |
| $this store $packageName $key $value |
| } |
| } |
| |
| # |
| # load - given a package names returns a dictionary storing the key - value pairs for this session |
| # |
| # |
| method load {packageName} { |
| set package_d [dict create] |
| |
| $dioObject forall \ |
| "select key_,data from $sessionCacheTable where package_='$packageName' and session_id='[$this id]'" a { |
| dict set package_d $a(key_) $a(data) |
| } |
| |
| return $package_d |
| } |
| |
| # clear - given a package name and optionally a key it deletes rows in the cache for this session. |
| # when also the key value is specified only the row for that package-key pair is deleted |
| # |
| method clear {packageName {key ""}} { |
| |
| if {$key == ""} { |
| set keyval [list [$this id] $packageName] |
| set keyfield {session_id package_} |
| } else { |
| set keyval [list [$this id] $packageName $key] |
| set keyfield {session_id package_ key_} |
| } |
| |
| $dioObject delete $keyval -table $sessionCacheTable -keyfield $keyfield |
| } |
| |
| # |
| # 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 ""}} { |
| |
| set ip_address [::rivet::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 {} { |
| |
| ## Create their session by storing their session information in |
| # the session table. |
| set a(ip_address) [::rivet::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" |
| |
| # a small percentage of the time, try to delete stale session data |
| consider_garbage_collection |
| |
| 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_session - 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>" |
| flush $debugFile |
| } |
| } |
| } |
| |