vendor/rails/actionpack/lib/action_controller/session/cookie_store.rb in radiant-0.7.2 vs vendor/rails/actionpack/lib/action_controller/session/cookie_store.rb in radiant-0.8.0

- old
+ new

@@ -1,166 +1,221 @@ -require 'cgi' -require 'cgi/session' -require 'openssl' # to generate the HMAC message digest +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 -# 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 + DEFAULT_OPTIONS = { + :key => '_session_id', + :domain => nil, + :path => "/", + :expire_after => nil, + :httponly => true + }.freeze - # Raised when storing more than 4K of session data. - class CookieOverflow < StandardError; end + ENV_SESSION_KEY = "rack.session".freeze + ENV_SESSION_OPTIONS_KEY = "rack.session.options".freeze + HTTP_SET_COOKIE = "Set-Cookie".freeze - # Raised when the cookie fails its integrity check. - class TamperedWithCookie < StandardError; end + # Raised when storing more than 4K of session data. + class CookieOverflow < 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 + 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 - # The secret option is required. - ensure_secret_secure(options['secret']) + @app = app - # Keep the session and its secret on hand so we can read and write cookies. - @session, @secret = session, options['secret'] + # The session_key option is required. + ensure_session_key(options[:key]) + @key = options.delete(:key).freeze - # Message digest defaults to SHA1. - @digest = options['digest'] || 'SHA1' + # The secret option is required. + ensure_secret_secure(options[:secret]) + @secret = options.delete(:secret).freeze - # 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'] - } + @digest = options.delete(:digest) || 'SHA1' + @verifier = verifier_for(@secret, @digest) - # 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 + @default_options = DEFAULT_OPTIONS.merge(options).freeze - # 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) + freeze + end - 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 + def call(env) + env[ENV_SESSION_KEY] = AbstractStore::SessionHash.new(self, env) + 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 + session_data = env[ENV_SESSION_KEY] + options = env[ENV_SESSION_OPTIONS_KEY] - # Wait until close to write the session data cookie. - def update; end + 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) - # 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 + raise CookieOverflow if session_data.size > MAX - # 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 = Hash.new + cookie[:value] = session_data + unless options[:expire_after].nil? + cookie[:expires] = Time.now + options[:expire_after] + end - # Generate the HMAC keyed message digest. Uses SHA1 by default. - def generate_digest(data) - key = @secret.respond_to?(:call) ? @secret.call(@session) : @secret - OpenSSL::HMAC.hexdigest(OpenSSL::Digest::Digest.new(@digest), key, data) - end + cookie = build_cookie(@key, cookie.merge(options)) + unless headers[HTTP_SET_COOKIE].blank? + headers[HTTP_SET_COOKIE] << "\n#{cookie}" + else + headers[HTTP_SET_COOKIE] = cookie + end + end - private - # Marshal a session hash into safe cookie data. Include an integrity hash. - def marshal(session) - data = ActiveSupport::Base64.encode64(Marshal.dump(session)).chop - "#{data}--#{generate_digest(data)}" - end + [status, headers, body] + end - # Unmarshal cookie data to a hash and verify its integrity. - def unmarshal(cookie) - if cookie - data, digest = cookie.split('--') + 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 - # Do two checks to transparently support old double-escaped data. - unless digest == generate_digest(data) || digest == generate_digest(data = CGI.unescape(data)) - delete - raise TamperedWithCookie + 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.load(ActiveSupport::Base64.decode64(data)) - end - end + # Marshal a session hash into safe cookie data. Include an integrity hash. + def marshal(session) + @verifier.generate(persistent_session_id!(session)) + end - # Read the session data cookie. - def read_cookie - @session.cgi.cookies[@cookie_options['name']].first - 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 - # 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 + 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 - # Clear cookie value so subsequent new_session doesn't reload old data. - def clear_old_cookie_value - @session.cgi.cookies[@cookie_options['name']].clear + # 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