Switch to Rack based session stores.

This commit is contained in:
Joshua Peek 2008-12-15 16:33:31 -06:00
parent e8c1915416
commit ed70830713
31 changed files with 1159 additions and 1831 deletions

@ -89,20 +89,17 @@ module Http
autoload :Headers, 'action_controller/headers' autoload :Headers, 'action_controller/headers'
end end
module Session
autoload :AbstractStore, 'action_controller/session/abstract_store'
autoload :CookieStore, 'action_controller/session/cookie_store'
autoload :MemCacheStore, 'action_controller/session/mem_cache_store'
end
# DEPRECATE: Remove CGI support # DEPRECATE: Remove CGI support
autoload :CgiRequest, 'action_controller/cgi_process' autoload :CgiRequest, 'action_controller/cgi_process'
autoload :CGIHandler, 'action_controller/cgi_process' autoload :CGIHandler, 'action_controller/cgi_process'
end end
class CGI
class Session
autoload :ActiveRecordStore, 'action_controller/session/active_record_store'
autoload :CookieStore, 'action_controller/session/cookie_store'
autoload :DRbStore, 'action_controller/session/drb_store'
autoload :MemCacheStore, 'action_controller/session/mem_cache_store'
end
end
autoload :Mime, 'action_controller/mime_type' autoload :Mime, 'action_controller/mime_type'
autoload :HTML, 'action_controller/vendor/html-scanner' autoload :HTML, 'action_controller/vendor/html-scanner'

@ -164,8 +164,8 @@ class UnknownHttpMethod < ActionControllerError #:nodoc:
# #
# Other options for session storage are: # Other options for session storage are:
# #
# * ActiveRecordStore - Sessions are stored in your database, which works better than PStore with multiple app servers and, # * ActiveRecord::SessionStore - Sessions are stored in your database, which works better than PStore with multiple app servers and,
# unlike CookieStore, hides your session contents from the user. To use ActiveRecordStore, set # unlike CookieStore, hides your session contents from the user. To use ActiveRecord::SessionStore, set
# #
# config.action_controller.session_store = :active_record_store # config.action_controller.session_store = :active_record_store
# #
@ -1216,7 +1216,6 @@ def initialize_current_url
def log_processing def log_processing
if logger && logger.info? if logger && logger.info?
log_processing_for_request_id log_processing_for_request_id
log_processing_for_session_id
log_processing_for_parameters log_processing_for_parameters
end end
end end
@ -1229,13 +1228,6 @@ def log_processing_for_request_id
logger.info(request_id) logger.info(request_id)
end end
def log_processing_for_session_id
if @_session && @_session.respond_to?(:session_id) && @_session.respond_to?(:dbman) &&
!@_session.dbman.is_a?(CGI::Session::CookieStore)
logger.info " Session ID: #{@_session.session_id}"
end
end
def log_processing_for_parameters def log_processing_for_parameters
parameters = respond_to?(:filter_parameters) ? filter_parameters(params) : params.dup parameters = respond_to?(:filter_parameters) ? filter_parameters(params) : params.dup
parameters = parameters.except!(:controller, :action, :format, :_method) parameters = parameters.except!(:controller, :action, :format, :_method)

@ -1,7 +1,6 @@
require 'action_controller/cgi_ext/stdinput' require 'action_controller/cgi_ext/stdinput'
require 'action_controller/cgi_ext/query_extension' require 'action_controller/cgi_ext/query_extension'
require 'action_controller/cgi_ext/cookie' require 'action_controller/cgi_ext/cookie'
require 'action_controller/cgi_ext/session'
class CGI #:nodoc: class CGI #:nodoc:
include ActionController::CgiExt::Stdinput include ActionController::CgiExt::Stdinput

@ -1,53 +0,0 @@
require 'digest/md5'
require 'cgi/session'
require 'cgi/session/pstore'
class CGI #:nodoc:
# * Expose the CGI instance to session stores.
# * Don't require 'digest/md5' whenever a new session id is generated.
class Session #:nodoc:
def self.generate_unique_id(constant = nil)
ActiveSupport::SecureRandom.hex(16)
end
# Make the CGI instance available to session stores.
attr_reader :cgi
attr_reader :dbman
alias_method :initialize_without_cgi_reader, :initialize
def initialize(cgi, options = {})
@cgi = cgi
initialize_without_cgi_reader(cgi, options)
end
private
# Create a new session id.
def create_new_id
@new_session = true
self.class.generate_unique_id
end
# * Don't require 'digest/md5' whenever a new session is started.
class PStore #:nodoc:
def initialize(session, option={})
dir = option['tmpdir'] || Dir::tmpdir
prefix = option['prefix'] || ''
id = session.session_id
md5 = Digest::MD5.hexdigest(id)[0,16]
path = dir+"/"+prefix+md5
path.untaint
if File::exist?(path)
@hash = nil
else
unless session.new_session
raise CGI::Session::NoSession, "uninitialized session"
end
@hash = {}
end
@p = ::PStore.new(path)
@p.transaction do |p|
File.chmod(0600, p.path)
end
end
end
end
end

