Switch to Rack based session stores.
This commit is contained in:
parent
e8c1915416
commit
ed70830713
@ -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
|
||||||
|
129
actionpack/lib/action_controller/session/abstract_store.rb
Normal file
129
actionpack/lib/action_controller/session/abstract_store.rb
Normal file
@ -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 it’s 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'
|
||||||
|
319
activerecord/lib/active_record/session_store.rb
Normal file
319
activerecord/lib/active_record/session_store.rb
Normal file
@ -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'
|
||||||
|
Loading…
Reference in New Issue
Block a user