Upgraded Rails and RSpec
[monkeycharger.git] / vendor / rails / actionpack / lib / action_controller / session / cookie_store.rb
blob81092882f759a4447c96046e4773a0b35c50b5cd
1 require 'cgi'
2 require 'cgi/session'
3 require 'base64'        # to convert Marshal.dump to ASCII
4 require 'openssl'       # to generate the HMAC message digest
6 # This cookie-based session store is the Rails default. Sessions typically
7 # contain at most a user_id and flash message; both fit within the 4K cookie
8 # size limit. Cookie-based sessions are dramatically faster than the
9 # alternatives.
11 # If you have more than 4K of session data or don't want your data to be
12 # visible to the user, pick another session store.
14 # CookieOverflow is raised if you attempt to store more than 4K of data.
15 # TamperedWithCookie is raised if the data integrity check fails.
17 # A message digest is included with the cookie to ensure data integrity:
18 # a user cannot alter his user_id without knowing the secret key included in
19 # the hash. New apps are generated with a pregenerated secret in
20 # config/environment.rb. Set your own for old apps you're upgrading.
22 # Session options:
23 #   :secret   An application-wide key string or block returning a string
24 #             called per generated digest. The block is called with the
25 #             CGI::Session instance as an argument. It's important that the
26 #             secret is not vulnerable to a dictionary attack. Therefore,
27 #             you should choose a secret consisting of random numbers and
28 #             letters and preferably more than 30 characters.
30 #             Example:  :secret => '449fe2e7daee471bffae2fd8dc02313d'
31 #                       :secret => Proc.new { User.current_user.secret_key }
33 #   :digest   The message digest algorithm used to verify session integrity
34 #             defaults to 'SHA1' but may be any digest provided by OpenSSL,
35 #             such as 'MD5', 'RIPEMD160', 'SHA256', etc.
37 # Note that changing digest or secret invalidates all existing sessions!
38 class CGI::Session::CookieStore
39   # Cookies can typically store 4096 bytes.
40   MAX = 4096
42   # Raised when storing more than 4K of session data.
43   class CookieOverflow < StandardError; end
45   # Raised when the cookie fails its integrity check.
46   class TamperedWithCookie < StandardError; end
48   # Called from CGI::Session only.
49   def initialize(session, options = {})
50     # The session_key option is required.
51     if options['session_key'].blank?
52       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'
53     end
55     # The secret option is required.
56     ensure_secret_secure(options['secret'])
58     # Keep the session and its secret on hand so we can read and write cookies.
59     @session, @secret = session, options['secret']
61     # Message digest defaults to SHA1.
62     @digest = options['digest'] || 'SHA1'
64     # Default cookie options derived from session settings.
65     @cookie_options = {
66       'name'    => options['session_key'],
67       'path'    => options['session_path'],
68       'domain'  => options['session_domain'],
69       'expires' => options['session_expires'],
70       'secure'  => options['session_secure']
71     }
73     # Set no_hidden and no_cookies since the session id is unused and we
74     # set our own data cookie.
75     options['no_hidden'] = true
76     options['no_cookies'] = true
77   end
79   # To prevent users from using something insecure like "Password" we make sure that the
80   # secret they've provided is at least 30 characters in length.
81   def ensure_secret_secure(secret)
82     # There's no way we can do this check if they've provided a proc for the
83     # secret.
84     return true if secret.is_a?(Proc)
86     if secret.blank?
87       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" } in config/environment.rb'
88     end
90     if secret.length < 30
91       raise ArgumentError, "Secret should be something secure, like #{CGI::Session.generate_unique_id}.  The value you provided: [#{secret}]"
92     end
93   end
95   # Restore session data from the cookie.
96   def restore
97     @original = read_cookie
98     @data = unmarshal(@original) || {}
99   end
101   # Wait until close to write the session data cookie.
102   def update; end
104   # Write the session data cookie if it was loaded and has changed.
105   def close
106     if defined?(@data) && !@data.blank?
107       updated = marshal(@data)
108       raise CookieOverflow if updated.size > MAX
109       write_cookie('value' => updated) unless updated == @original
110     end
111   end
113   # Delete the session data by setting an expired cookie with no data.
114   def delete
115     @data = nil
116     clear_old_cookie_value
117     write_cookie('value' => '', 'expires' => 1.year.ago)
118   end
120   # Generate the HMAC keyed message digest. Uses SHA1 by default.
121   def generate_digest(data)
122     key = @secret.respond_to?(:call) ? @secret.call(@session) : @secret
123     OpenSSL::HMAC.hexdigest(OpenSSL::Digest::Digest.new(@digest), key, data)
124   end
126   private
127     # Marshal a session hash into safe cookie data. Include an integrity hash.
128     def marshal(session)
129       data = Base64.encode64(Marshal.dump(session)).chop
130       CGI.escape "#{data}--#{generate_digest(data)}"
131     end
133     # Unmarshal cookie data to a hash and verify its integrity.
134     def unmarshal(cookie)
135       if cookie
136         data, digest = CGI.unescape(cookie).split('--')
137         unless digest == generate_digest(data)
138           delete
139           raise TamperedWithCookie
140         end
141         Marshal.load(Base64.decode64(data))
142       end
143     end
145     # Read the session data cookie.
146     def read_cookie
147       @session.cgi.cookies[@cookie_options['name']].first
148     end
150     # CGI likes to make you hack.
151     def write_cookie(options)
152       cookie = CGI::Cookie.new(@cookie_options.merge(options))
153       @session.cgi.send :instance_variable_set, '@output_cookies', [cookie]
154     end
156     # Clear cookie value so subsequent new_session doesn't reload old data.
157     def clear_old_cookie_value
158       @session.cgi.cookies[@cookie_options['name']].clear
159     end