aboutsummaryrefslogtreecommitdiffstats
path: root/actionpack/lib/action_controller/session
diff options
context:
space:
mode:
Diffstat (limited to 'actionpack/lib/action_controller/session')
-rw-r--r--actionpack/lib/action_controller/session/abstract_store.rb168
-rw-r--r--actionpack/lib/action_controller/session/cookie_store.rb224
-rw-r--r--actionpack/lib/action_controller/session/management.rb54
-rw-r--r--actionpack/lib/action_controller/session/mem_cache_store.rb51
4 files changed, 54 insertions, 443 deletions
diff --git a/actionpack/lib/action_controller/session/abstract_store.rb b/actionpack/lib/action_controller/session/abstract_store.rb
deleted file mode 100644
index 9434c2e05e..0000000000
--- a/actionpack/lib/action_controller/session/abstract_store.rb
+++ /dev/null
@@ -1,168 +0,0 @@
-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)
- super()
- @by = by
- @env = env
- @loaded = false
- end
-
- def id
- load! unless @loaded
- @id
- end
-
- def session_id
- ActiveSupport::Deprecation.warn(
- "ActionController::Session::AbstractStore::SessionHash#session_id" +
- "has been deprecated.Please use #id instead.", caller)
- id
- end
-
- def [](key)
- load! unless @loaded
- super
- end
-
- def []=(key, value)
- load! unless @loaded
- super
- end
-
- def to_hash
- h = {}.replace(self)
- h.delete_if { |k,v| v.nil? }
- h
- end
-
- def data
- ActiveSupport::Deprecation.warn(
- "ActionController::Session::AbstractStore::SessionHash#data" +
- "has been deprecated.Please use #to_hash instead.", caller)
- to_hash
- end
-
- private
- def loaded?
- @loaded
- end
-
- def load!
- @id, session = @by.send(:load_session, @env)
- replace(session)
- @loaded = true
- end
- end
-
- DEFAULT_OPTIONS = {
- :key => '_session_id',
- :path => '/',
- :domain => nil,
- :expire_after => nil,
- :secure => false,
- :httponly => true,
- :cookie_only => true
- }
-
- def initialize(app, options = {})
- # Process legacy CGI options
- options = options.symbolize_keys
- if options.has_key?(:session_path)
- options[:path] = options.delete(:session_path)
- end
- if options.has_key?(:session_key)
- options[:key] = options.delete(:session_key)
- end
- if options.has_key?(:session_http_only)
- options[:httponly] = options.delete(:session_http_only)
- end
-
- @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)
-
- env[ENV_SESSION_KEY] = session
- env[ENV_SESSION_OPTIONS_KEY] = @default_options.dup
-
- response = @app.call(env)
-
- session_data = env[ENV_SESSION_KEY]
- options = env[ENV_SESSION_OPTIONS_KEY]
-
- if !session_data.is_a?(AbstractStore::SessionHash) || session_data.send(:loaded?) || options[:expire_after]
- session_data.send(:load!) if session_data.is_a?(AbstractStore::SessionHash) && !session_data.send(:loaded?)
-
- if session_data.is_a?(AbstractStore::SessionHash)
- sid = session_data.id
- else
- sid = generate_sid
- end
-
- unless set_session(env, sid, session_data.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
diff --git a/actionpack/lib/action_controller/session/cookie_store.rb b/actionpack/lib/action_controller/session/cookie_store.rb
deleted file mode 100644
index 5a728d1877..0000000000
--- a/actionpack/lib/action_controller/session/cookie_store.rb
+++ /dev/null
@@ -1,224 +0,0 @@
-module ActionController
- 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
-
- DEFAULT_OPTIONS = {
- :key => '_session_id',
- :domain => nil,
- :path => "/",
- :expire_after => nil,
- :httponly => true
- }.freeze
-
- ENV_SESSION_KEY = "rack.session".freeze
- ENV_SESSION_OPTIONS_KEY = "rack.session.options".freeze
- HTTP_SET_COOKIE = "Set-Cookie".freeze
-
- # Raised when storing more than 4K of session data.
- class CookieOverflow < StandardError; end
-
- def initialize(app, options = {})
- # Process legacy CGI options
- options = options.symbolize_keys
- if options.has_key?(:session_path)
- options[:path] = options.delete(:session_path)
- end
- if options.has_key?(:session_key)
- options[:key] = options.delete(:session_key)
- end
- if options.has_key?(:session_http_only)
- options[:httponly] = options.delete(:session_http_only)
- end
-
- @app = app
-
- # The session_key option is required.
- ensure_session_key(options[:key])
- @key = options.delete(:key).freeze
-
- # The secret option is required.
- ensure_secret_secure(options[:secret])
- @secret = options.delete(:secret).freeze
-
- @digest = options.delete(:digest) || 'SHA1'
- @verifier = verifier_for(@secret, @digest)
-
- @default_options = DEFAULT_OPTIONS.merge(options).freeze
-
- freeze
- end
-
- def call(env)
- env[ENV_SESSION_KEY] = AbstractStore::SessionHash.new(self, env)
- env[ENV_SESSION_OPTIONS_KEY] = @default_options
-
- status, headers, body = @app.call(env)
-
- session_data = env[ENV_SESSION_KEY]
- options = env[ENV_SESSION_OPTIONS_KEY]
-
- if !session_data.is_a?(AbstractStore::SessionHash) || session_data.send(:loaded?) || options[:expire_after]
- session_data.send(:load!) if session_data.is_a?(AbstractStore::SessionHash) && !session_data.send(:loaded?)
- session_data = marshal(session_data.to_hash)
-
- raise CookieOverflow if session_data.size > MAX
-
- 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]
- data = unmarshal(session_data) || persistent_session_id!({})
- [data[:session_id], data]
- end
-
- # Marshal a session hash into safe cookie data. Include an integrity hash.
- def marshal(session)
- @verifier.generate(persistent_session_id!(session))
- end
-
- # Unmarshal cookie data to a hash and verify its integrity.
- def unmarshal(cookie)
- persistent_session_id!(@verifier.verify(cookie)) if cookie
- rescue ActiveSupport::MessageVerifier::InvalidSignature
- nil
- end
-
- def ensure_session_key(key)
- if key.blank?
- raise ArgumentError, 'A key is required to write a ' +
- 'cookie containing the session data. Use ' +
- 'config.action_controller.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 = { :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
-
- def generate_sid
- ActiveSupport::SecureRandom.hex(16)
- end
-
- def persistent_session_id!(data)
- (data ||= {}).merge!(inject_persistent_session_id(data))
- end
-
- def inject_persistent_session_id(data)
- requires_session_id?(data) ? { :session_id => generate_sid } : {}
- end
-
- def requires_session_id?(data)
- if data
- data.respond_to?(:key?) && !data.key?(:session_id)
- else
- true
- end
- end
- end
- end
-end
diff --git a/actionpack/lib/action_controller/session/management.rb b/actionpack/lib/action_controller/session/management.rb
new file mode 100644
index 0000000000..ffce8e1bd1
--- /dev/null
+++ b/actionpack/lib/action_controller/session/management.rb
@@ -0,0 +1,54 @@
+module ActionController #:nodoc:
+ module SessionManagement #:nodoc:
+ def self.included(base)
+ base.class_eval do
+ extend ClassMethods
+ end
+ end
+
+ module ClassMethods
+ # 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>),
+ # but you can also specify one of the other included stores (<tt>:active_record_store</tt>,
+ # <tt>:mem_cache_store</tt>, or your own custom class.
+ def session_store=(store)
+ if store == :active_record_store
+ self.session_store = ActiveRecord::SessionStore
+ else
+ @@session_store = store.is_a?(Symbol) ?
+ Session.const_get(store.to_s.camelize) :
+ store
+ end
+ end
+
+ # Returns the session store class currently used.
+ def session_store
+ if defined? @@session_store
+ @@session_store
+ else
+ ActionDispatch::Session::CookieStore
+ end
+ end
+
+ def session=(options = {})
+ self.session_store = nil if options.delete(:disabled)
+ session_options.merge!(options)
+ end
+
+ # Returns the hash used to configure the session. Example use:
+ #
+ # ActionController::Base.session_options[:secure] = true # session only available over HTTPS
+ def session_options
+ @session_options ||= {}
+ end
+
+ def session(*args)
+ ActiveSupport::Deprecation.warn(
+ "Disabling sessions for a single controller has been deprecated. " +
+ "Sessions are now lazy loaded. So if you don't access them, " +
+ "consider them off. You can still modify the session cookie " +
+ "options with request.session_options.", caller)
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/session/mem_cache_store.rb b/actionpack/lib/action_controller/session/mem_cache_store.rb
deleted file mode 100644
index f745715a97..0000000000
--- a/actionpack/lib/action_controller/session/mem_cache_store.rb
+++ /dev/null
@@ -1,51 +0,0 @@
-begin
- require_library_or_gem 'memcache'
-
- module ActionController
- module Session
- class MemCacheStore < AbstractStore
- def initialize(app, options = {})
- # Support old :expires option
- options[:expire_after] ||= options[:expires]
-
- super
-
- @default_options = {
- :namespace => 'rack:session',
- :memcache_server => 'localhost:11211'
- }.merge(@default_options)
-
- @pool = options[:cache] || MemCache.new(@default_options[:memcache_server], @default_options)
- unless @pool.servers.any? { |s| s.alive? }
- raise "#{self} unable to find server during initialization."
- end
- @mutex = Mutex.new
-
- super
- end
-
- private
- def get_session(env, sid)
- sid ||= generate_sid
- begin
- session = @pool.get(sid) || {}
- rescue MemCache::MemCacheError, Errno::ECONNREFUSED
- session = {}
- end
- [sid, session]
- 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
-rescue LoadError
- # MemCache wasn't available so neither can the store be
-end