diff options
Diffstat (limited to 'lib/cgi/session.rb')
| -rw-r--r-- | lib/cgi/session.rb | 290 |
1 files changed, 166 insertions, 124 deletions
diff --git a/lib/cgi/session.rb b/lib/cgi/session.rb index 5568b28dce..fbb42986a4 100644 --- a/lib/cgi/session.rb +++ b/lib/cgi/session.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true # # cgi/session.rb - session support for cgi scripts # @@ -8,28 +9,22 @@ # 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. + # == 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. # # == Lifecycle # @@ -47,7 +42,7 @@ class CGI # == 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 + # 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). # @@ -60,21 +55,21 @@ class CGI # # == Storing session state # - # The caller can specify what form of storage to use for the session's + # 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 + # 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 + # 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, + # 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 + # Custom storage types can also be created by defining a class with # the following methods: # # new(session, options) @@ -99,14 +94,14 @@ class CGI # 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 + # 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. @@ -124,10 +119,10 @@ class CGI # 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 + # '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 + # # coerce to String: cgi[] returns the # # string-like CGI::QueryExtension::Value # session['user_name'] = cgi['user_name'].to_s # elsif !session['user_name'] @@ -143,11 +138,11 @@ class CGI # 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 + # # 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 + # 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) @@ -163,27 +158,34 @@ class CGI def Session::callback(dbman) #:nodoc: Proc.new{ - dbman[0].close unless dbman.empty? + 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. + # The session id is a secure random number by SecureRandom + # if possible, otherwise an SHA512 hash based upon the time, + # a random number, and a constant string. This routine is + # used internally for automatically generated session ids. def 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') - @new_session = true - md5.hexdigest + require 'securerandom' + begin + # by OpenSSL, or system provided entropy pool + session_id = SecureRandom.hex(16) + rescue NotImplementedError + # never happens on modern systems + require 'digest' + d = Digest('SHA512').new + now = Time::now + d.update(now.to_s) + d.update(String(now.usec)) + d.update(String(rand(0))) + d.update(String($$)) + d.update('foobar') + session_id = d.hexdigest[0, 32] + end + session_id end private :create_new_id @@ -200,7 +202,7 @@ class CGI # 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, + # 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+ @@ -215,7 +217,7 @@ class CGI # 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 + # 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. @@ -224,13 +226,13 @@ class CGI # 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 + # +option+ is also passed on to the session storage class initializer; 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. + # to add hidden input elements to forms. # # *WARNING* the +output_hidden+ # fields are surrounded by a <fieldset> tag in HTML 4 generation, which @@ -248,24 +250,26 @@ class CGI session_key = option['session_key'] || '_session_id' session_id = option['session_id'] unless session_id - if option['new_session'] - session_id = create_new_id - end + if option['new_session'] + session_id = create_new_id + @new_session = true + end end unless session_id - if request.key?(session_key) - session_id = request[session_key] - session_id = session_id.read if session_id.respond_to?(:read) - end - unless session_id - session_id, = request.cookies[session_key] - end - unless session_id - unless option.fetch('new_session', true) - raise ArgumentError, "session_key `%s' should be supplied"%session_key - end - session_id = create_new_id - end + if request.key?(session_key) + session_id = request[session_key] + session_id = session_id.read if session_id.respond_to?(:read) + end + unless session_id + session_id, = request.cookies[session_key] + end + unless session_id + unless option.fetch('new_session', true) + raise ArgumentError, "session_key `%s' should be supplied"%session_key + end + session_id = create_new_id + @new_session = true + end end @session_id = session_id dbman = option['database_manager'] || FileStore @@ -275,24 +279,26 @@ class CGI unless option.fetch('new_session', true) raise ArgumentError, "invalid session_id `%s'"%session_id end - session_id = @session_id = create_new_id + session_id = @session_id = create_new_id unless session_id + @new_session=true retry end request.instance_eval do - @output_hidden = {session_key => session_id} unless option['no_hidden'] - @output_cookies = [ + @output_hidden = {session_key => session_id} unless option['no_hidden'] + @output_cookies = [ Cookie::new("name" => session_key, - "value" => session_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) + "value" => session_id, + "expires" => option['session_expires'], + "domain" => option['session_domain'], + "secure" => option['session_secure'], + "path" => + if option['session_path'] + option['session_path'] + elsif ENV["SCRIPT_NAME"] + File::dirname(ENV["SCRIPT_NAME"]) + else + "" + end) ] unless option['no_cookies'] end @dbprot = [@dbman] @@ -305,7 +311,7 @@ class CGI @data[key] end - # Set the session date for key +key+. + # Set the session data for key +key+. def []=(key, val) @write_lock ||= true @data ||= @dbman.restore @@ -314,11 +320,11 @@ class CGI # Store session data on the server. For some session storage types, # this is a no-op. - def update + def update @dbman.update end - # Store session data on the server and close the session storage. + # 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 @@ -350,8 +356,8 @@ class CGI # 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 + # + # +option+ is a hash of options for the initializer. The # following options are recognised: # # tmpdir:: the directory to use for storing the FileStore @@ -359,7 +365,7 @@ class CGI # 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. + # Defaults to "cgi_sid_". # suffix:: the prefix to add to the session id when generating # the filename for this session's FileStore file. # Defaults to the empty string. @@ -367,82 +373,81 @@ class CGI # 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'] || '' - suffix = option['suffix'] || '' - id = session.session_id + dir = option['tmpdir'] || Dir::tmpdir + prefix = option['prefix'] || 'cgi_sid_' + suffix = option['suffix'] || '' + id = session.session_id require 'digest/md5' md5 = Digest::MD5.hexdigest(id)[0,16] - @path = dir+"/"+prefix+md5+suffix - if File::exist? @path - @hash = nil - else + @path = dir+"/"+prefix+md5+suffix + if File::exist? @path + @hash = nil + else unless session.new_session raise CGI::Session::NoSession, "uninitialized session" end - @hash = {} - end + @hash = {} + end end # Restore session state from the session's FileStore file. # # Returns the session state as a hash. def restore - unless @hash - @hash = {} + unless @hash + @hash = {} begin lockf = File.open(@path+".lock", "r") lockf.flock File::LOCK_SH - f = File.open(@path, 'r') - for line in f - line.chomp! - k, v = line.split('=',2) - @hash[CGI::unescape(k)] = CGI::unescape(v) - end + f = File.open(@path, 'r') + for line in f + line.chomp! + k, v = line.split('=',2) + @hash[CGI::unescape(k)] = Marshal.restore(CGI::unescape(v)) + end ensure - f.close unless f.nil? + f.close unless f.nil? lockf.close if lockf end - end - @hash + end + @hash end # Save session state to the session's FileStore file. def update - return unless @hash + return unless @hash begin lockf = File.open(@path+".lock", File::CREAT|File::RDWR, 0600) - lockf.flock File::LOCK_EX + lockf.flock File::LOCK_EX f = File.open(@path+".new", File::CREAT|File::TRUNC|File::WRONLY, 0600) - for k,v in @hash - f.printf "%s=%s\n", CGI::escape(k), CGI::escape(String(v)) - end + for k,v in @hash + f.printf "%s=%s\n", CGI::escape(k), CGI::escape(String(Marshal.dump(v))) + end f.close File.rename @path+".new", @path ensure - f.close if f and !f.closed? + f.close if f lockf.close if lockf end end # Update and close the session's FileStore file. def close - update + update end # Close and delete the session's FileStore file. def delete File::unlink @path+".lock" rescue nil File::unlink @path+".new" rescue nil - File::unlink @path - rescue Errno::ENOENT + File::unlink @path rescue nil 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 + # data will only persist for as long as the Ruby interpreter # instance does. class MemoryStore GLOBAL_HASH_TABLE = {} #:nodoc: @@ -451,9 +456,9 @@ class CGI # # +session+ is the session this instance is associated with. # +option+ is a list of initialisation options. None are - # currently recognised. + # currently recognized. def initialize(session, option=nil) - @session_id = session.session_id + @session_id = session.session_id unless GLOBAL_HASH_TABLE.key?(@session_id) unless session.new_session raise CGI::Session::NoSession, "uninitialized session" @@ -466,26 +471,63 @@ class CGI # # Returns session data as a hash. def restore - GLOBAL_HASH_TABLE[@session_id] + GLOBAL_HASH_TABLE[@session_id] end # Update session state. # # A no-op. def update - # don't need to update; hash is shared + # don't need to update; hash is shared end # Close session storage. # # A no-op. def close - # don't need to close + # don't need to close end # Delete the session state. def delete - GLOBAL_HASH_TABLE.delete(@session_id) + GLOBAL_HASH_TABLE.delete(@session_id) + end + end + + # Dummy session storage class. + # + # Implements session storage place holder. No actual storage + # will be done. + class NullStore + # Create a new NullStore 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) + end + + # Restore (empty) session state. + def restore + {} + end + + # Update session state. + # + # A no-op. + def update + end + + # Close session storage. + # + # A no-op. + def close + end + + # Delete the session state. + # + # A no-op. + def delete end end end |
