# # cgi/session.rb - session support for cgi scripts # # Copyright (C) 2001 Yukihiro "Matz" Matsumoto # Copyright (C) 2000 Network Applied Communication Laboratory, Inc. # Copyright (C) 2000 Information-technology Promotion Agency, Japan # # Author: Yukihiro "Matz" Matsumoto # # Documentation: William Webber (william@williamwebber.com) # # == Overview # # This file provides the +CGI::Session+ class, which provides session # support for CGI scripts. A session is a sequence of HTTP requests # and responses linked together and associated with a single client. # Information associated with the session is stored # on the server between requests. A session id is passed between client # and server with every request and response, transparently # to the user. This adds state information to the otherwise stateless # HTTP request/response protocol. # # See the documentation to the +CGI::Session+ class for more details # and examples of usage. See cgi.rb for the +CGI+ class itself. require 'cgi' require 'tmpdir' class CGI # Class representing an HTTP session. See documentation for the file # cgi/session.rb for an introduction to HTTP sessions. # # == Lifecycle # # A CGI::Session instance is created from a CGI object. By default, # this CGI::Session instance will start a new session if none currently # exists, or continue the current session for this client if one does # exist. The +new_session+ option can be used to either always or # never create a new session. See #new() for more details. # # #delete() deletes a session from session storage. It # does not however remove the session id from the client. If the client # makes another request with the same id, the effect will be to start # a new session with the old session's id. # # == Setting and retrieving session data. # # The Session class associates data with a session as key-value pairs. # This data can be set and retrieved by indexing the Session instance # using '[]', much the same as hashes (although other hash methods # are not supported). # # When session processing has been completed for a request, the # session should be closed using the close() method. This will # store the session's state to persistent storage. If you want # to store the session's state to persistent storage without # finishing session processing for this request, call the update() # method. # # == Storing session state # # The caller can specify what form of storage to use for the session's # data with the +database_manager+ option to CGI::Session::new. The # following storage classes are provided as part of the standard library: # # CGI::Session::FileStore:: stores data as plain text in a flat file. Only # works with String data. This is the default # storage type. # CGI::Session::MemoryStore:: stores data in an in-memory hash. The data # only persists for as long as the current ruby # interpreter instance does. # CGI::Session::PStore:: stores data in Marshalled format. Provided by # cgi/session/pstore.rb. Supports data of any type, # and provides file-locking and transaction support. # # Custom storage types can also be created by defining a class with # the following methods: # # new(session, options) # restore # returns hash of session data. # update # close # delete # # Changing storage type mid-session does not work. Note in particular # that by default the FileStore and PStore session data files have the # same name. If your application switches from one to the other without # making sure that filenames will be different # and clients still have old sessions lying around in cookies, then # things will break nastily! # # == Maintaining the session id. # # Most session state is maintained on the server. However, a session # id must be passed backwards and forwards between client and server # to maintain a reference to this session state. # # The simplest way to do this is via cookies. The CGI::Session class # provides transparent support for session id communication via cookies # if the client has cookies enabled. # # If the client has cookies disabled, the session id must be included # as a parameter of all requests sent by the client to the server. The # CGI::Session class in conjunction with the CGI class will transparently # add the session id as a hidden input field to all forms generated # using the CGI#form() HTML generation method. No built-in support is # provided for other mechanisms, such as URL re-writing. The caller is # responsible for extracting the session id from the session_id # attribute and manually encoding it in URLs and adding it as a hidden # input to HTML forms created by other mechanisms. Also, session expiry # is not automatically handled. # # == Examples of use # # === Setting the user's name # # require 'cgi' # require 'cgi/session' # require 'cgi/session/pstore' # provides CGI::Session::PStore # # cgi = CGI.new("html4") # # session = CGI::Session.new(cgi, # 'database_manager' => CGI::Session::PStore, # use PStore # 'session_key' => '_rb_sess_id', # custom session key # 'session_expires' => Time.now + 30 * 60, # 30 minute timeout # 'prefix' => 'pstore_sid_') # PStore option # if cgi.has_key?('user_name') and cgi['user_name'] != '' # # coerce to String: cgi[] returns the # # string-like CGI::QueryExtension::Value # session['user_name'] = cgi['user_name'].to_s # elsif !session['user_name'] # session['user_name'] = "guest" # end # session.close # # === Creating a new session safely # # require 'cgi' # require 'cgi/session' # # cgi = CGI.new("html4") # # # We make sure to delete an old session if one exists, # # not just to free resources, but to prevent the session # # from being maliciously hijacked later on. # begin # session = CGI::Session.new(cgi, 'new_session' => false) # session.delete # rescue ArgumentError # if no old session # end # session = CGI::Session.new(cgi, 'new_session' => true) # session.close # class Session # The id of this session. attr_reader :session_id def Session::callback(dbman) #:nodoc: lambda{ dbman[0].close unless dbman.empty? } end # Create a new session id. # # The session id is an MD5 hash based upon the time, # a random number, and a constant string. This routine # is used internally for automatically generated # session ids. def Session::create_new_id require 'digest/md5' md5 = Digest::MD5::new now = Time::now md5.update(now.to_s) md5.update(String(now.usec)) md5.update(String(rand(0))) md5.update(String($$)) md5.update('foobar') md5.hexdigest[0,16] end # Create a new CGI::Session object for +request+. # # +request+ is an instance of the +CGI+ class (see cgi.rb). # +option+ is a hash of options for initialising this # CGI::Session instance. The following options are # recognised: # # session_key:: the parameter name used for the session id. # Defaults to '_session_id'. # session_id:: the session id to use. If not provided, then # it is retrieved from the +session_key+ parameter # of the request, or automatically generated for # a new session. # new_session:: if true, force creation of a new session. If not set, # a new session is only created if none currently # exists. If false, a new session is never created, # and if none currently exists and the +session_id+ # option is not set, an ArgumentError is raised. # database_manager:: the name of the class providing storage facilities # for session state persistence. Built-in support # is provided for +FileStore+ (the default), # +MemoryStore+, and +PStore+ (from # cgi/session/pstore.rb). See the documentation for # these classes for more details. # # The following options are also recognised, but only apply if the # session id is stored in a cookie. # # session_expires:: the time the current session expires, as a # +Time+ object. If not set, the session will terminate # when the user's browser is closed. # session_domain:: the hostname domain for which this session is valid. # If not set, defaults to the hostname of the server. # session_secure:: if +true+, this session will only work over HTTPS. # session_path:: the path for which this session applies. Defaults # to the directory of the CGI script. # # +option+ is also passed on to the session storage class initialiser; see # the documentation for each session storage class for the options # they support. # # The retrieved or created session is automatically added to +request+ # as a cookie, and also to its +output_hidden+ table, which is used # to add hidden input elements to forms. # # *WARNING* the +output_hidden+ # fields are surrounded by a
tag in HTML 4 generation, which # is _not_ invisible on many browsers; you may wish to disable the # use of fieldsets with code similar to the following # (see http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-list/37805) # # cgi = CGI.new("html4") # class << cgi # undef_method :fieldset # end # def initialize(request, option={}) session_key = option['session_key'] || '_session_id' id = option['session_id'] unless id if option['new_session'] id = Session::create_new_id end end unless id if request.key?(session_key) id = request[session_key] id = id.read if id.respond_to?(:read) end unless id id, = request.cookies[session_key] end unless id if option.key?('new_session') and not option['new_session'] raise ArgumentError, "session_key `%s' should be supplied"%session_key end id = Session::create_new_id end end @session_id = id dbman = option['database_manager'] || FileStore @dbman = dbman::new(self, option) request.instance_eval do @output_hidden = {session_key => id} @output_cookies = [ Cookie::new("name" => session_key, "value" => id, "expires" => option['session_expires'], "domain" => option['session_domain'], "secure" => option['session_secure'], "path" => if option['session_path'] then option['session_path'] elsif ENV["SCRIPT_NAME"] then File::dirname(ENV["SCRIPT_NAME"]) else "" end) ] end @dbprot = [@dbman] ObjectSpace::define_finalizer(self, Session::callback(@dbprot)) end # Retrieve the session data for key +key+. def [](key) unless @data @data = @dbman.restore end @data[key] end # Set the session date for key +key+. def []=(key, val) unless @write_lock @write_lock = true end unless @data @data = @dbman.restore end @data[key] = val end # Store session data on the server. For some session storage types, # this is a no-op. def update @dbman.update end # Store session data on the server and close the session storage. # For some session storage types, this is a no-op. def close @dbman.close @dbprot.clear end # Delete the session from storage. Also closes the storage. # # Note that the session's data is _not_ automatically deleted # upon the session expiring. def delete @dbman.delete @dbprot.clear end # File-based session storage class. # # Implements session storage as a flat file of 'key=value' values. # This storage type only works directly with String values; the # user is responsible for converting other types to Strings when # storing and from Strings when retrieving. class FileStore # Create a new FileStore instance. # # This constructor is used internally by CGI::Session. The # user does not generally need to call it directly. # # +session+ is the session for which this instance is being # created. The session id must only contain alphanumeric # characters; automatically generated session ids observe # this requirement. # # +option+ is a hash of options for the initialiser. The # following options are recognised: # # tmpdir:: the directory to use for storing the FileStore # file. Defaults to Dir::tmpdir (generally "/tmp" # on Unix systems). # prefix:: the prefix to add to the session id when generating # the filename for this session's FileStore file. # Defaults to the empty string. # # This session's FileStore file will be created if it does # not exist, or opened if it does. def initialize(session, option={}) dir = option['tmpdir'] || Dir::tmpdir prefix = option['prefix'] || '' id = session.session_id require 'digest/md5' md5 = Digest::MD5.hexdigest(id)[0,16] @path = dir+"/"+prefix+md5 unless File::exist? @path @hash = {} end end # Restore session state from the session's FileStore file. # # Returns the session state as a hash. def restore unless @hash @hash = {} begin f = File.open(@path, 'r') f.flock File::LOCK_SH for line in f line.chomp! k, v = line.split('=',2) @hash[CGI::unescape(k)] = CGI::unescape(v) end ensure f.close unless f.nil? end end @hash end # Save session state to the session's FileStore file. def update return unless @hash begin f = File.open(@path, File::CREAT|File::TRUNC|File::RDWR, 0600) f.flock File::LOCK_EX for k,v in @hash f.printf "%s=%s\n", CGI::escape(k), CGI::escape(String(v)) end ensure f.close unless f.nil? end end # Update and close the session's FileStore file. def close update end # Close and delete the session's FileStore file. def delete File::unlink @path rescue Errno::ENOENT end end # In-memory session storage class. # # Implements session storage as a global in-memory hash. Session # data will only persist for as long as the ruby interpreter # instance does. class MemoryStore GLOBAL_HASH_TABLE = {} #:nodoc: # Create a new MemoryStore instance. # # +session+ is the session this instance is associated with. # +option+ is a list of initialisation options. None are # currently recognised. def initialize(session, option=nil) @session_id = session.session_id GLOBAL_HASH_TABLE[@session_id] ||= {} end # Restore session state. # # Returns session data as a hash. def restore GLOBAL_HASH_TABLE[@session_id] end # Update session state. # # A no-op. def update # don't need to update; hash is shared end # Close session storage. # # A no-op. def close # don't need to close end # Delete the session state. def delete GLOBAL_HASH_TABLE.delete(@session_id) end end end end