@ -61,7 +61,7 @@ def self.dispatch_cgi(app, cgi, out = $stdout)
class CgiRequest #:nodoc: class CgiRequest #:nodoc:
DEFAULT_SESSION_OPTIONS = { DEFAULT_SESSION_OPTIONS = {
:database_manager => CGI::Session::CookieStore, :database_manager => nil,
:prefix => "ruby_sess.", :prefix => "ruby_sess.",
:session_path => "/", :session_path => "/",
:session_key => "_session_id", :session_key => "_session_id",

@ -45,8 +45,10 @@ def to_prepare(identifier = nil, &block)
end end
cattr_accessor :middleware cattr_accessor :middleware
self.middleware = MiddlewareStack.new self.middleware = MiddlewareStack.new do |middleware|
self.middleware.use "ActionController::Failsafe" middleware.use "ActionController::Failsafe"
middleware.use "ActionController::SessionManagement::Middleware"
end
include ActiveSupport::Callbacks include ActiveSupport::Callbacks
define_callbacks :prepare_dispatch, :before_dispatch, :after_dispatch define_callbacks :prepare_dispatch, :before_dispatch, :after_dispatch
@ -89,7 +91,7 @@ def call(env)
def _call(env) def _call(env)
@request = RackRequest.new(env) @request = RackRequest.new(env)
@response = RackResponse.new(@request) @response = RackResponse.new
dispatch dispatch
end end

@ -489,8 +489,8 @@ def reset!
# By default, a single session is automatically created for you, but you # By default, a single session is automatically created for you, but you
# can use this method to open multiple sessions that ought to be tested # can use this method to open multiple sessions that ought to be tested
# simultaneously. # simultaneously.
def open_session def open_session(application = nil)
application = ActionController::Dispatcher.new application ||= ActionController::Dispatcher.new
session = Integration::Session.new(application) session = Integration::Session.new(application)
# delegate the fixture accessors back to the test instance # delegate the fixture accessors back to the test instance

@ -4,7 +4,12 @@ class Middleware
attr_reader :klass, :args, :block attr_reader :klass, :args, :block
def initialize(klass, *args, &block) def initialize(klass, *args, &block)
@klass = klass.is_a?(Class) ? klass : klass.to_s.constantize if klass.is_a?(Class)
@klass = klass
else
@klass = klass.to_s.constantize
end
@args = args @args = args
@block = block @block = block
end end
@ -21,18 +26,28 @@ def ==(middleware)
end end
def inspect def inspect
str = @klass.to_s str = klass.to_s
@args.each { |arg| str += ", #{arg.inspect}" } args.each { |arg| str += ", #{arg.inspect}" }
str str
end end
def build(app) def build(app)
klass.new(app, *args, &block) if block
klass.new(app, *args, &block)
else
klass.new(app, *args)
end
end end
end end
def initialize(*args, &block)
super(*args)
block.call(self) if block_given?
end
def use(*args, &block) def use(*args, &block)
push(Middleware.new(*args, &block)) middleware = Middleware.new(*args, &block)
push(middleware)
end end
def build(app) def build(app)

@ -3,24 +3,12 @@
module ActionController #:nodoc: module ActionController #:nodoc:
class RackRequest < AbstractRequest #:nodoc: class RackRequest < AbstractRequest #:nodoc:
attr_accessor :session_options attr_accessor :session_options
attr_reader :cgi
class SessionFixationAttempt < StandardError #:nodoc: class SessionFixationAttempt < StandardError #:nodoc:
end end
DEFAULT_SESSION_OPTIONS = { def initialize(env)
:database_manager => CGI::Session::CookieStore, # store data in cookie
:prefix => "ruby_sess.", # prefix session file names
:session_path => "/", # available to all paths in app
:session_key => "_session_id",
:cookie_only => true,
:session_http_only=> true
}
def initialize(env, session_options = DEFAULT_SESSION_OPTIONS)
@session_options = session_options
@env = env @env = env
@cgi = CGIWrapper.new(self)
super() super()
end end
@ -66,87 +54,25 @@ def server_software
@env['SERVER_SOFTWARE'].split("/").first @env['SERVER_SOFTWARE'].split("/").first
end end
def session_options
@env['rack.session.options'] ||= {}
end
def session_options=(options)
@env['rack.session.options'] = options
end
def session def session
unless defined?(@session) @env['rack.session'] ||= {}
if @session_options == false
@session = Hash.new
else
stale_session_check! do
if cookie_only? && query_parameters[session_options_with_string_keys['session_key']]
raise SessionFixationAttempt
end
case value = session_options_with_string_keys['new_session']
when true
@session = new_session
when false
begin
@session = CGI::Session.new(@cgi, session_options_with_string_keys)
# CGI::Session raises ArgumentError if 'new_session' == false
# and no session cookie or query param is present.
rescue ArgumentError
@session = Hash.new
end
when nil
@session = CGI::Session.new(@cgi, session_options_with_string_keys)
else
raise ArgumentError, "Invalid new_session option: #{value}"
end
@session['__valid_session']
end
end
end
@session
end end
def reset_session def reset_session
@session.delete if defined?(@session) && @session.is_a?(CGI::Session) @env['rack.session'] = {}
@session = new_session
end end
private
# Delete an old session if it exists then create a new one.
def new_session
if @session_options == false
Hash.new
else
CGI::Session.new(@cgi, session_options_with_string_keys.merge("new_session" => false)).delete rescue nil
CGI::Session.new(@cgi, session_options_with_string_keys.merge("new_session" => true))
end
end
def cookie_only?
session_options_with_string_keys['cookie_only']
end
def stale_session_check!
yield
rescue ArgumentError => argument_error
if argument_error.message =~ %r{undefined class/module ([\w:]*\w)}
begin
# Note that the regexp does not allow $1 to end with a ':'
$1.constantize
rescue LoadError, NameError => const_error
raise ActionController::SessionRestoreError, <<-end_msg
Session contains objects whose class definition isn\'t available.
Remember to require the classes for all objects kept in the session.
(Original exception: #{const_error.message} [#{const_error.class}])
end_msg
end
retry
else
raise
end
end
def session_options_with_string_keys
@session_options_with_string_keys ||= DEFAULT_SESSION_OPTIONS.merge(@session_options).stringify_keys
end
end end
class RackResponse < AbstractResponse #:nodoc: class RackResponse < AbstractResponse #:nodoc:
def initialize(request) def initialize
@cgi = request.cgi
@writer = lambda { |x| @body << x } @writer = lambda { |x| @body << x }
@block = nil @block = nil
super() super()
@ -247,49 +173,8 @@ def set_cookies!
else cookies << cookie.to_s else cookies << cookie.to_s
end end
@cgi.output_cookies.each { |c| cookies << c.to_s } if @cgi.output_cookies
headers['Set-Cookie'] = [headers['Set-Cookie'], cookies].flatten.compact headers['Set-Cookie'] = [headers['Set-Cookie'], cookies].flatten.compact
end end
end end
end end
class CGIWrapper < ::CGI
attr_reader :output_cookies
def initialize(request, *args)
@request = request
@args = *args
@input = request.body
super *args
end
def params
@params ||= @request.params
end
def cookies
@request.cookies
end
def query_string
@request.query_string
end
# Used to wrap the normal args variable used inside CGI.
def args
@args
end
# Used to wrap the normal env_table variable used inside CGI.
def env_table
@request.env
end
# Used to wrap the normal stdinput variable used inside CGI.
def stdinput
@input
end
end
end end

@ -0,0 +1,129 @@
require 'rack/utils'
module ActionController
module Session
class AbstractStore
ENV_SESSION_KEY = 'rack.session'.freeze
ENV_SESSION_OPTIONS_KEY = 'rack.session.options'.freeze
HTTP_COOKIE = 'HTTP_COOKIE'.freeze
SET_COOKIE = 'Set-Cookie'.freeze
class SessionHash < Hash
def initialize(by, env)
@by = by
@env = env
@loaded = false
end
def id
load! unless @loaded
@id
end
def [](key)
load! unless @loaded
super
end
def []=(key, value)
load! unless @loaded
super
end
def to_hash
{}.replace(self)
end
private
def load!
@id, session = @by.send(:load_session, @env)
replace(session)
@loaded = true
end
end
DEFAULT_OPTIONS = {
:key => 'rack.session',
:path => '/',
:domain => nil,
:expire_after => nil,
:secure => false,
:httponly => true,
:cookie_only => true
}
def initialize(app, options = {})
@app = app
@default_options = DEFAULT_OPTIONS.merge(options)
@key = @default_options[:key]
@cookie_only = @default_options[:cookie_only]
end
def call(env)
session = SessionHash.new(self, env)
original_session = session.dup
env[ENV_SESSION_KEY] = session
env[ENV_SESSION_OPTIONS_KEY] = @default_options.dup
response = @app.call(env)
session = env[ENV_SESSION_KEY]
unless session == original_session
options = env[ENV_SESSION_OPTIONS_KEY]
sid = session.id
unless set_session(env, sid, session.to_hash)
return response
end
cookie = Rack::Utils.escape(@key) + '=' + Rack::Utils.escape(sid)
cookie << "; domain=#{options[:domain]}" if options[:domain]
cookie << "; path=#{options[:path]}" if options[:path]
if options[:expire_after]
expiry = Time.now + options[:expire_after]
cookie << "; expires=#{expiry.httpdate}"
end
cookie << "; Secure" if options[:secure]
cookie << "; HttpOnly" if options[:httponly]
headers = response[1]
case a = headers[SET_COOKIE]
when Array
a << cookie
when String
headers[SET_COOKIE] = [a, cookie]
when nil
headers[SET_COOKIE] = cookie
end
end
response
end
private
def generate_sid
ActiveSupport::SecureRandom.hex(16)
end
def load_session(env)
request = Rack::Request.new(env)
sid = request.cookies[@key]
unless @cookie_only
sid ||= request.params[@key]
end
sid, session = get_session(env, sid)
[sid, session]
end
def get_session(env, sid)
raise '#get_session needs to be implemented.'
end
def set_session(env, sid, session_data)
raise '#set_session needs to be implemented.'
end
end
end
end

@ -1,350 +0,0 @@
require 'cgi'
require 'cgi/session'
require 'digest/md5'
class CGI
class Session
attr_reader :data
# Return this session's underlying Session instance. Useful for the DB-backed session stores.
def model
@dbman.model if @dbman
end
# A session store backed by an Active Record class. A default class is
# provided, but any object duck-typing to an Active Record Session class
# with text +session_id+ and +data+ attributes is sufficient.
#
# The default assumes a +sessions+ tables with columns:
# +id+ (numeric primary key),
# +session_id+ (text, or longtext if your session data exceeds 65K), and
# +data+ (text or longtext; careful if your session data exceeds 65KB).
# The +session_id+ column should always be indexed for speedy lookups.
# Session data is marshaled to the +data+ column in Base64 format.
# If the data you write is larger than the column's size limit,
# ActionController::SessionOverflowError will be raised.
#
# You may configure the table name, primary key, and data column.
# For example, at the end of <tt>config/environment.rb</tt>:
# CGI::Session::ActiveRecordStore::Session.table_name = 'legacy_session_table'
# CGI::Session::ActiveRecordStore::Session.primary_key = 'session_id'
# CGI::Session::ActiveRecordStore::Session.data_column_name = 'legacy_session_data'
# Note that setting the primary key to the +session_id+ frees you from
# having a separate +id+ column if you don't want it. However, you must
# set <tt>session.model.id = session.session_id</tt> by hand! A before filter
# on ApplicationController is a good place.
#
# Since the default class is a simple Active Record, you get timestamps
# for free if you add +created_at+ and +updated_at+ datetime columns to
# the +sessions+ table, making periodic session expiration a snap.
#
# You may provide your own session class implementation, whether a
# feature-packed Active Record or a bare-metal high-performance SQL
# store, by setting
# CGI::Session::ActiveRecordStore.session_class = MySessionClass
# You must implement these methods:
# self.find_by_session_id(session_id)
# initialize(hash_of_session_id_and_data)
# attr_reader :session_id
# attr_accessor :data
# save
# destroy
#
# The example SqlBypass class is a generic SQL session store. You may
# use it as a basis for high-performance database-specific stores.
class ActiveRecordStore
# The default Active Record class.
class Session < ActiveRecord::Base
##
# :singleton-method:
# Customizable data column name. Defaults to 'data'.
cattr_accessor :data_column_name
self.data_column_name = 'data'
before_save :marshal_data!
before_save :raise_on_session_data_overflow!
class << self
# Don't try to reload ARStore::Session in dev mode.
def reloadable? #:nodoc:
false
end
def data_column_size_limit
@data_column_size_limit ||= columns_hash[@@data_column_name].limit
end
# Hook to set up sessid compatibility.
def find_by_session_id(session_id)
setup_sessid_compatibility!
find_by_session_id(session_id)
end
def marshal(data) ActiveSupport::Base64.encode64(Marshal.dump(data)) if data end
def unmarshal(data) Marshal.load(ActiveSupport::Base64.decode64(data)) if data end
def create_table!
connection.execute <<-end_sql
CREATE TABLE #{table_name} (
id INTEGER PRIMARY KEY,
#{connection.quote_column_name('session_id')} TEXT UNIQUE,
#{connection.quote_column_name(@@data_column_name)} TEXT(255)
)
end_sql
end
def drop_table!
connection.execute "DROP TABLE #{table_name}"
end
private
# Compatibility with tables using sessid instead of session_id.
def setup_sessid_compatibility!
# Reset column info since it may be stale.
reset_column_information
if columns_hash['sessid']
def self.find_by_session_id(*args)
find_by_sessid(*args)
end
define_method(:session_id) { sessid }
define_method(:session_id=) { |session_id| self.sessid = session_id }
else
def self.find_by_session_id(session_id)
find :first, :conditions => ["session_id #{attribute_condition(session_id)}", session_id]
end
end
end
end
# Lazy-unmarshal session state.
def data
@data ||= self.class.unmarshal(read_attribute(@@data_column_name)) || {}
end
attr_writer :data
# Has the session been loaded yet?
def loaded?
!! @data
end
private
def marshal_data!
return false if !loaded?
write_attribute(@@data_column_name, self.class.marshal(self.data))
end
# Ensures that the data about to be stored in the database is not
# larger than the data storage column. Raises
# ActionController::SessionOverflowError.
def raise_on_session_data_overflow!
return false if !loaded?
limit = self.class.data_column_size_limit
if loaded? and limit and read_attribute(@@data_column_name).size > limit
raise ActionController::SessionOverflowError
end
end
end
# A barebones session store which duck-types with the default session
# store but bypasses Active Record and issues SQL directly. This is
# an example session model class meant as a basis for your own classes.
#
# The database connection, table name, and session id and data columns
# are configurable class attributes. Marshaling and unmarshaling
# are implemented as class methods that you may override. By default,
# marshaling data is
#
# ActiveSupport::Base64.encode64(Marshal.dump(data))
#
# and unmarshaling data is
#
# Marshal.load(ActiveSupport::Base64.decode64(data))
#
# This marshaling behavior is intended to store the widest range of
# binary session data in a +text+ column. For higher performance,
# store in a +blob+ column instead and forgo the Base64 encoding.
class SqlBypass
##
# :singleton-method:
# Use the ActiveRecord::Base.connection by default.
cattr_accessor :connection
##
# :singleton-method:
# The table name defaults to 'sessions'.
cattr_accessor :table_name
@@table_name = 'sessions'
##
# :singleton-method:
# The session id field defaults to 'session_id'.
cattr_accessor :session_id_column
@@session_id_column = 'session_id'
##
# :singleton-method:
# The data field defaults to 'data'.
cattr_accessor :data_column
@@data_column = 'data'
class << self
def connection
@@connection ||= ActiveRecord::Base.connection
end
# Look up a session by id and unmarshal its data if found.
def find_by_session_id(session_id)
if record = @@connection.select_one("SELECT * FROM #{@@table_name} WHERE #{@@session_id_column}=#{@@connection.quote(session_id)}")
new(:session_id => session_id, :marshaled_data => record['data'])
end
end
def marshal(data) ActiveSupport::Base64.encode64(Marshal.dump(data)) if data end
def unmarshal(data) Marshal.load(ActiveSupport::Base64.decode64(data)) if data end
def create_table!
@@connection.execute <<-end_sql
CREATE TABLE #{table_name} (
id INTEGER PRIMARY KEY,
#{@@connection.quote_column_name(session_id_column)} TEXT UNIQUE,
#{@@connection.quote_column_name(data_column)} TEXT
)
end_sql
end
def drop_table!
@@connection.execute "DROP TABLE #{table_name}"
end
end
attr_reader :session_id
attr_writer :data
# Look for normal and marshaled data, self.find_by_session_id's way of
# telling us to postpone unmarshaling until the data is requested.
# We need to handle a normal data attribute in case of a new record.
def initialize(attributes)
@session_id, @data, @marshaled_data = attributes[:session_id], attributes[:data], attributes[:marshaled_data]
@new_record = @marshaled_data.nil?
end
def new_record?
@new_record
end
# Lazy-unmarshal session state.
def data
unless @data
if @marshaled_data
@data, @marshaled_data = self.class.unmarshal(@marshaled_data) || {}, nil
else
@data = {}
end
end
@data
end
def loaded?
!! @data
end
def save
return false if !loaded?
marshaled_data = self.class.marshal(data)
if @new_record
@new_record = false
@@connection.update <<-end_sql, 'Create session'
INSERT INTO #{@@table_name} (
#{@@connection.quote_column_name(@@session_id_column)},
#{@@connection.quote_column_name(@@data_column)} )
VALUES (
#{@@connection.quote(session_id)},
#{@@connection.quote(marshaled_data)} )
end_sql
else
@@connection.update <<-end_sql, 'Update session'
UPDATE #{@@table_name}
SET #{@@connection.quote_column_name(@@data_column)}=#{@@connection.quote(marshaled_data)}
WHERE #{@@connection.quote_column_name(@@session_id_column)}=#{@@connection.quote(session_id)}
end_sql
end
end
def destroy
unless @new_record
@@connection.delete <<-end_sql, 'Destroy session'
DELETE FROM #{@@table_name}
WHERE #{@@connection.quote_column_name(@@session_id_column)}=#{@@connection.quote(session_id)}
end_sql
end
end
end
# The class used for session storage. Defaults to
# CGI::Session::ActiveRecordStore::Session.
cattr_accessor :session_class
self.session_class = Session
# Find or instantiate a session given a CGI::Session.
def initialize(session, option = nil)
session_id = session.session_id
unless @session = ActiveRecord::Base.silence { @@session_class.find_by_session_id(session_id) }
unless session.new_session
raise CGI::Session::NoSession, 'uninitialized session'
end
@session = @@session_class.new(:session_id => session_id, :data => {})
# session saving can be lazy again, because of improved component implementation
# therefore next line gets commented out:
# @session.save
end
end
# Access the underlying session model.
def model
@session
end
# Restore session state. The session model handles unmarshaling.
def restore
if @session
@session.data
end
end
# Save session store.
def update
if @session
ActiveRecord::Base.silence { @session.save }
end
end
# Save and close the session store.
def close
if @session
update
@session = nil
end
end
# Delete and close the session store.
def delete
if @session
ActiveRecord::Base.silence { @session.destroy }
@session = nil
end
end
protected
def logger
ActionController::Base.logger rescue nil
end
end
end
end

@ -1,163 +1,219 @@
require 'cgi' module ActionController
require 'cgi/session' module Session
# This cookie-based session store is the Rails default. Sessions typically
# contain at most a user_id and flash message; both fit within the 4K cookie
# size limit. Cookie-based sessions are dramatically faster than the
# alternatives.
#
# If you have more than 4K of session data or don't want your data to be
# visible to the user, pick another session store.
#
# CookieOverflow is raised if you attempt to store more than 4K of data.
#
# A message digest is included with the cookie to ensure data integrity:
# a user cannot alter his +user_id+ without knowing the secret key
# included in the hash. New apps are generated with a pregenerated secret
# in config/environment.rb. Set your own for old apps you're upgrading.
#
# Session options:
#
# * <tt>:secret</tt>: An application-wide key string or block returning a
# string called per generated digest. The block is called with the
# CGI::Session instance as an argument. It's important that the secret
# is not vulnerable to a dictionary attack. Therefore, you should choose
# a secret consisting of random numbers and letters and more than 30
# characters. Examples:
#
# :secret => '449fe2e7daee471bffae2fd8dc02313d'
# :secret => Proc.new { User.current_user.secret_key }
#
# * <tt>:digest</tt>: The message digest algorithm used to verify session
# integrity defaults to 'SHA1' but may be any digest provided by OpenSSL,
# such as 'MD5', 'RIPEMD160', 'SHA256', etc.
#
# To generate a secret key for an existing application, run
# "rake secret" and set the key in config/environment.rb.
#
# Note that changing digest or secret invalidates all existing sessions!
class CookieStore
# Cookies can typically store 4096 bytes.
MAX = 4096
SECRET_MIN_LENGTH = 30 # characters
# This cookie-based session store is the Rails default. Sessions typically DEFAULT_OPTIONS = {
# contain at most a user_id and flash message; both fit within the 4K cookie :domain => nil,
# size limit. Cookie-based sessions are dramatically faster than the :path => "/",
# alternatives. :expire_after => nil
# }.freeze
# If you have more than 4K of session data or don't want your data to be
# visible to the user, pick another session store.
#
# CookieOverflow is raised if you attempt to store more than 4K of data.
# TamperedWithCookie is raised if the data integrity check fails.
#
# A message digest is included with the cookie to ensure data integrity:
# a user cannot alter his +user_id+ without knowing the secret key included in
# the hash. New apps are generated with a pregenerated secret in
# config/environment.rb. Set your own for old apps you're upgrading.
#
# Session options:
#
# * <tt>:secret</tt>: An application-wide key string or block returning a string
# called per generated digest. The block is called with the CGI::Session
# instance as an argument. It's important that the secret is not vulnerable to
# a dictionary attack. Therefore, you should choose a secret consisting of
# random numbers and letters and more than 30 characters. Examples:
#
# :secret => '449fe2e7daee471bffae2fd8dc02313d'
# :secret => Proc.new { User.current_user.secret_key }
#
# * <tt>:digest</tt>: The message digest algorithm used to verify session
# integrity defaults to 'SHA1' but may be any digest provided by OpenSSL,
# such as 'MD5', 'RIPEMD160', 'SHA256', etc.
#
# To generate a secret key for an existing application, run
# "rake secret" and set the key in config/environment.rb.
#
# Note that changing digest or secret invalidates all existing sessions!
class CGI::Session::CookieStore
# Cookies can typically store 4096 bytes.
MAX = 4096
SECRET_MIN_LENGTH = 30 # characters
# Raised when storing more than 4K of session data. ENV_SESSION_KEY = "rack.session".freeze
class CookieOverflow < StandardError; end ENV_SESSION_OPTIONS_KEY = "rack.session.options".freeze
HTTP_SET_COOKIE = "Set-Cookie".freeze
# Raised when the cookie fails its integrity check. # Raised when storing more than 4K of session data.
class TamperedWithCookie < StandardError; end class CookieOverflow < StandardError; end
# Called from CGI::Session only. def initialize(app, options = {})
def initialize(session, options = {}) options = options.dup
# The session_key option is required.
if options['session_key'].blank?
raise ArgumentError, 'A session_key is required to write a cookie containing the session data. Use config.action_controller.session = { :session_key => "_myapp_session", :secret => "some secret phrase" } in config/environment.rb'
end
# The secret option is required. @app = app
ensure_secret_secure(options['secret'])
# Keep the session and its secret on hand so we can read and write cookies. # The session_key option is required.
@session, @secret = session, options['secret'] ensure_session_key(options[:key])
@key = options.delete(:key).freeze
# Message digest defaults to SHA1. # The secret option is required.
@digest = options['digest'] || 'SHA1' ensure_secret_secure(options[:secret])
@secret = options.delete(:secret).freeze
# Default cookie options derived from session settings. @digest = options.delete(:digest) || 'SHA1'
@cookie_options = { @verifier = verifier_for(@secret, @digest)
'name' => options['session_key'],
'path' => options['session_path'],
'domain' => options['session_domain'],
'expires' => options['session_expires'],
'secure' => options['session_secure'],
'http_only' => options['session_http_only']
}
# Set no_hidden and no_cookies since the session id is unused and we @default_options = DEFAULT_OPTIONS.merge(options).freeze
# set our own data cookie.
options['no_hidden'] = true
options['no_cookies'] = true
end
# To prevent users from using something insecure like "Password" we make sure that the freeze
# secret they've provided is at least 30 characters in length.
def ensure_secret_secure(secret)
# There's no way we can do this check if they've provided a proc for the
# secret.
return true if secret.is_a?(Proc)
if secret.blank?
raise ArgumentError, %Q{A secret is required to generate an integrity hash for cookie session data. Use config.action_controller.session = { :session_key => "_myapp_session", :secret => "some secret phrase of at least #{SECRET_MIN_LENGTH} characters" } in config/environment.rb}
end
if secret.length < SECRET_MIN_LENGTH
raise ArgumentError, %Q{Secret should be something secure, like "#{CGI::Session.generate_unique_id}". The value you provided, "#{secret}", is shorter than the minimum length of #{SECRET_MIN_LENGTH} characters}
end
end
# Restore session data from the cookie.
def restore
@original = read_cookie
@data = unmarshal(@original) || {}
end
# Wait until close to write the session data cookie.
def update; end
# Write the session data cookie if it was loaded and has changed.
def close
if defined?(@data) && !@data.blank?
updated = marshal(@data)
raise CookieOverflow if updated.size > MAX
write_cookie('value' => updated) unless updated == @original
end
end
# Delete the session data by setting an expired cookie with no data.
def delete
@data = nil
clear_old_cookie_value
write_cookie('value' => nil, 'expires' => 1.year.ago)
end
private
# Marshal a session hash into safe cookie data. Include an integrity hash.
def marshal(session)
verifier.generate(session)
end
# Unmarshal cookie data to a hash and verify its integrity.
def unmarshal(cookie)
if cookie
verifier.verify(cookie)
end end
rescue ActiveSupport::MessageVerifier::InvalidSignature
delete
raise TamperedWithCookie
end
# Read the session data cookie. class SessionHash < Hash
def read_cookie def initialize(middleware, env)
@session.cgi.cookies[@cookie_options['name']].first @middleware = middleware
end @env = env
@loaded = false
end
# CGI likes to make you hack. def [](key)
def write_cookie(options) load! unless @loaded
cookie = CGI::Cookie.new(@cookie_options.merge(options)) super
@session.cgi.send :instance_variable_set, '@output_cookies', [cookie] end
end
# Clear cookie value so subsequent new_session doesn't reload old data. def []=(key, value)
def clear_old_cookie_value load! unless @loaded
@session.cgi.cookies[@cookie_options['name']].clear super
end end
def verifier def to_hash
if @secret.respond_to?(:call) {}.replace(self)
key = @secret.call end
else
key = @secret private
def load!
replace(@middleware.send(:load_session, @env))
@loaded = true
end
end end
ActiveSupport::MessageVerifier.new(key, @digest)
def call(env)
session_data = SessionHash.new(self, env)
original_value = session_data.dup
env[ENV_SESSION_KEY] = session_data
env[ENV_SESSION_OPTIONS_KEY] = @default_options.dup
status, headers, body = @app.call(env)
unless env[ENV_SESSION_KEY] == original_value
session_data = marshal(env[ENV_SESSION_KEY].to_hash)
raise CookieOverflow if session_data.size > MAX
options = env[ENV_SESSION_OPTIONS_KEY]
cookie = Hash.new
cookie[:value] = session_data
unless options[:expire_after].nil?
cookie[:expires] = Time.now + options[:expire_after]
end
cookie = build_cookie(@key, cookie.merge(options))
case headers[HTTP_SET_COOKIE]
when Array
headers[HTTP_SET_COOKIE] << cookie
when String
headers[HTTP_SET_COOKIE] = [headers[HTTP_SET_COOKIE], cookie]
when nil
headers[HTTP_SET_COOKIE] = cookie
end
end
[status, headers, body]
end
private
# Should be in Rack::Utils soon
def build_cookie(key, value)
case value
when Hash
domain = "; domain=" + value[:domain] if value[:domain]
path = "; path=" + value[:path] if value[:path]
# According to RFC 2109, we need dashes here.
# N.B.: cgi.rb uses spaces...
expires = "; expires=" + value[:expires].clone.gmtime.
strftime("%a, %d-%b-%Y %H:%M:%S GMT") if value[:expires]
secure = "; secure" if value[:secure]
httponly = "; httponly" if value[:httponly]
value = value[:value]
end
value = [value] unless Array === value
cookie = Rack::Utils.escape(key) + "=" +
value.map { |v| Rack::Utils.escape(v) }.join("&") +
"#{domain}#{path}#{expires}#{secure}#{httponly}"
end
def load_session(env)
request = Rack::Request.new(env)
session_data = request.cookies[@key]
unmarshal(session_data) || {}
end
# Marshal a session hash into safe cookie data. Include an integrity hash.
def marshal(session)
@verifier.generate(session)
end
# Unmarshal cookie data to a hash and verify its integrity.
def unmarshal(cookie)
@verifier.verify(cookie) if cookie
rescue ActiveSupport::MessageVerifier::InvalidSignature
nil
end
def ensure_session_key(key)
if key.blank?
raise ArgumentError, 'A session_key is required to write a ' +
'cookie containing the session data. Use ' +
'config.action_controller.session = { :session_key => ' +
'"_myapp_session", :secret => "some secret phrase" } in ' +
'config/environment.rb'
end
end
# To prevent users from using something insecure like "Password" we make sure that the
# secret they've provided is at least 30 characters in length.
def ensure_secret_secure(secret)
# There's no way we can do this check if they've provided a proc for the
# secret.
return true if secret.is_a?(Proc)
if secret.blank?
raise ArgumentError, "A secret is required to generate an " +
"integrity hash for cookie session data. Use " +
"config.action_controller.session = { :session_key => " +
"\"_myapp_session\", :secret => \"some secret phrase of at " +
"least #{SECRET_MIN_LENGTH} characters\" } " +
"in config/environment.rb"
end
if secret.length < SECRET_MIN_LENGTH
raise ArgumentError, "Secret should be something secure, " +
"like \"#{ActiveSupport::SecureRandom.hex(16)}\". The value you " +
"provided, \"#{secret}\", is shorter than the minimum length " +
"of #{SECRET_MIN_LENGTH} characters"
end
end
def verifier_for(secret, digest)
key = secret.respond_to?(:call) ? secret.call : secret
ActiveSupport::MessageVerifier.new(key, digest)
end
end end
end
end end

@ -1,32 +0,0 @@
#!/usr/bin/env ruby
# This is a really simple session storage daemon, basically just a hash,
# which is enabled for DRb access.
require 'drb'
session_hash = Hash.new
session_hash.instance_eval { @mutex = Mutex.new }
class <<session_hash
def []=(key, value)
@mutex.synchronize do
super(key, value)
end
end
def [](key)
@mutex.synchronize do
super(key)
end
end
def delete(key)
@mutex.synchronize do
super(key)
end
end
end
DRb.start_service('druby://127.0.0.1:9192', session_hash)
DRb.thread.join

@ -1,35 +0,0 @@
require 'cgi'
require 'cgi/session'
require 'drb'
class CGI #:nodoc:all
class Session
class DRbStore
@@session_data = DRbObject.new(nil, 'druby://localhost:9192')
def initialize(session, option=nil)
@session_id = session.session_id
end
def restore
@h = @@session_data[@session_id] || {}
end
def update
@@session_data[@session_id] = @h
end
def close
update
end
def delete
@@session_data.delete(@session_id)
end
def data
@@session_data[@session_id]
end
end
end
end

@ -1,95 +1,48 @@
# cgi/session/memcached.rb - persistent storage of marshalled session data
#
# == Overview
#
# This file provides the CGI::Session::MemCache class, which builds
# persistence of storage data on top of the MemCache library. See
# cgi/session.rb for more details on session storage managers.
#
begin begin
require 'cgi/session'
require_library_or_gem 'memcache' require_library_or_gem 'memcache'
class CGI module ActionController
class Session module Session
# MemCache-based session storage class. class MemCacheStore < AbstractStore
# def initialize(app, options = {})
# This builds upon the top-level MemCache class provided by the # Support old :expires option
# library file memcache.rb. Session data is marshalled and stored options[:expire_after] ||= options[:expires]
# in a memcached cache.
class MemCacheStore
def check_id(id) #:nodoc:#
/[^0-9a-zA-Z]+/ =~ id.to_s ? false : true
end
# Create a new CGI::Session::MemCache instance super
#
# This constructor is used internally by CGI::Session. The @default_options = {
# user does not generally need to call it directly. :namespace => 'rack:session',
# :memcache_server => 'localhost:11211'
# +session+ is the session for which this instance is being }.merge(@default_options)
# created. The session id must only contain alphanumeric
# characters; automatically generated session ids observe @pool = options[:cache] || MemCache.new(@default_options[:memcache_server], @default_options)
# this requirement. unless @pool.servers.any? { |s| s.alive? }
# raise "#{self} unable to find server during initialization."
# +options+ is a hash of options for the initializer. The
# following options are recognized:
#
# cache:: an instance of a MemCache client to use as the
# session cache.
#
# expires:: an expiry time value to use for session entries in
# the session cache. +expires+ is interpreted in seconds
# relative to the current time if its less than 60*60*24*30
# (30 days), or as an absolute Unix time (e.g., Time#to_i) if
# greater. If +expires+ is +0+, or not passed on +options+,
# the entry will never expire.
#
# This session's memcache entry will be created if it does
# not exist, or retrieved if it does.
def initialize(session, options = {})
id = session.session_id
unless check_id(id)
raise ArgumentError, "session_id '%s' is invalid" % id
end end
@cache = options['cache'] || MemCache.new('localhost') @mutex = Mutex.new
@expires = options['expires'] || 0
@session_key = "session:#{id}" super
@session_data = {} end
# Add this key to the store if haven't done so yet
unless @cache.get(@session_key) private
@cache.add(@session_key, @session_data, @expires) def get_session(env, sid)
sid ||= generate_sid
begin
session = @pool.get(sid) || {}
rescue MemCache::MemCacheError, Errno::ECONNREFUSED
session = {}
end
[sid, session]
end end
end
# Restore session state from the session's memcache entry.
#
# Returns the session state as a hash.
def restore
@session_data = @cache[@session_key] || {}
end
# Save session state to the session's memcache entry.
def update
@cache.set(@session_key, @session_data, @expires)
end
# Update and close the session's memcache entry.
def close
update
end
# Delete the session's memcache entry.
def delete
@cache.delete(@session_key)
@session_data = {}
end
def data
@session_data
end
def set_session(env, sid, session_data)
options = env['rack.session.options']
expiry = options[:expire_after] || 0
@pool.set(sid, session_data, expiry)
return true
rescue MemCache::MemCacheError, Errno::ECONNREFUSED
return false
end
end end
end end
end end

@ -3,8 +3,29 @@ module SessionManagement #:nodoc:
def self.included(base) def self.included(base)
base.class_eval do base.class_eval do
extend ClassMethods extend ClassMethods
alias_method_chain :process, :session_management_support end
alias_method_chain :process_cleanup, :session_management_support end
class Middleware
DEFAULT_OPTIONS = {
:path => "/",
:key => "_session_id",
:httponly => true,
}.freeze
def self.new(app)
cgi_options = ActionController::Base.session_options
options = cgi_options.symbolize_keys
options = DEFAULT_OPTIONS.merge(options)
options[:path] = options.delete(:session_path)
options[:key] = options.delete(:session_key)
options[:httponly] = options.delete(:session_http_only)
if store = ActionController::Base.session_store
store.new(app, options)
else # Sessions disabled
lambda { |env| app.call(env) }
end
end end
end end
@ -12,144 +33,45 @@ module ClassMethods
# Set the session store to be used for keeping the session data between requests. # Set the session store to be used for keeping the session data between requests.
# By default, sessions are stored in browser cookies (<tt>:cookie_store</tt>), # By default, sessions are stored in browser cookies (<tt>:cookie_store</tt>),
# but you can also specify one of the other included stores (<tt>:active_record_store</tt>, # but you can also specify one of the other included stores (<tt>:active_record_store</tt>,
# <tt>:p_store</tt>, <tt>:drb_store</tt>, <tt>:mem_cache_store</tt>, or # <tt>:mem_cache_store</tt>, or your own custom class.
# <tt>:memory_store</tt>) or your own custom class.
def session_store=(store) def session_store=(store)
ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS[:database_manager] = if store == :active_record_store
store.is_a?(Symbol) ? CGI::Session.const_get(store == :drb_store ? "DRbStore" : store.to_s.camelize) : store self.session_store = ActiveRecord::SessionStore
else
@@session_store = store.is_a?(Symbol) ?
Session.const_get(store.to_s.camelize) :
store
end
end end
# Returns the session store class currently used. # Returns the session store class currently used.
def session_store def session_store
ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS[:database_manager] if defined? @@session_store
@@session_store
else
Session::CookieStore
end
end
def session=(options = {})
self.session_store = nil if options.delete(:disabled)
session_options.merge!(options)
end end
# Returns the hash used to configure the session. Example use: # Returns the hash used to configure the session. Example use:
# #
# ActionController::Base.session_options[:session_secure] = true # session only available over HTTPS # ActionController::Base.session_options[:session_secure] = true # session only available over HTTPS
def session_options def session_options
ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS @session_options ||= {}
end end
# Specify how sessions ought to be managed for a subset of the actions on
# the controller. Like filters, you can specify <tt>:only</tt> and
# <tt>:except</tt> clauses to restrict the subset, otherwise options
# apply to all actions on this controller.
#
# The session options are inheritable, as well, so if you specify them in
# a parent controller, they apply to controllers that extend the parent.
#
# Usage:
#
# # turn off session management for all actions.
# session :off
#
# # turn off session management for all actions _except_ foo and bar.
# session :off, :except => %w(foo bar)
#
# # turn off session management for only the foo and bar actions.
# session :off, :only => %w(foo bar)
#
# # the session will only work over HTTPS, but only for the foo action
# session :only => :foo, :session_secure => true
#
# # the session by default uses HttpOnly sessions for security reasons.
# # this can be switched off.
# session :only => :foo, :session_http_only => false
#
# # the session will only be disabled for 'foo', and only if it is
# # requested as a web service
# session :off, :only => :foo,
# :if => Proc.new { |req| req.parameters[:ws] }
#
# # the session will be disabled for non html/ajax requests
# session :off,
# :if => Proc.new { |req| !(req.format.html? || req.format.js?) }
#
# # turn the session back on, useful when it was turned off in the
# # application controller, and you need it on in another controller
# session :on
#
# All session options described for ActionController::Base.process_cgi
# are valid arguments.
def session(*args) def session(*args)
options = args.extract_options! ActiveSupport::Deprecation.warn(
"Disabling sessions for a single controller has been deprecated. " +
options[:disabled] = false if args.delete(:on) "Sessions are now lazy loaded. So if you don't access them, " +
options[:disabled] = true if !args.empty? "consider them off. You can still modify the session cookie " +
options[:only] = [*options[:only]].map { |o| o.to_s } if options[:only] "options with request.session_options.", caller)
options[:except] = [*options[:except]].map { |o| o.to_s } if options[:except]
if options[:only] && options[:except]
raise ArgumentError, "only one of either :only or :except are allowed"
end
write_inheritable_array(:session_options, [options])
end
# So we can declare session options in the Rails initializer.
alias_method :session=, :session
def cached_session_options #:nodoc:
@session_options ||= read_inheritable_attribute(:session_options) || []
end
def session_options_for(request, action) #:nodoc:
if (session_options = cached_session_options).empty?
{}
else
options = {}
action = action.to_s
session_options.each do |opts|
next if opts[:if] && !opts[:if].call(request)
if opts[:only] && opts[:only].include?(action)
options.merge!(opts)
elsif opts[:except] && !opts[:except].include?(action)
options.merge!(opts)
elsif !opts[:only] && !opts[:except]
options.merge!(opts)
end
end
if options.empty? then options
else
options.delete :only
options.delete :except
options.delete :if
options[:disabled] ? false : options
end
end
end end
end end
def process_with_session_management_support(request, response, method = :perform_action, *arguments) #:nodoc:
set_session_options(request)
process_without_session_management_support(request, response, method, *arguments)
end
private
def set_session_options(request)
request.session_options = self.class.session_options_for(request, request.parameters["action"] || "index")
end
def process_cleanup_with_session_management_support
clear_persistent_model_associations
process_cleanup_without_session_management_support
end
# Clear cached associations in session data so they don't overflow
# the database field. Only applies to ActiveRecordStore since there
# is not a standard way to iterate over session data.
def clear_persistent_model_associations #:doc:
if defined?(@_session) && @_session.respond_to?(:data)
session_data = @_session.data
if session_data && session_data.respond_to?(:each_value)
session_data.each_value do |obj|
obj.clear_association_cache if obj.respond_to?(:clear_association_cache)
end
end
end
end
end end
end end

@ -30,6 +30,8 @@
ActionController::Base.logger = nil ActionController::Base.logger = nil
ActionController::Routing::Routes.reload rescue nil ActionController::Routing::Routes.reload rescue nil
ActionController::Base.session_store = nil
FIXTURE_LOAD_PATH = File.join(File.dirname(__FILE__), 'fixtures') FIXTURE_LOAD_PATH = File.join(File.dirname(__FILE__), 'fixtures')
ActionController::Base.view_paths = FIXTURE_LOAD_PATH ActionController::Base.view_paths = FIXTURE_LOAD_PATH
ActionController::Base.view_paths.load ActionController::Base.view_paths.load

@ -1,140 +1,128 @@
# These tests exercise CGI::Session::ActiveRecordStore, so you're going to
# need AR in a sibling directory to AP and have SQLite installed.
require 'active_record_unit' require 'active_record_unit'
module CommonActiveRecordStoreTests class ActiveRecordStoreTest < ActionController::IntegrationTest
def test_basics DispatcherApp = ActionController::Dispatcher.new
s = session_class.new(:session_id => '1234', :data => { 'foo' => 'bar' }) SessionApp = ActiveRecord::SessionStore.new(DispatcherApp,
assert_equal 'bar', s.data['foo'] :key => '_session_id')
assert s.save SessionAppWithFixation = ActiveRecord::SessionStore.new(DispatcherApp,
assert_equal 'bar', s.data['foo'] :key => '_session_id', :cookie_only => false)
assert_not_nil t = session_class.find_by_session_id('1234') class TestController < ActionController::Base
assert_not_nil t.data def no_session_access
assert_equal 'bar', t.data['foo'] head :ok
end
def set_session_value
session[:foo] = params[:foo] || "bar"
head :ok
end
def get_session_value
render :text => "foo: #{session[:foo].inspect}"
end
def rescue_action(e) raise end
end end
def test_reload_same_session def setup
@new_session.update ActiveRecord::SessionStore.session_class.create_table!
reloaded = CGI::Session.new(CGI.new, 'session_id' => @new_session.session_id, 'database_manager' => CGI::Session::ActiveRecordStore) @integration_session = open_session(SessionApp)
assert_equal 'bar', reloaded['foo']
end end
def test_tolerates_close_close def teardown
assert_nothing_raised do ActiveRecord::SessionStore.session_class.drop_table!
@new_session.close end
@new_session.close
def test_setting_and_getting_session_value
with_test_route_set do
get '/set_session_value'
assert_response :success
assert cookies['_session_id']
get '/get_session_value'
assert_response :success
assert_equal 'foo: "bar"', response.body
get '/set_session_value', :foo => "baz"
assert_response :success
assert cookies['_session_id']
get '/get_session_value'
assert_response :success
assert_equal 'foo: "baz"', response.body
end end
end end
end
class ActiveRecordStoreTest < ActiveRecordTestCase def test_getting_nil_session_value
include CommonActiveRecordStoreTests with_test_route_set do
get '/get_session_value'
def session_class assert_response :success
CGI::Session::ActiveRecordStore::Session assert_equal 'foo: nil', response.body
end
def session_id_column
"session_id"
end
def setup
session_class.create_table!
ENV['REQUEST_METHOD'] = 'GET'
ENV['REQUEST_URI'] = '/'
CGI::Session::ActiveRecordStore.session_class = session_class
@cgi = CGI.new
@new_session = CGI::Session.new(@cgi, 'database_manager' => CGI::Session::ActiveRecordStore, 'new_session' => true)
@new_session['foo'] = 'bar'
end
# this test only applies for eager session saving
# def test_another_instance
# @another = CGI::Session.new(@cgi, 'session_id' => @new_session.session_id, 'database_manager' => CGI::Session::ActiveRecordStore)
# assert_equal @new_session.session_id, @another.session_id
# end
def test_model_attribute
assert_kind_of CGI::Session::ActiveRecordStore::Session, @new_session.model
assert_equal({ 'foo' => 'bar' }, @new_session.model.data)
end
def test_save_unloaded_session
c = session_class.connection
bogus_class = c.quote(ActiveSupport::Base64.encode64("\004\010o:\vBlammo\000"))
c.insert("INSERT INTO #{session_class.table_name} ('#{session_id_column}', 'data') VALUES ('abcdefghijklmnop', #{bogus_class})")
sess = session_class.find_by_session_id('abcdefghijklmnop')
assert_not_nil sess
assert !sess.loaded?
# because the session is not loaded, the save should be a no-op. If it
# isn't, this'll try and unmarshall the bogus class, and should get an error.
assert_nothing_raised { sess.save }
end
def teardown
session_class.drop_table!
end
end
class ColumnLimitTest < ActiveRecordTestCase
def setup
@session_class = CGI::Session::ActiveRecordStore::Session
@session_class.create_table!
end
def teardown
@session_class.drop_table!
end
def test_protection_from_data_larger_than_column
# Can't test this unless there is a limit
return unless limit = @session_class.data_column_size_limit
too_big = ':(' * limit
s = @session_class.new(:session_id => '666', :data => {'foo' => too_big})
s.data
assert_raise(ActionController::SessionOverflowError) { s.save }
end
end
class DeprecatedActiveRecordStoreTest < ActiveRecordStoreTest
def session_id_column
"sessid"
end
def setup
session_class.connection.execute 'create table old_sessions (id integer primary key, sessid text unique, data text)'
session_class.table_name = 'old_sessions'
session_class.send :setup_sessid_compatibility!
ENV['REQUEST_METHOD'] = 'GET'
CGI::Session::ActiveRecordStore.session_class = session_class
@new_session = CGI::Session.new(CGI.new, 'database_manager' => CGI::Session::ActiveRecordStore, 'new_session' => true)
@new_session['foo'] = 'bar'
end
def teardown
session_class.connection.execute 'drop table old_sessions'
session_class.table_name = 'sessions'
end
end
class SqlBypassActiveRecordStoreTest < ActiveRecordStoreTest
def session_class
unless defined? @session_class
@session_class = CGI::Session::ActiveRecordStore::SqlBypass
@session_class.connection = CGI::Session::ActiveRecordStore::Session.connection
end end
@session_class
end end
def test_model_attribute def test_prevents_session_fixation
assert_kind_of CGI::Session::ActiveRecordStore::SqlBypass, @new_session.model with_test_route_set do
assert_equal({ 'foo' => 'bar' }, @new_session.model.data) get '/set_session_value'
assert_response :success
assert cookies['_session_id']
get '/get_session_value'
assert_response :success
assert_equal 'foo: "bar"', response.body
session_id = cookies['_session_id']
assert session_id
reset!
get '/set_session_value', :_session_id => session_id, :foo => "baz"
assert_response :success
assert_equal nil, cookies['_session_id']
get '/get_session_value', :_session_id => session_id
assert_response :success
assert_equal 'foo: nil', response.body
assert_equal nil, cookies['_session_id']
end
end end
def test_allows_session_fixation
@integration_session = open_session(SessionAppWithFixation)
with_test_route_set do
get '/set_session_value'
assert_response :success
assert cookies['_session_id']
get '/get_session_value'
assert_response :success
assert_equal 'foo: "bar"', response.body
session_id = cookies['_session_id']
assert session_id
reset!
@integration_session = open_session(SessionAppWithFixation)
get '/set_session_value', :_session_id => session_id, :foo => "baz"
assert_response :success
assert_equal session_id, cookies['_session_id']
get '/get_session_value', :_session_id => session_id
assert_response :success
assert_equal 'foo: "baz"', response.body
assert_equal session_id, cookies['_session_id']
end
end
private
def with_test_route_set
with_routing do |set|
set.draw do |map|
map.with_options :controller => "active_record_store_test/test" do |c|
c.connect "/:action"
end
end
yield
end
end
end end

@ -231,8 +231,6 @@ def test_integration_methods_called
class IntegrationProcessTest < ActionController::IntegrationTest class IntegrationProcessTest < ActionController::IntegrationTest
class IntegrationController < ActionController::Base class IntegrationController < ActionController::Base
session :off
def get def get
respond_to do |format| respond_to do |format|
format.html { render :text => "OK", :status => 200 } format.html { render :text => "OK", :status => 200 }

@ -6,8 +6,6 @@ class ApplicationController < ActionController::Base
end end
class UploadTestController < ActionController::Base class UploadTestController < ActionController::Base
session :off
def update def update
SessionUploadTest.last_request_type = ActionController::Base.param_parsers[request.content_type] SessionUploadTest.last_request_type = ActionController::Base.param_parsers[request.content_type]
render :text => "got here" render :text => "got here"

@ -229,7 +229,7 @@ def test_body_should_be_rewound
class RackResponseTest < BaseRackTest class RackResponseTest < BaseRackTest
def setup def setup
super super
@response = ActionController::RackResponse.new(@request) @response = ActionController::RackResponse.new
end end
def test_simple_output def test_simple_output
@ -265,34 +265,12 @@ def test_streaming_block
body.each { |part| parts << part } body.each { |part| parts << part }
assert_equal ["0", "1", "2", "3", "4"], parts assert_equal ["0", "1", "2", "3", "4"], parts
end end
def test_set_session_cookie
cookie = CGI::Cookie.new({"name" => "name", "value" => "Josh"})
@request.cgi.send :instance_variable_set, '@output_cookies', [cookie]
@response.body = "Hello, World!"
@response.prepare!
status, headers, body = @response.out
assert_equal "200 OK", status
assert_equal({
"Content-Type" => "text/html; charset=utf-8",
"Cache-Control" => "private, max-age=0, must-revalidate",
"ETag" => '"65a8e27d8879283831b664bd8b7f0ad4"',
"Set-Cookie" => ["name=Josh; path="],
"Content-Length" => "13"
}, headers)
parts = []
body.each { |part| parts << part }
assert_equal ["Hello, World!"], parts
end
end end
class RackResponseHeadersTest < BaseRackTest class RackResponseHeadersTest < BaseRackTest
def setup def setup
super super
@response = ActionController::RackResponse.new(@request) @response = ActionController::RackResponse.new
@response.headers['Status'] = "200 OK" @response.headers['Status'] = "200 OK"
end end

@ -1,298 +1,146 @@
require 'abstract_unit' require 'abstract_unit'
require 'stringio' require 'stringio'
class CookieStoreTest < ActionController::IntegrationTest
SessionKey = '_myapp_session'
SessionSecret = 'b3c631c314c0bbca50c1b2843150fe33'
class CGI::Session::CookieStore DispatcherApp = ActionController::Dispatcher.new
def ensure_secret_secure_with_test_hax(secret) CookieStoreApp = ActionController::Session::CookieStore.new(DispatcherApp,
if secret == CookieStoreTest.default_session_options['secret'] :key => SessionKey, :secret => SessionSecret)
return true
else SignedBar = "BAh7BjoIZm9vIghiYXI%3D--" +
ensure_secret_secure_without_test_hax(secret) "fef868465920f415f2c0652d6910d3af288a0367"
class TestController < ActionController::Base
def no_session_access
head :ok
end end
end
alias_method_chain :ensure_secret_secure, :test_hax
end
def set_session_value
# Expose for tests. session[:foo] = "bar"
class CGI head :ok
attr_reader :output_cookies, :output_hidden
class Session
attr_reader :dbman
class CookieStore
attr_reader :data, :original, :cookie_options
end end
end
end
class CookieStoreTest < Test::Unit::TestCase def get_session_value
def self.default_session_options render :text => "foo: #{session[:foo].inspect}"
{ 'database_manager' => CGI::Session::CookieStore, end
'session_key' => '_myapp_session',
'secret' => 'Keep it secret; keep it safe.',
'no_cookies' => true,
'no_hidden' => true,
'session_http_only' => true
}
end
def self.cookies def raise_data_overflow
{ :empty => ['BAgw--0686dcaccc01040f4bd4f35fe160afe9bc04c330', {}], session[:foo] = 'bye!' * 1024
:a_one => ['BAh7BiIGYWkG--5689059497d7f122a7119f171aef81dcfd807fec', { 'a' => 1 }], head :ok
:typical => ['BAh7ByIMdXNlcl9pZGkBeyIKZmxhc2h7BiILbm90aWNlIgxIZXkgbm93--9d20154623b9eeea05c62ab819be0e2483238759', { 'user_id' => 123, 'flash' => { 'notice' => 'Hey now' }}], end
:flashed => ['BAh7ByIMdXNlcl9pZGkBeyIKZmxhc2h7AA==--bf9785a666d3c4ac09f7fe3353496b437546cfbf', { 'user_id' => 123, 'flash' => {} }]
}
def rescue_action(e) raise end
end end
def setup def setup
ENV.delete('HTTP_COOKIE') @integration_session = open_session(CookieStoreApp)
end end
def test_raises_argument_error_if_missing_session_key def test_raises_argument_error_if_missing_session_key
[nil, ''].each do |blank| assert_raise(ArgumentError, nil.inspect) {
assert_raise(ArgumentError, blank.inspect) { new_session 'session_key' => blank } ActionController::Session::CookieStore.new(nil,
end :key => nil, :secret => SessionSecret)
}
assert_raise(ArgumentError, ''.inspect) {
ActionController::Session::CookieStore.new(nil,
:key => '', :secret => SessionSecret)
}
end end
def test_raises_argument_error_if_missing_secret def test_raises_argument_error_if_missing_secret
[nil, ''].each do |blank| assert_raise(ArgumentError, nil.inspect) {
assert_raise(ArgumentError, blank.inspect) { new_session 'secret' => blank } ActionController::Session::CookieStore.new(nil,
end :key => SessionKey, :secret => nil)
}
assert_raise(ArgumentError, ''.inspect) {
ActionController::Session::CookieStore.new(nil,
:key => SessionKey, :secret => '')
}
end end
def test_raises_argument_error_if_secret_is_probably_insecure def test_raises_argument_error_if_secret_is_probably_insecure
["password", "secret", "12345678901234567890123456789"].each do |blank| assert_raise(ArgumentError, "password".inspect) {
assert_raise(ArgumentError, blank.inspect) { new_session 'secret' => blank } ActionController::Session::CookieStore.new(nil,
end :key => SessionKey, :secret => "password")
}
assert_raise(ArgumentError, "secret".inspect) {
ActionController::Session::CookieStore.new(nil,
:key => SessionKey, :secret => "secret")
}
assert_raise(ArgumentError, "12345678901234567890123456789".inspect) {
ActionController::Session::CookieStore.new(nil,
:key => SessionKey, :secret => "12345678901234567890123456789")
}
end end
def test_reconfigures_session_to_omit_id_cookie_and_hidden_field def test_setting_session_value
new_session do |session| with_test_route_set do
assert_equal true, @options['no_hidden'] get '/set_session_value'
assert_equal true, @options['no_cookies'] assert_response :success
end assert_equal ["_myapp_session=#{SignedBar}; path=/"],
headers['Set-Cookie']
end
end end
def test_restore_unmarshals_missing_cookie_as_empty_hash def test_getting_session_value
new_session do |session| with_test_route_set do
assert_nil session.dbman.data cookies[SessionKey] = SignedBar
assert_nil session['test'] get '/get_session_value'
assert_equal Hash.new, session.dbman.data assert_response :success
end assert_equal 'foo: "bar"', response.body
end
end end
def test_restore_unmarshals_good_cookies def test_disregards_tampered_sessions
cookies(:empty, :a_one, :typical).each do |value, expected| with_test_route_set do
set_cookie! value cookies[SessionKey] = "BAh7BjoIZm9vIghiYXI%3D--123456780"
new_session do |session| get '/get_session_value'
assert_nil session['lazy loads the data hash'] assert_response :success
assert_equal expected, session.dbman.data assert_equal 'foo: nil', response.body
end
end
end
def test_restore_deletes_tampered_cookies
set_cookie! 'a--b'
new_session do |session|
assert_raise(CGI::Session::CookieStore::TamperedWithCookie) { session['fail'] }
assert_cookie_deleted session
end
end
def test_close_doesnt_write_cookie_if_data_is_blank
new_session do |session|
assert_no_cookies session
session.close
assert_no_cookies session
end
end
def test_close_doesnt_write_cookie_if_data_is_unchanged
set_cookie! cookie_value(:typical)
new_session do |session|
assert_no_cookies session
session['user_id'] = session['user_id']
session.close
assert_no_cookies session
end end
end end
def test_close_raises_when_data_overflows def test_close_raises_when_data_overflows
set_cookie! cookie_value(:empty) with_test_route_set do
new_session do |session| assert_raise(ActionController::Session::CookieStore::CookieOverflow) {
session['overflow'] = 'bye!' * 1024 get '/raise_data_overflow'
assert_raise(CGI::Session::CookieStore::CookieOverflow) { session.close } }
assert_no_cookies session
end end
end end
def test_close_marshals_and_writes_cookie def test_doesnt_write_session_cookie_if_session_is_not_accessed
set_cookie! cookie_value(:typical) with_test_route_set do
new_session do |session| get '/no_session_access'
assert_no_cookies session assert_response :success
session['flash'] = {} assert_equal [], headers['Set-Cookie']
assert_no_cookies session
session.close
assert_equal 1, session.cgi.output_cookies.size
cookie = session.cgi.output_cookies.first
assert_cookie cookie, cookie_value(:flashed)
assert_http_only_cookie cookie
assert_secure_cookie cookie, false
end end
end end
def test_writes_non_secure_cookie_by_default def test_doesnt_write_session_cookie_if_session_is_unchanged
set_cookie! cookie_value(:typical) with_test_route_set do
new_session do |session| cookies[SessionKey] = "BAh7BjoIZm9vIghiYXI%3D--" +
session['flash'] = {} "fef868465920f415f2c0652d6910d3af288a0367"
session.close get '/no_session_access'
cookie = session.cgi.output_cookies.first assert_response :success
assert_secure_cookie cookie,false assert_equal [], headers['Set-Cookie']
end
end
def test_writes_secure_cookie
set_cookie! cookie_value(:typical)
new_session('session_secure'=>true) do |session|
session['flash'] = {}
session.close
cookie = session.cgi.output_cookies.first
assert_secure_cookie cookie
end
end
def test_http_only_cookie_by_default
set_cookie! cookie_value(:typical)
new_session do |session|
session['flash'] = {}
session.close
cookie = session.cgi.output_cookies.first
assert_http_only_cookie cookie
end
end
def test_overides_http_only_cookie
set_cookie! cookie_value(:typical)
new_session('session_http_only'=>false) do |session|
session['flash'] = {}
session.close
cookie = session.cgi.output_cookies.first
assert_http_only_cookie cookie, false
end
end
def test_delete_writes_expired_empty_cookie_and_sets_data_to_nil
set_cookie! cookie_value(:typical)
new_session do |session|
assert_no_cookies session
session.delete
assert_cookie_deleted session
# @data is set to nil so #close doesn't send another cookie.
session.close
assert_cookie_deleted session
end
end
def test_new_session_doesnt_reuse_deleted_cookie_data
set_cookie! cookie_value(:typical)
new_session do |session|
assert_not_nil session['user_id']
session.delete
# Start a new session using the same CGI instance.
post_delete_session = CGI::Session.new(session.cgi, self.class.default_session_options)
assert_nil post_delete_session['user_id']
end end
end end
private private
def assert_no_cookies(session) def with_test_route_set
assert_nil session.cgi.output_cookies, session.cgi.output_cookies.inspect with_routing do |set|
end set.draw do |map|
map.with_options :controller => "cookie_store_test/test" do |c|
def assert_cookie_deleted(session, message = 'Expected session deletion cookie to be set') c.connect "/:action"
assert_equal 1, session.cgi.output_cookies.size end
cookie = session.cgi.output_cookies.first end
assert_cookie cookie, nil, 1.year.ago.to_date, "#{message}: #{cookie.name} => #{cookie.value}" yield
end
def assert_cookie(cookie, value = nil, expires = nil, message = nil)
assert_equal '_myapp_session', cookie.name, message
assert_equal [value].compact, cookie.value, message
assert_equal expires, cookie.expires ? cookie.expires.to_date : cookie.expires, message
end
def assert_secure_cookie(cookie,value=true)
assert cookie.secure==value
end
def assert_http_only_cookie(cookie,value=true)
assert cookie.http_only==value
end
def cookies(*which)
self.class.cookies.values_at(*which)
end
def cookie_value(which)
self.class.cookies[which].first
end
def set_cookie!(value)
ENV['HTTP_COOKIE'] = "_myapp_session=#{value}"
end
def new_session(options = {})
with_cgi do |cgi|
assert_nil cgi.output_hidden, "Output hidden params should be empty: #{cgi.output_hidden.inspect}"
assert_nil cgi.output_cookies, "Output cookies should be empty: #{cgi.output_cookies.inspect}"
@options = self.class.default_session_options.merge(options)
session = CGI::Session.new(cgi, @options)
ObjectSpace.undefine_finalizer(session)
assert_nil cgi.output_hidden, "Output hidden params should be empty: #{cgi.output_hidden.inspect}"
assert_nil cgi.output_cookies, "Output cookies should be empty: #{cgi.output_cookies.inspect}"
yield session if block_given?
session
end end
end end
def with_cgi
ENV['REQUEST_METHOD'] = 'GET'
ENV['HTTP_HOST'] = 'example.com'
ENV['QUERY_STRING'] = ''
cgi = CGI.new('query', StringIO.new(''))
yield cgi if block_given?
cgi
end
end
class CookieStoreWithBlockAsSecretTest < CookieStoreTest
def self.default_session_options
CookieStoreTest.default_session_options.merge 'secret' => Proc.new { 'Keep it secret; keep it safe.' }
end
end
class CookieStoreWithMD5DigestTest < CookieStoreTest
def self.default_session_options
CookieStoreTest.default_session_options.merge 'digest' => 'MD5'
end
def self.cookies
{ :empty => ['BAgw--0415cc0be9579b14afc22ee2d341aa21', {}],
:a_one => ['BAh7BiIGYWkG--5a0ed962089cc6600ff44168a5d59bc8', { 'a' => 1 }],
:typical => ['BAh7ByIMdXNlcl9pZGkBeyIKZmxhc2h7BiILbm90aWNlIgxIZXkgbm93--f426763f6ef435b3738b493600db8d64', { 'user_id' => 123, 'flash' => { 'notice' => 'Hey now' }}],
:flashed => ['BAh7ByIMdXNlcl9pZGkBeyIKZmxhc2h7AA==--0af9156650dab044a53a91a4ddec2c51', { 'user_id' => 123, 'flash' => {} }],
:double_escaped => [CGI.escape('BAh7ByIMdXNlcl9pZGkBeyIKZmxhc2h7AA%3D%3D--0af9156650dab044a53a91a4ddec2c51'), { 'user_id' => 123, 'flash' => {} }] }
end
end end

@ -1,178 +1,81 @@
require 'abstract_unit' require 'abstract_unit'
class CGI::Session # You need to start a memcached server inorder to run these tests
def cache class MemCacheStoreTest < ActionController::IntegrationTest
dbman.instance_variable_get(:@cache) class TestController < ActionController::Base
end def no_session_access
end head :ok
uses_mocha 'MemCacheStore tests' do
if defined? MemCache::MemCacheError
class MemCacheStoreTest < Test::Unit::TestCase
SESSION_KEY_RE = /^session:[0-9a-z]+/
CONN_TEST_KEY = 'connection_test'
MULTI_TEST_KEY = '0123456789'
TEST_DATA = 'Hello test'
def self.get_mem_cache_if_available
begin
require 'memcache'
cache = MemCache.new('127.0.0.1')
# Test availability of the connection
cache.set(CONN_TEST_KEY, 1)
unless cache.get(CONN_TEST_KEY) == 1
puts 'Warning: memcache server available but corrupted.'
return nil
end
rescue LoadError, MemCache::MemCacheError
return nil
end end
return cache
def set_session_value
session[:foo] = "bar"
head :ok
end
def get_session_value
render :text => "foo: #{session[:foo].inspect}"
end
def rescue_action(e) raise end
end end
CACHE = get_mem_cache_if_available begin
DispatcherApp = ActionController::Dispatcher.new
MemCacheStoreApp = ActionController::Session::MemCacheStore.new(
DispatcherApp, :key => '_session_id')
def test_initialization def setup
assert_raise(ArgumentError) { new_session('session_id' => '!invalid_id') } @integration_session = open_session(MemCacheStoreApp)
new_session do |s|
assert_equal Hash.new, s.cache.get('session:' + s.session_id)
end end
def test_setting_and_getting_session_value
with_test_route_set do
get '/set_session_value'
assert_response :success
assert cookies['_session_id']
get '/get_session_value'
assert_response :success
assert_equal 'foo: "bar"', response.body
end
end
def test_getting_nil_session_value
with_test_route_set do
get '/get_session_value'
assert_response :success
assert_equal 'foo: nil', response.body
end
end
def test_prevents_session_fixation
with_test_route_set do
get '/get_session_value'
assert_response :success
assert_equal 'foo: nil', response.body
session_id = cookies['_session_id']
reset!
get '/set_session_value', :_session_id => session_id
assert_response :success
assert_equal nil, cookies['_session_id']
end
end
rescue LoadError, RuntimeError
$stderr.puts "Skipping MemCacheStoreTest tests. Start memcached and try again."
end end
def test_storage
d = rand(0xffff)
new_session do |s|
session_key = 'session:' + s.session_id
unless CACHE
s.cache.expects(:get).with(session_key) \
.returns(:test => d)
s.cache.expects(:set).with(session_key,
has_entry(:test, d),
0)
end
s[:test] = d
s.close
assert_equal d, s.cache.get(session_key)[:test]
assert_equal d, s[:test]
end
end
def test_deletion
new_session do |s|
session_key = 'session:' + s.session_id
unless CACHE
s.cache.expects(:delete)
s.cache.expects(:get).with(session_key) \
.returns(nil)
end
s[:test] = rand(0xffff)
s.delete
assert_nil s.cache.get(session_key)
end
end
def test_other_session_retrieval
new_session do |sa|
unless CACHE
sa.cache.expects(:set).with('session:' + sa.session_id,
has_entry(:test, TEST_DATA),
0)
end
sa[:test] = TEST_DATA
sa.close
new_session('session_id' => sa.session_id) do |sb|
unless CACHE
sb.cache.expects(:[]).with('session:' + sb.session_id) \
.returns(:test => TEST_DATA)
end
assert_equal(TEST_DATA, sb[:test])
end
end
end
def test_multiple_sessions
s_slots = Array.new(10)
operation = :write
last_data = nil
reads = writes = 0
50.times do
current = rand(10)
s_slots[current] ||= new_session('session_id' => MULTI_TEST_KEY,
'new_session' => true)
s = s_slots[current]
case operation
when :write
last_data = rand(0xffff)
unless CACHE
s.cache.expects(:set).with('session:' + MULTI_TEST_KEY,
{ :test => last_data },
0)
end
s[:test] = last_data
s.close
writes += 1
when :read
# Make CGI::Session#[] think there was no data retrieval yet.
# Normally, the session caches the data during its lifetime.
s.instance_variable_set(:@data, nil)
unless CACHE
s.cache.expects(:[]).with('session:' + MULTI_TEST_KEY) \
.returns(:test => last_data)
end
d = s[:test]
assert_equal(last_data, d, "OK reads: #{reads}, OK writes: #{writes}")
reads += 1
end
operation = rand(5) == 0 ? :write : :read
end
end
private private
def obtain_session_options def with_test_route_set
options = { 'database_manager' => CGI::Session::MemCacheStore, with_routing do |set|
'session_key' => '_test_app_session' set.draw do |map|
} map.with_options :controller => "mem_cache_store_test/test" do |c|
# if don't have running memcache server we use mock instead c.connect "/:action"
unless CACHE end
options['cache'] = c = mock end
c.stubs(:[]).with(regexp_matches(SESSION_KEY_RE)) yield
c.stubs(:get).with(regexp_matches(SESSION_KEY_RE)) \ end
.returns(Hash.new)
c.stubs(:add).with(regexp_matches(SESSION_KEY_RE),
instance_of(Hash),
0)
end end
options
end
def new_session(options = {})
with_cgi do |cgi|
@options = obtain_session_options.merge(options)
session = CGI::Session.new(cgi, @options)
yield session if block_given?
return session
end
end
def with_cgi
ENV['REQUEST_METHOD'] = 'GET'
ENV['HTTP_HOST'] = 'example.com'
ENV['QUERY_STRING'] = ''
cgi = CGI.new('query', StringIO.new(''))
yield cgi if block_given?
cgi
end
end end
end # defined? MemCache
end # uses_mocha

@ -1,84 +1,84 @@
require 'abstract_unit' # require 'abstract_unit'
#
class SessionFixationTest < ActionController::IntegrationTest # class SessionFixationTest < ActionController::IntegrationTest
class TestController < ActionController::Base # class TestController < ActionController::Base
session :session_key => '_myapp_session_id', # session :session_key => '_myapp_session_id',
:secret => CGI::Session.generate_unique_id, # :secret => CGI::Session.generate_unique_id,
:except => :default_session_key # :except => :default_session_key
#
session :cookie_only => false, # session :cookie_only => false,
:only => :allow_session_fixation # :only => :allow_session_fixation
#
def default_session_key # def default_session_key
render :text => "default_session_key" # render :text => "default_session_key"
end # end
#
def custom_session_key # def custom_session_key
render :text => "custom_session_key: #{params[:id]}" # render :text => "custom_session_key: #{params[:id]}"
end # end
#
def allow_session_fixation # def allow_session_fixation
render :text => "allow_session_fixation" # render :text => "allow_session_fixation"
end # end
#
def rescue_action(e) raise end # def rescue_action(e) raise end
end # end
#
def setup # def setup
@controller = TestController.new # @controller = TestController.new
end # end
#
def test_should_be_able_to_make_a_successful_request # def test_should_be_able_to_make_a_successful_request
with_test_route_set do # with_test_route_set do
assert_nothing_raised do # assert_nothing_raised do
get '/custom_session_key', :id => "1" # get '/custom_session_key', :id => "1"
end # end
assert_equal 'custom_session_key: 1', @controller.response.body # assert_equal 'custom_session_key: 1', @controller.response.body
assert_not_nil @controller.session # assert_not_nil @controller.session
end # end
end # end
#
def test_should_catch_session_fixation_attempt # def test_should_catch_session_fixation_attempt
with_test_route_set do # with_test_route_set do
assert_raises(ActionController::RackRequest::SessionFixationAttempt) do # assert_raises(ActionController::RackRequest::SessionFixationAttempt) do
get '/custom_session_key', :_myapp_session_id => "42" # get '/custom_session_key', :_myapp_session_id => "42"
end # end
assert_nil @controller.session # assert_nil @controller.session
end # end
end # end
#
def test_should_not_catch_session_fixation_attempt_when_cookie_only_setting_is_disabled # def test_should_not_catch_session_fixation_attempt_when_cookie_only_setting_is_disabled
with_test_route_set do # with_test_route_set do
assert_nothing_raised do # assert_nothing_raised do
get '/allow_session_fixation', :_myapp_session_id => "42" # get '/allow_session_fixation', :_myapp_session_id => "42"
end # end
assert !@controller.response.body.blank? # assert !@controller.response.body.blank?
assert_not_nil @controller.session # assert_not_nil @controller.session
end # end
end # end
#
def test_should_catch_session_fixation_attempt_with_default_session_key # def test_should_catch_session_fixation_attempt_with_default_session_key
# using the default session_key is not possible with cookie store # # using the default session_key is not possible with cookie store
ActionController::Base.session_store = :p_store # ActionController::Base.session_store = :p_store
#
with_test_route_set do # with_test_route_set do
assert_raises ActionController::RackRequest::SessionFixationAttempt do # assert_raises ActionController::RackRequest::SessionFixationAttempt do
get '/default_session_key', :_session_id => "42" # get '/default_session_key', :_session_id => "42"
end # end
assert_nil @controller.response # assert_nil @controller.response
assert_nil @controller.session # assert_nil @controller.session
end # end
end # end
#
private # private
def with_test_route_set # def with_test_route_set
with_routing do |set| # with_routing do |set|
set.draw do |map| # set.draw do |map|
map.with_options :controller => "session_fixation_test/test" do |c| # map.with_options :controller => "session_fixation_test/test" do |c|
c.connect "/:action" # c.connect "/:action"
end # end
end # end
yield # yield
end # end
end # end
end # end

@ -1,178 +0,0 @@
require 'abstract_unit'
class SessionManagementTest < Test::Unit::TestCase
class SessionOffController < ActionController::Base
session :off
def show
render :text => "done"
end
def tell
render :text => "done"
end
end
class SessionOffOnController < ActionController::Base
session :off
session :on, :only => :tell
def show
render :text => "done"
end
def tell
render :text => "done"
end
end
class TestController < ActionController::Base
session :off, :only => :show
session :session_secure => true, :except => :show
session :off, :only => :conditional,
:if => Proc.new { |r| r.parameters[:ws] }
def show
render :text => "done"
end
def tell
render :text => "done"
end
def conditional
render :text => ">>>#{params[:ws]}<<<"
end
end
class SpecializedController < SessionOffController
session :disabled => false, :only => :something
def something
render :text => "done"
end
def another
render :text => "done"
end
end
class AssociationCachingTestController < ActionController::Base
class ObjectWithAssociationCache
def initialize
@cached_associations = false
end
def fetch_associations
@cached_associations = true
end
def clear_association_cache
@cached_associations = false
end
def has_cached_associations?
@cached_associations
end
end
def show
session[:object] = ObjectWithAssociationCache.new
session[:object].fetch_associations
if session[:object].has_cached_associations?
render :text => "has cached associations"
else
render :text => "does not have cached associations"
end
end
def tell
if session[:object]
if session[:object].has_cached_associations?
render :text => "has cached associations"
else
render :text => "does not have cached associations"
end
else
render :text => "there is no object"
end
end
end
def setup
@request, @response = ActionController::TestRequest.new,
ActionController::TestResponse.new
end
def test_session_off_globally
@controller = SessionOffController.new
get :show
assert_equal false, @request.session_options
get :tell
assert_equal false, @request.session_options
end
def test_session_off_then_on_globally
@controller = SessionOffOnController.new
get :show
assert_equal false, @request.session_options
get :tell
assert_instance_of Hash, @request.session_options
assert_equal false, @request.session_options[:disabled]
end
def test_session_off_conditionally
@controller = TestController.new
get :show
assert_equal false, @request.session_options
get :tell
assert_instance_of Hash, @request.session_options
assert @request.session_options[:session_secure]
end
def test_controller_specialization_overrides_settings
@controller = SpecializedController.new
get :something
assert_instance_of Hash, @request.session_options
get :another
assert_equal false, @request.session_options
end
def test_session_off_with_if
@controller = TestController.new
get :conditional
assert_instance_of Hash, @request.session_options
get :conditional, :ws => "ws"
assert_equal false, @request.session_options
end
def test_session_store_setting
ActionController::Base.session_store = :drb_store
assert_equal CGI::Session::DRbStore, ActionController::Base.session_store
if Object.const_defined?(:ActiveRecord)
ActionController::Base.session_store = :active_record_store
assert_equal CGI::Session::ActiveRecordStore, ActionController::Base.session_store
end
end
def test_process_cleanup_with_session_management_support
@controller = AssociationCachingTestController.new
get :show
assert_equal "has cached associations", @response.body
get :tell
assert_equal "does not have cached associations", @response.body
end
def test_session_is_enabled
@controller = TestController.new
get :show
assert_nothing_raised do
assert_equal false, @controller.session_enabled?
end
get :tell
assert @controller.session_enabled?
end
end

@ -2,8 +2,6 @@
class WebServiceTest < ActionController::IntegrationTest class WebServiceTest < ActionController::IntegrationTest
class TestController < ActionController::Base class TestController < ActionController::Base
session :off
def assign_parameters def assign_parameters
if params[:full] if params[:full]
render :text => dump_params_keys render :text => dump_params_keys

@ -60,6 +60,7 @@ def self.load_all!
autoload :Schema, 'active_record/schema' autoload :Schema, 'active_record/schema'
autoload :SchemaDumper, 'active_record/schema_dumper' autoload :SchemaDumper, 'active_record/schema_dumper'
autoload :Serialization, 'active_record/serialization' autoload :Serialization, 'active_record/serialization'
autoload :SessionStore, 'active_record/session_store'
autoload :TestCase, 'active_record/test_case' autoload :TestCase, 'active_record/test_case'
autoload :Timestamp, 'active_record/timestamp' autoload :Timestamp, 'active_record/timestamp'
autoload :Transactions, 'active_record/transactions' autoload :Transactions, 'active_record/transactions'

@ -0,0 +1,319 @@
module ActiveRecord
# A session store backed by an Active Record class. A default class is
# provided, but any object duck-typing to an Active Record Session class
# with text +session_id+ and +data+ attributes is sufficient.
#
# The default assumes a +sessions+ tables with columns:
# +id+ (numeric primary key),
# +session_id+ (text, or longtext if your session data exceeds 65K), and
# +data+ (text or longtext; careful if your session data exceeds 65KB).
# The +session_id+ column should always be indexed for speedy lookups.
# Session data is marshaled to the +data+ column in Base64 format.
# If the data you write is larger than the column's size limit,
# ActionController::SessionOverflowError will be raised.
#
# You may configure the table name, primary key, and data column.
# For example, at the end of <tt>config/environment.rb</tt>:
# ActiveRecord::SessionStore::Session.table_name = 'legacy_session_table'
# ActiveRecord::SessionStore::Session.primary_key = 'session_id'
# ActiveRecord::SessionStore::Session.data_column_name = 'legacy_session_data'
# Note that setting the primary key to the +session_id+ frees you from
# having a separate +id+ column if you don't want it. However, you must
# set <tt>session.model.id = session.session_id</tt> by hand! A before filter
# on ApplicationController is a good place.
#
# Since the default class is a simple Active Record, you get timestamps
# for free if you add +created_at+ and +updated_at+ datetime columns to
# the +sessions+ table, making periodic session expiration a snap.
#
# You may provide your own session class implementation, whether a
# feature-packed Active Record or a bare-metal high-performance SQL
# store, by setting
# ActiveRecord::SessionStore.session_class = MySessionClass
# You must implement these methods:
# self.find_by_session_id(session_id)
# initialize(hash_of_session_id_and_data)
# attr_reader :session_id
# attr_accessor :data
# save
# destroy
#
# The example SqlBypass class is a generic SQL session store. You may
# use it as a basis for high-performance database-specific stores.
class SessionStore < ActionController::Session::AbstractStore
# The default Active Record class.
class Session < ActiveRecord::Base
##
# :singleton-method:
# Customizable data column name. Defaults to 'data'.
cattr_accessor :data_column_name
self.data_column_name = 'data'
before_save :marshal_data!
before_save :raise_on_session_data_overflow!
class << self
# Don't try to reload ARStore::Session in dev mode.
def reloadable? #:nodoc:
false
end
def data_column_size_limit
@data_column_size_limit ||= columns_hash[@@data_column_name].limit
end
# Hook to set up sessid compatibility.
def find_by_session_id(session_id)
setup_sessid_compatibility!
find_by_session_id(session_id)
end
def marshal(data)
ActiveSupport::Base64.encode64(Marshal.dump(data)) if data
end
def unmarshal(data)
Marshal.load(ActiveSupport::Base64.decode64(data)) if data
end
def create_table!
connection.execute <<-end_sql
CREATE TABLE #{table_name} (
id INTEGER PRIMARY KEY,
#{connection.quote_column_name('session_id')} TEXT UNIQUE,
#{connection.quote_column_name(@@data_column_name)} TEXT(255)
)
end_sql
end
def drop_table!
connection.execute "DROP TABLE #{table_name}"
end
private
# Compatibility with tables using sessid instead of session_id.
def setup_sessid_compatibility!
# Reset column info since it may be stale.
reset_column_information
if columns_hash['sessid']
def self.find_by_session_id(*args)
find_by_sessid(*args)
end
define_method(:session_id) { sessid }
define_method(:session_id=) { |session_id| self.sessid = session_id }
else
def self.find_by_session_id(session_id)
find :first, :conditions => ["session_id #{attribute_condition(session_id)}", session_id]
end
end
end
end
# Lazy-unmarshal session state.
def data
@data ||= self.class.unmarshal(read_attribute(@@data_column_name)) || {}
end
attr_writer :data
# Has the session been loaded yet?
def loaded?
!!@data
end
private
def marshal_data!
return false if !loaded?
write_attribute(@@data_column_name, self.class.marshal(self.data))
end
# Ensures that the data about to be stored in the database is not
# larger than the data storage column. Raises
# ActionController::SessionOverflowError.
def raise_on_session_data_overflow!
return false if !loaded?
limit = self.class.data_column_size_limit
if loaded? and limit and read_attribute(@@data_column_name).size > limit
raise ActionController::SessionOverflowError
end
end
end
# A barebones session store which duck-types with the default session
# store but bypasses Active Record and issues SQL directly. This is
# an example session model class meant as a basis for your own classes.
#
# The database connection, table name, and session id and data columns
# are configurable class attributes. Marshaling and unmarshaling
# are implemented as class methods that you may override. By default,
# marshaling data is
#
# ActiveSupport::Base64.encode64(Marshal.dump(data))
#
# and unmarshaling data is
#
# Marshal.load(ActiveSupport::Base64.decode64(data))
#
# This marshaling behavior is intended to store the widest range of
# binary session data in a +text+ column. For higher performance,
# store in a +blob+ column instead and forgo the Base64 encoding.
class SqlBypass
##
# :singleton-method:
# Use the ActiveRecord::Base.connection by default.
cattr_accessor :connection
##
# :singleton-method:
# The table name defaults to 'sessions'.
cattr_accessor :table_name
@@table_name = 'sessions'
##
# :singleton-method:
# The session id field defaults to 'session_id'.
cattr_accessor :session_id_column
@@session_id_column = 'session_id'
##
# :singleton-method:
# The data field defaults to 'data'.
cattr_accessor :data_column
@@data_column = 'data'
class << self
def connection
@@connection ||= ActiveRecord::Base.connection
end
# Look up a session by id and unmarshal its data if found.
def find_by_session_id(session_id)
if record = @@connection.select_one("SELECT * FROM #{@@table_name} WHERE #{@@session_id_column}=#{@@connection.quote(session_id)}")
new(:session_id => session_id, :marshaled_data => record['data'])
end
end
def marshal(data)
ActiveSupport::Base64.encode64(Marshal.dump(data)) if data
end
def unmarshal(data)
Marshal.load(ActiveSupport::Base64.decode64(data)) if data
end
def create_table!
@@connection.execute <<-end_sql
CREATE TABLE #{table_name} (
id INTEGER PRIMARY KEY,
#{@@connection.quote_column_name(session_id_column)} TEXT UNIQUE,
#{@@connection.quote_column_name(data_column)} TEXT
)
end_sql
end
def drop_table!
@@connection.execute "DROP TABLE #{table_name}"
end
end
attr_reader :session_id
attr_writer :data
# Look for normal and marshaled data, self.find_by_session_id's way of
# telling us to postpone unmarshaling until the data is requested.
# We need to handle a normal data attribute in case of a new record.
def initialize(attributes)
@session_id, @data, @marshaled_data = attributes[:session_id], attributes[:data], attributes[:marshaled_data]
@new_record = @marshaled_data.nil?
end
def new_record?
@new_record
end
# Lazy-unmarshal session state.
def data
unless @data
if @marshaled_data
@data, @marshaled_data = self.class.unmarshal(@marshaled_data) || {}, nil
else
@data = {}
end
end
@data
end
def loaded?
!!@data
end
def save
return false if !loaded?
marshaled_data = self.class.marshal(data)
if @new_record
@new_record = false
@@connection.update <<-end_sql, 'Create session'
INSERT INTO #{@@table_name} (
#{@@connection.quote_column_name(@@session_id_column)},
#{@@connection.quote_column_name(@@data_column)} )
VALUES (
#{@@connection.quote(session_id)},
#{@@connection.quote(marshaled_data)} )
end_sql
else
@@connection.update <<-end_sql, 'Update session'
UPDATE #{@@table_name}
SET #{@@connection.quote_column_name(@@data_column)}=#{@@connection.quote(marshaled_data)}
WHERE #{@@connection.quote_column_name(@@session_id_column)}=#{@@connection.quote(session_id)}
end_sql
end
end
def destroy
unless @new_record
@@connection.delete <<-end_sql, 'Destroy session'
DELETE FROM #{@@table_name}
WHERE #{@@connection.quote_column_name(@@session_id_column)}=#{@@connection.quote(session_id)}
end_sql
end
end
end
# The class used for session storage. Defaults to
# ActiveRecord::SessionStore::Session
cattr_accessor :session_class
self.session_class = Session
SESSION_RECORD_KEY = 'rack.session.record'.freeze
private
def get_session(env, sid)
Base.silence do
sid ||= generate_sid
session = @@session_class.find_by_session_id(sid)
session ||= @@session_class.new(:session_id => sid, :data => {})
env[SESSION_RECORD_KEY] = session
[sid, session.data]
end
end
def set_session(env, sid, session_data)
Base.silence do
record = env[SESSION_RECORD_KEY]
record.data = session_data
return false unless record.save
session_data = record.data
if session_data && session_data.respond_to?(:each_value)
session_data.each_value do |obj|
obj.clear_association_cache if obj.respond_to?(:clear_association_cache)
end
end
end
return true
end
end
end

@ -39,7 +39,7 @@ def logger
nil nil
end end
end end
def backtrace_cleaner def backtrace_cleaner
@@backtrace_cleaner ||= begin @@backtrace_cleaner ||= begin
# Relies on ActiveSupport, so we have to lazy load to postpone definition until AS has been loaded # Relies on ActiveSupport, so we have to lazy load to postpone definition until AS has been loaded
@ -148,7 +148,6 @@ def process
initialize_dependency_mechanism initialize_dependency_mechanism
initialize_whiny_nils initialize_whiny_nils
initialize_temporary_session_directory
initialize_time_zone initialize_time_zone
initialize_i18n initialize_i18n
@ -501,13 +500,6 @@ def initialize_whiny_nils
require('active_support/whiny_nil') if configuration.whiny_nils require('active_support/whiny_nil') if configuration.whiny_nils
end end
def initialize_temporary_session_directory
if configuration.frameworks.include?(:action_controller)
session_path = "#{configuration.root_path}/tmp/sessions/"
ActionController::Base.session_options[:tmpdir] = File.exist?(session_path) ? session_path : Dir::tmpdir
end
end
# Sets the default value for Time.zone, and turns on ActiveRecord::Base#time_zone_aware_attributes. # Sets the default value for Time.zone, and turns on ActiveRecord::Base#time_zone_aware_attributes.
# If assigned value cannot be matched to a TimeZone, an exception will be raised. # If assigned value cannot be matched to a TimeZone, an exception will be raised.
def initialize_time_zone def initialize_time_zone
@ -529,7 +521,7 @@ def initialize_time_zone
end end
end end
# Set the i18n configuration from config.i18n but special-case for the load_path which should be # Set the i18n configuration from config.i18n but special-case for the load_path which should be
# appended to what's already set instead of overwritten. # appended to what's already set instead of overwritten.
def initialize_i18n def initialize_i18n
configuration.i18n.each do |setting, value| configuration.i18n.each do |setting, value|

@ -380,7 +380,7 @@ namespace :db do
end end
namespace :sessions do namespace :sessions do
desc "Creates a sessions migration for use with CGI::Session::ActiveRecordStore" desc "Creates a sessions migration for use with ActiveRecord::SessionStore"
task :create => :environment do task :create => :environment do
raise "Task unavailable to this database (no migration support)" unless ActiveRecord::Base.connection.supports_migrations? raise "Task unavailable to this database (no migration support)" unless ActiveRecord::Base.connection.supports_migrations?
require 'rails_generator' require 'rails_generator'

@ -4,6 +4,7 @@
unless defined? ApplicationController unless defined? ApplicationController
class ApplicationController < ActionController::Base; end class ApplicationController < ActionController::Base; end
ActionController::Base.session_store = nil
end end
require 'dispatcher' require 'dispatcher'