summaryrefslogtreecommitdiff
path: root/lib/cgi/session.rb
diff options
context:
space:
mode:
Diffstat (limited to 'lib/cgi/session.rb')
-rw-r--r--lib/cgi/session.rb278
1 files changed, 157 insertions, 121 deletions
diff --git a/lib/cgi/session.rb b/lib/cgi/session.rb
index a6e999bc34..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,32 +158,33 @@ 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 'securerandom'
begin
+ # by OpenSSL, or system provided entropy pool
session_id = SecureRandom.hex(16)
rescue NotImplementedError
- require 'digest/md5'
- md5 = Digest::MD5::new
+ # never happens on modern systems
+ require 'digest'
+ d = Digest('SHA512').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')
- session_id = md5.hexdigest
+ 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
- @new_session = true
session_id
end
private :create_new_id
@@ -206,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+
@@ -221,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.
@@ -233,10 +229,10 @@ class CGI
# +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
@@ -254,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
@@ -281,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]
@@ -311,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
@@ -320,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
@@ -356,7 +356,7 @@ 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 initializer. The
# following options are recognised:
#
@@ -365,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.
@@ -373,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:
@@ -457,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"
@@ -472,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
+ 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
+
+ # 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
- # 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.
+ #
+ # A no-op.
def delete
- GLOBAL_HASH_TABLE.delete(@session_id)
end
end
end