module Innate # Mostly ported from Ramaze, but behaves lazy, no session will be created if # no session is used. # # We keep session data in memory until #flush is called, at which point it # will be persisted completely into the cache, no question asked. # # You may store anything in here that you may also store in the corresponding # store, usually it's best to keep it to things that are safe to Marshal. # # The Session instance is compatible with the specification of rack.session. # # Since the Time class is used to create the cookie expiration timestamp, you # will have to keep the ttl in a reasonable range. # The maximum value that Time can store on a 32bit system is: # Time.at(2147483647) # => Tue Jan 19 12:14:07 +0900 2038 # # The default expiration time for cookies and the session cache was reduced # to a default of 30 days. # This was done to be compatible with the maximum ttl of MemCache. You may # increase this value if you do not use MemCache to persist your sessions. class Session include Optioned options.dsl do o "Key for the session cookie", :key, 'innate.sid' o "Domain the cookie relates to, unspecified if false", :domain, false o "Path the cookie relates to", :path, '/' o "Use secure cookie", :secure, false o "Time of cookie expiration", :expires, nil o "Time to live for session cookies and cache, nil/false will prevent setting", :ttl, (60 * 60 * 24 * 30) # 30 days o "Length of generated Session ID (only applies when using SecureRandom)", :sid_length, 64 o "cookie cannot be accessed through client side script (http://www.owasp.org/index.php/HttpOnly)", :httponly, false end attr_reader :cookie_set, :request, :response, :flash def initialize(request, response) @request, @response = request, response @cookie_set = false @force_new_cookie = false @cache_sid = nil @flash = Flash.new(self) end # Rack interface def store(key, value) cache_sid[key] = value end alias []= store def fetch(key, value = nil) cache_sid[key] end alias [] fetch def delete(key) cache_sid.delete(key) end def clear cache.delete(sid) @cache_sid = nil end # Additional interface def flush(response = @response) return if !@cache_sid or @cache_sid.empty? flash.rotate! cache.store(sid, cache_sid, :ttl => options.ttl) set_cookie(response) end def sid @sid ||= cookie || generate_sid end def resid! cache_sid cache.delete(sid) @sid = generate_sid @force_new_cookie = true end private def cache_sid @cache_sid ||= cache[sid] || {} end def cookie @request.cookies[options.key] end def cache Innate::Cache.session end def set_cookie(response) return if @cookie_set || (!@force_new_cookie && cookie) @cookie_set = true response.set_cookie(options.key, cookie_value) @force_new_cookie = false end def cookie_value o = options cookie = { :domain => o.domain, :path => o.path, :secure => o.secure, :httponly => o.httponly } cookie[:expires] = (Time.now + o.ttl) if o.ttl cookie.merge!(:value => sid) end def generate_sid begin sid = sid_algorithm end while cache[sid] sid end begin require 'securerandom' # Using SecureRandom, optional length. # SecureRandom is available since Ruby 1.8.7. # For Ruby versions earlier than that, you can require the uuidtools gem, # which has a drop-in replacement for SecureRandom. def sid_algorithm; SecureRandom.hex(options.sid_length); end rescue LoadError require 'openssl' # Using OpenSSL::Random for generation, this is comparable in performance # with stdlib SecureRandom and also allows for optional length, it should # have the same behaviour as the SecureRandom::hex method of the # uuidtools gem. def sid_algorithm OpenSSL::Random.random_bytes(options.sid_length / 2).unpack('H*')[0] end rescue LoadError warn "Falling back to low-entropy Session ID generation" warn "Avoid this by upgrading Ruby, installing OpenSSL, or UUIDTools" # Digest::SHA2::hexdigest produces a string of length 64, although # collisions are not very likely, the entropy is still very low and # length is not optional. # # Replacing it with OS-provided random data would take a lot of code and # won't be as cross-platform as Ruby. def sid_algorithm entropy = [ srand, rand, Time.now.to_f, rand, $$, rand, object_id ] Digest::SHA2.hexdigest(entropy.join) end end end end