lib/rack/session/abstract/id.rb in rack-1.2.8 vs lib/rack/session/abstract/id.rb in rack-1.3.0.beta
- old
+ new
@@ -2,17 +2,140 @@
# bugrep: Andreas Zehnder
require 'time'
require 'rack/request'
require 'rack/response'
+begin
+ require 'securerandom'
+rescue LoadError
+ # We just won't get securerandom
+end
module Rack
module Session
module Abstract
+ ENV_SESSION_KEY = 'rack.session'.freeze
+ ENV_SESSION_OPTIONS_KEY = 'rack.session.options'.freeze
+ # Thin wrapper around Hash that allows us to lazily load session id into session_options.
+
+ class OptionsHash < Hash #:nodoc:
+ def initialize(by, env, default_options)
+ @by = by
+ @env = env
+ @session_id_loaded = false
+ merge!(default_options)
+ end
+
+ def [](key)
+ load_session_id! if key == :id && session_id_not_loaded?
+ super
+ end
+
+ private
+
+ def session_id_not_loaded?
+ !key?(:id) && !@session_id_loaded
+ end
+
+ def load_session_id!
+ self[:id] = @by.send(:extract_session_id, @env)
+ @session_id_loaded = true
+ end
+ end
+
+ # SessionHash is responsible to lazily load the session from store.
+
+ class SessionHash < Hash
+ def initialize(by, env)
+ super()
+ @by = by
+ @env = env
+ @loaded = false
+ end
+
+ def [](key)
+ load_for_read!
+ super(key.to_s)
+ end
+
+ def has_key?(key)
+ load_for_read!
+ super(key.to_s)
+ end
+ alias :key? :has_key?
+ alias :include? :has_key?
+
+ def []=(key, value)
+ load_for_write!
+ super(key.to_s, value)
+ end
+
+ def clear
+ load_for_write!
+ super
+ end
+
+ def to_hash
+ load_for_read!
+ h = {}.replace(self)
+ h.delete_if { |k,v| v.nil? }
+ h
+ end
+
+ def update(hash)
+ load_for_write!
+ super(stringify_keys(hash))
+ end
+
+ def delete(key)
+ load_for_write!
+ super(key.to_s)
+ end
+
+ def inspect
+ load_for_read!
+ super
+ end
+
+ def exists?
+ return @exists if instance_variable_defined?(:@exists)
+ @exists = @by.send(:session_exists?, @env)
+ end
+
+ def loaded?
+ @loaded
+ end
+
+ private
+
+ def load_for_read!
+ load! if !loaded? && exists?
+ end
+
+ def load_for_write!
+ load! unless loaded?
+ end
+
+ def load!
+ id, session = @by.send(:load_session, @env)
+ @env[ENV_SESSION_OPTIONS_KEY][:id] = id
+ replace(stringify_keys(session))
+ @loaded = true
+ end
+
+ def stringify_keys(other)
+ hash = {}
+ other.each do |key, value|
+ hash[key.to_s] = value
+ end
+ hash
+ end
+ end
+
# ID sets up a basic framework for implementing an id based sessioning
# service. Cookies sent to the client for maintaining sessions will only
# contain an id reference. Only #get_session and #set_session are
# required to be overwritten.
#
@@ -32,95 +155,175 @@
# env['rack.session.options']. Additionally the id of the session can be
# found within the options hash at the key :id. It is highly not
# recommended to change its value.
#
# Is Rack::Utils::Context compatible.
+ #
+ # Not included by default; you must require 'rack/session/abstract/id'
+ # to use.
class ID
DEFAULT_OPTIONS = {
+ :key => 'rack.session',
:path => '/',
:domain => nil,
:expire_after => nil,
:secure => false,
:httponly => true,
:defer => false,
:renew => false,
- :sidbits => 128
+ :sidbits => 128,
+ :cookie_only => true,
+ :secure_random => begin ::SecureRandom rescue false end
}
attr_reader :key, :default_options
+
def initialize(app, options={})
@app = app
- @key = options[:key] || "rack.session"
@default_options = self.class::DEFAULT_OPTIONS.merge(options)
+ @key = options[:key] || "rack.session"
+ @cookie_only = @default_options.delete(:cookie_only)
+ initialize_sid
end
def call(env)
context(env)
end
def context(env, app=@app)
- load_session(env)
+ prepare_session(env)
status, headers, body = app.call(env)
commit_session(env, status, headers, body)
end
private
+ def initialize_sid
+ sidbits = @default_options.delete(:sidbits)
+ @sid_secure = @default_options.delete(:secure_random)
+ @sid_template = "%0#{sidbits / 4}x"
+ @sid_rand_width = (2**sidbits - 1)
+ end
+
# Generate a new session id using Ruby #rand. The size of the
# session id is controlled by the :sidbits option.
# Monkey patch this to use custom methods for session id generation.
def generate_sid
- "%0#{@default_options[:sidbits] / 4}x" %
- rand(2**@default_options[:sidbits] - 1)
+ r = if @sid_secure
+ SecureRandom.random_number(@sid_rand_width)
+ else
+ Kernel.rand(@sid_rand_width)
+ end
+ @sid_template % r
end
+ # Sets the lazy session at 'rack.session' and places options and session
+ # metadata into 'rack.session.options'.
+
+ def prepare_session(env)
+ env[ENV_SESSION_KEY] = SessionHash.new(self, env)
+ env[ENV_SESSION_OPTIONS_KEY] = OptionsHash.new(self, env, @default_options)
+ end
+
# Extracts the session id from provided cookies and passes it and the
- # environment to #get_session. It then sets the resulting session into
- # 'rack.session', and places options and session metadata into
- # 'rack.session.options'.
+ # environment to #get_session.
def load_session(env)
+ sid = current_session_id(env)
+ sid, session = get_session(env, sid)
+ [sid, session || {}]
+ end
+
+ # Extract session id from request object.
+
+ def extract_session_id(env)
request = Rack::Request.new(env)
- session_id = request.cookies[@key]
+ sid = request.cookies[@key]
+ sid ||= request.params[@key] unless @cookie_only
+ sid
+ end
- begin
- session_id, session = get_session(env, session_id)
- env['rack.session'] = session
- rescue
- env['rack.session'] = Hash.new
- end
+ # Returns the current session id from the OptionsHash.
- env['rack.session.options'] = @default_options.
- merge(:id => session_id)
+ def current_session_id(env)
+ env[ENV_SESSION_OPTIONS_KEY][:id]
end
+ # Check if the session exists or not.
+
+ def session_exists?(env)
+ value = current_session_id(env)
+ value && !value.empty?
+ end
+
+ # Session should be commited if it was loaded, any of specific options like :renew, :drop
+ # or :expire_after was given and the security permissions match.
+
+ def commit_session?(env, session, options)
+ (loaded_session?(session) || force_options?(options)) && secure_session?(env, options)
+ end
+
+ def loaded_session?(session)
+ !session.is_a?(SessionHash) || session.loaded?
+ end
+
+ def force_options?(options)
+ options.values_at(:renew, :drop, :defer, :expire_after).any?
+ end
+
+ def secure_session?(env, options)
+ return true unless options[:secure]
+ request = Rack::Request.new(env)
+ request.ssl?
+ end
+
# Acquires the session from the environment and the session id from
# the session options and passes them to #set_session. If successful
# and the :defer option is not true, a cookie will be added to the
# response with the session's id.
def commit_session(env, status, headers, body)
session = env['rack.session']
options = env['rack.session.options']
- session_id = options[:id]
- if not session_id = set_session(env, session_id, session, options)
+ if options[:drop] || options[:renew]
+ session_id = destroy_session(env, options[:id] || generate_sid, options)
+ return [status, headers, body] unless session_id
+ end
+
+ return [status, headers, body] unless commit_session?(env, session, options)
+
+ session.send(:load!) unless loaded_session?(session)
+ session = session.to_hash
+ session_id ||= options[:id] || generate_sid
+
+ if not data = set_session(env, session_id, session, options)
env["rack.errors"].puts("Warning! #{self.class.name} failed to save session. Content dropped.")
elsif options[:defer] and not options[:renew]
env["rack.errors"].puts("Defering cookie for #{session_id}") if $VERBOSE
else
cookie = Hash.new
- cookie[:value] = session_id
- cookie[:expires] = Time.now + options[:expire_after] unless options[:expire_after].nil?
- Utils.set_cookie_header!(headers, @key, cookie.merge(options))
+ cookie[:value] = data
+ cookie[:expires] = Time.now + options[:expire_after] if options[:expire_after]
+ set_cookie(env, headers, cookie.merge!(options))
end
[status, headers, body]
end
+ # Sets the cookie back to the client with session id. We skip the cookie
+ # setting if the value didn't change (sid is the same) or expires was given.
+
+ def set_cookie(env, headers, cookie)
+ request = Rack::Request.new(env)
+ if request.cookies[@key] != cookie[:value] || cookie[:expires]
+ Utils.set_cookie_header!(headers, @key, cookie)
+ end
+ end
+
# All thread safety and session retrival proceedures should occur here.
# Should return [session_id, session].
# If nil is provided as the session id, generation of a new valid id
# should occur within.
@@ -129,11 +332,19 @@
end
# All thread safety and session storage proceedures should occur here.
# Should return true or false dependant on whether or not the session
# was saved or not.
+
def set_session(env, sid, session, options)
raise '#set_session not implemented.'
+ end
+
+ # All thread safety and session destroy proceedures should occur here.
+ # Should return a new session id or nil if options[:drop]
+
+ def destroy_session(env, sid, options)
+ raise '#destroy_session not implemented'
end
end
end
end
end