require 'md5' require 'webrick' require 'facet/synchash' require 'facet/times' require 'glue' require 'glue/attribute' require 'glue/configuration' require 'glue/expirable' require 'nitro/cgi/cookie' module Nitro # A web application session. # # State is a neccessary evil but session variables # should be avoided as much as possible. Session state # is typically distributed to many servers so avoid # storing complete objects in session variables, only # store oids and small integer/strings. # # The session should be persistable to survive server # shutdowns. # #-- # TODO: rehash of the session cookie # TODO: store -> cache, reimplement helpers. #++ class Session < Hash include Glue::Expirable # Session id salt. setting :session_id_salt, :default => 'SALT', :doc => 'Session id salt' # The name of the cookie that stores the session id. setting :cookie_name, :default => 'nsid', :doc => 'The name of the cookie that stores the session id' # useful with persistents sessions stores setting :cookie_expires, :default => false, :doc => 'Set expires parameter of session cookie equal to the keepalive setting?' # The session keepalive time. The session is eligable for # garbage collection after this time passes. setting :keepalive, :default => 30.minutes, :doc => 'The session keepalive time' # The sessions cache (store). cattr_accessor :cache class << self # Set the session cache. The generalized caching system in # Glue is used. The following options are available: # # * :memory [default] # * :drb # * :og # * :file # * :memcached def cache_type=(cache_type) # gmosx: RDoc friendly. require 'nitro/session/' + cache_type.to_s end alias_method :set_cache_type, :cache_type= alias_method :store_type=, :cache_type= alias_method :set_store_type, :cache_type= # Lookup the session in the cache by using the session # cookie value as a key. If the session does not exist # creates a new session, inserts it in the cache and # appends a new session cookie in the response. def lookup(context) if session_id = context.cookies[Session.cookie_name] session = Session.cache[session_id] end unless session # Create new session. session = Session.new(context) cookie = Cookie.new(Session.cookie_name, session.session_id) if Session.cookie_expires cookie.expires = Time.now + Session.keepalive end context.add_cookie(cookie) Session.cache[session.session_id] = session else # Access ('touch') the existing session. session.touch! end return session end end # By default sessions are stored in memory. #-- # gmosx: should be placed here. #++ set_cache_type(:memory) # The unique id of this session. attr_reader :session_id # Create the session for the given context. # If the hook method 'created' is defined it is called # at the end. Typically used to initialize the session # hash. def initialize(context = nil) @session_id = create_id expires_after(Session.keepalive) created if respond_to?(:created) end # Synchronize the session store, by # restoring this session. Especially useful # in distributed and/or multiprocess setups. def sync Session.cache[@session_id] = self end alias_method :restore, :sync def touch! expires_after(Session.keepalive) end protected # Calculates a unique id. # # The session id must be unique, a monotonically # increasing function like time is appropriate. # Random may produce equal ids? add a prefix # (SALT) to stop hackers from creating session_ids. #-- # THINK: Is MD5 slow??? #++ def create_id now = Time.now md5 = Digest::MD5.new md5.update(now.to_s) md5.update(now.usec.to_s) md5.update(rand(0).to_s) md5.update(Session.session_id_salt) md5.hexdigest end end end # * George Moschovitis # * Guillaume Pierronnet