require 'md5' require 'facets/more/synchash' require 'facets/more/times' require 'facets/more/expirable' require 'facets/core/class/cattr' require 'glue' require 'glue/configuration' 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. # # The session can be considered as a Hash where key-value # pairs are stored. Typically symbols are used as keys. By # convention uppercase symbols are used for internal Nitro # session variables (ie :FLASH, :USER, etc). User applications # typically use lowercase symbols (ie :cart, :history, etc). # #-- # TODO: rehash of the session cookie # TODO: store -> cache, reimplement helpers. #++ class Session < Hash include 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 type of the session cache. The generalized caching # system in Glue is used. The following options are available: # # * :memory [default] # * :drb # * :og # * :file # * :memcached setting :cache_type, :default => :memory, :doc => 'The type of session cache' # The address of the cache store. setting :cache_address, :default => '127.0.0.1', :doc => 'The address of the cache store' # The port of the cache store. setting :cache_port, :default => 9069, :doc => 'The port of the cache store' class << self # The sessions cache (store). attr_accessor :cache alias store cache # Load the correct Session specialization according to the # cache type. def setup(type = Session.cache_type) # gmosx: RDoc friendly. require 'nitro/session/' + type.to_s end # 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 # The number of active (online) sessions. # DON'T use yet! def count Session.cache.size end # Perform Session garbage collection. You may call this # method from a cron job. def garbage_collect expired = [] for s in Session.cache.all expired << s.session_id if s.expired? end for sid in expired Session.cache.delete(sid) end end alias_method :gc!, :garbage_collect # Returns the current session from the context thread local # variable. def current Context.current.session end end # 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??? Allow for pluggable hashes. #++ 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