aboutsummaryrefslogtreecommitdiffstats
path: root/actionpack/lib/action_controller/session
diff options
context:
space:
mode:
authorJoshua Peek <josh@joshpeek.com>2008-12-15 16:33:31 -0600
committerJoshua Peek <josh@joshpeek.com>2008-12-15 16:33:31 -0600
commited708307137c811d14e5fd2cb4ea550add381a82 (patch)
tree31cb7df0a489bb4bbb0a9bc9edb24a70a869a0d1 /actionpack/lib/action_controller/session
parente8c1915416579a3840573ca2c80822d96cb31823 (diff)
downloadrails-ed708307137c811d14e5fd2cb4ea550add381a82.tar.gz
rails-ed708307137c811d14e5fd2cb4ea550add381a82.tar.bz2
rails-ed708307137c811d14e5fd2cb4ea550add381a82.zip
Switch to Rack based session stores.
Diffstat (limited to 'actionpack/lib/action_controller/session')
-rw-r--r--actionpack/lib/action_controller/session/abstract_store.rb129
-rw-r--r--actionpack/lib/action_controller/session/active_record_store.rb350
-rw-r--r--actionpack/lib/action_controller/session/cookie_store.rb356
-rwxr-xr-xactionpack/lib/action_controller/session/drb_server.rb32
-rw-r--r--actionpack/lib/action_controller/session/drb_store.rb35
-rw-r--r--actionpack/lib/action_controller/session/mem_cache_store.rb119
6 files changed, 371 insertions, 650 deletions
diff --git a/actionpack/lib/action_controller/session/abstract_store.rb b/actionpack/lib/action_controller/session/abstract_store.rb
new file mode 100644
index 0000000000..b23bf062c4
--- /dev/null
+++ b/actionpack/lib/action_controller/session/abstract_store.rb
@@ -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
diff --git a/actionpack/lib/action_controller/session/active_record_store.rb b/actionpack/lib/action_controller/session/active_record_store.rb
deleted file mode 100644
index fadf2a6b32..0000000000
--- a/actionpack/lib/action_controller/session/active_record_store.rb
+++ /dev/null
@@ -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
diff --git a/actionpack/lib/action_controller/session/cookie_store.rb b/actionpack/lib/action_controller/session/cookie_store.rb
index ea0ea4f841..f13c9290c0 100644
--- a/actionpack/lib/action_controller/session/cookie_store.rb
+++ b/actionpack/lib/action_controller/session/cookie_store.rb
@@ -1,163 +1,219 @@
-require 'cgi'
-require 'cgi/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.
-# 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.
- class CookieOverflow < StandardError; end
-
- # Raised when the cookie fails its integrity check.
- class TamperedWithCookie < StandardError; end
-
- # Called from CGI::Session only.
- def initialize(session, options = {})
- # 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
+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 = {
+ :domain => nil,
+ :path => "/",
+ :expire_after => nil
+ }.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 = {})
+ options = options.dup
+
+ @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
- # The secret option is required.
- ensure_secret_secure(options['secret'])
-
- # Keep the session and its secret on hand so we can read and write cookies.
- @session, @secret = session, options['secret']
-
- # Message digest defaults to SHA1.
- @digest = options['digest'] || 'SHA1'
-
- # Default cookie options derived from session settings.
- @cookie_options = {
- '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
- # set our own data cookie.
- options['no_hidden'] = true
- options['no_cookies'] = true
- end
+ class SessionHash < Hash
+ def initialize(middleware, env)
+ @middleware = middleware
+ @env = env
+ @loaded = false
+ 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!
+ replace(@middleware.send(:load_session, @env))
+ @loaded = true
+ 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)
+ def call(env)
+ session_data = SessionHash.new(self, env)
+ original_value = session_data.dup
- 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
+ env[ENV_SESSION_KEY] = session_data
+ env[ENV_SESSION_OPTIONS_KEY] = @default_options.dup
- 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
+ status, headers, body = @app.call(env)
- # Restore session data from the cookie.
- def restore
- @original = read_cookie
- @data = unmarshal(@original) || {}
- end
+ unless env[ENV_SESSION_KEY] == original_value
+ session_data = marshal(env[ENV_SESSION_KEY].to_hash)
- # Wait until close to write the session data cookie.
- def update; end
+ raise CookieOverflow if session_data.size > MAX
- # 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
+ 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
- # 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
+ 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
- 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)
+ [status, headers, body]
end
- rescue ActiveSupport::MessageVerifier::InvalidSignature
- delete
- raise TamperedWithCookie
- end
-
- # Read the session data cookie.
- def read_cookie
- @session.cgi.cookies[@cookie_options['name']].first
- end
- # CGI likes to make you hack.
- def write_cookie(options)
- cookie = CGI::Cookie.new(@cookie_options.merge(options))
- @session.cgi.send :instance_variable_set, '@output_cookies', [cookie]
- end
-
- # Clear cookie value so subsequent new_session doesn't reload old data.
- def clear_old_cookie_value
- @session.cgi.cookies[@cookie_options['name']].clear
- end
-
- def verifier
- if @secret.respond_to?(:call)
- key = @secret.call
- else
- key = @secret
- end
- ActiveSupport::MessageVerifier.new(key, @digest)
+ 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
diff --git a/actionpack/lib/action_controller/session/drb_server.rb b/actionpack/lib/action_controller/session/drb_server.rb
deleted file mode 100755
index 2caa27f62a..0000000000
--- a/actionpack/lib/action_controller/session/drb_server.rb
+++ /dev/null
@@ -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
diff --git a/actionpack/lib/action_controller/session/drb_store.rb b/actionpack/lib/action_controller/session/drb_store.rb
deleted file mode 100644
index 4feb2636e7..0000000000
--- a/actionpack/lib/action_controller/session/drb_store.rb
+++ /dev/null
@@ -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
diff --git a/actionpack/lib/action_controller/session/mem_cache_store.rb b/actionpack/lib/action_controller/session/mem_cache_store.rb
index 2f08af663d..f745715a97 100644
--- a/actionpack/lib/action_controller/session/mem_cache_store.rb
+++ b/actionpack/lib/action_controller/session/mem_cache_store.rb
@@ -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
- require 'cgi/session'
require_library_or_gem 'memcache'
- class CGI
- class Session
- # MemCache-based session storage class.
- #
- # This builds upon the top-level MemCache class provided by the
- # library file memcache.rb. Session data is marshalled and stored
- # in a memcached cache.
- class MemCacheStore
- def check_id(id) #:nodoc:#
- /[^0-9a-zA-Z]+/ =~ id.to_s ? false : true
- end
+ module ActionController
+ module Session
+ class MemCacheStore < AbstractStore
+ def initialize(app, options = {})
+ # Support old :expires option
+ options[:expire_after] ||= options[:expires]
- # Create a new CGI::Session::MemCache instance
- #
- # This constructor is used internally by CGI::Session. The
- # user does not generally need to call it directly.
- #
- # +session+ is the session for which this instance is being
- # created. The session id must only contain alphanumeric
- # characters; automatically generated session ids observe
- # this requirement.
- #
- # +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
- @cache = options['cache'] || MemCache.new('localhost')
- @expires = options['expires'] || 0
- @session_key = "session:#{id}"
- @session_data = {}
- # Add this key to the store if haven't done so yet
- unless @cache.get(@session_key)
- @cache.add(@session_key, @session_data, @expires)
- end
- end
+ super
- # Restore session state from the session's memcache entry.
- #
- # Returns the session state as a hash.
- def restore
- @session_data = @cache[@session_key] || {}
- end
+ @default_options = {
+ :namespace => 'rack:session',
+ :memcache_server => 'localhost:11211'
+ }.merge(@default_options)
- # 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
+ @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
- # Delete the session's memcache entry.
- def delete
- @cache.delete(@session_key)
- @session_data = {}
- end
-
- def data
- @session_data
+ 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