Updated README.rdoc again
[feedcatcher.git] / vendor / rails / actionpack / lib / action_controller / session / cookie_store.rb
1 module ActionController
2 module Session
3 # This cookie-based session store is the Rails default. Sessions typically
4 # contain at most a user_id and flash message; both fit within the 4K cookie
5 # size limit. Cookie-based sessions are dramatically faster than the
6 # alternatives.
7 #
8 # If you have more than 4K of session data or don't want your data to be
9 # visible to the user, pick another session store.
10 #
11 # CookieOverflow is raised if you attempt to store more than 4K of data.
12 #
13 # A message digest is included with the cookie to ensure data integrity:
14 # a user cannot alter his +user_id+ without knowing the secret key
15 # included in the hash. New apps are generated with a pregenerated secret
16 # in config/environment.rb. Set your own for old apps you're upgrading.
17 #
18 # Session options:
19 #
20 # * <tt>:secret</tt>: An application-wide key string or block returning a
21 # string called per generated digest. The block is called with the
22 # CGI::Session instance as an argument. It's important that the secret
23 # is not vulnerable to a dictionary attack. Therefore, you should choose
24 # a secret consisting of random numbers and letters and more than 30
25 # characters. Examples:
26 #
27 # :secret => '449fe2e7daee471bffae2fd8dc02313d'
28 # :secret => Proc.new { User.current_user.secret_key }
29 #
30 # * <tt>:digest</tt>: The message digest algorithm used to verify session
31 # integrity defaults to 'SHA1' but may be any digest provided by OpenSSL,
32 # such as 'MD5', 'RIPEMD160', 'SHA256', etc.
33 #
34 # To generate a secret key for an existing application, run
35 # "rake secret" and set the key in config/environment.rb.
36 #
37 # Note that changing digest or secret invalidates all existing sessions!
38 class CookieStore
39 # Cookies can typically store 4096 bytes.
40 MAX = 4096
41 SECRET_MIN_LENGTH = 30 # characters
42
43 DEFAULT_OPTIONS = {
44 :key => '_session_id',
45 :domain => nil,
46 :path => "/",
47 :expire_after => nil,
48 :httponly => true
49 }.freeze
50
51 ENV_SESSION_KEY = "rack.session".freeze
52 ENV_SESSION_OPTIONS_KEY = "rack.session.options".freeze
53 HTTP_SET_COOKIE = "Set-Cookie".freeze
54
55 # Raised when storing more than 4K of session data.
56 class CookieOverflow < StandardError; end
57
58 def initialize(app, options = {})
59 # Process legacy CGI options
60 options = options.symbolize_keys
61 if options.has_key?(:session_path)
62 options[:path] = options.delete(:session_path)
63 end
64 if options.has_key?(:session_key)
65 options[:key] = options.delete(:session_key)
66 end
67 if options.has_key?(:session_http_only)
68 options[:httponly] = options.delete(:session_http_only)
69 end
70
71 @app = app
72
73 # The session_key option is required.
74 ensure_session_key(options[:key])
75 @key = options.delete(:key).freeze
76
77 # The secret option is required.
78 ensure_secret_secure(options[:secret])
79 @secret = options.delete(:secret).freeze
80
81 @digest = options.delete(:digest) || 'SHA1'
82 @verifier = verifier_for(@secret, @digest)
83
84 @default_options = DEFAULT_OPTIONS.merge(options).freeze
85
86 freeze
87 end
88
89 def call(env)
90 env[ENV_SESSION_KEY] = AbstractStore::SessionHash.new(self, env)
91 env[ENV_SESSION_OPTIONS_KEY] = @default_options.dup
92
93 status, headers, body = @app.call(env)
94
95 session_data = env[ENV_SESSION_KEY]
96 options = env[ENV_SESSION_OPTIONS_KEY]
97
98 if !session_data.is_a?(AbstractStore::SessionHash) || session_data.send(:loaded?) || options[:expire_after]
99 session_data.send(:load!) if session_data.is_a?(AbstractStore::SessionHash) && !session_data.send(:loaded?)
100 session_data = marshal(session_data.to_hash)
101
102 raise CookieOverflow if session_data.size > MAX
103
104 cookie = Hash.new
105 cookie[:value] = session_data
106 unless options[:expire_after].nil?
107 cookie[:expires] = Time.now + options[:expire_after]
108 end
109
110 cookie = build_cookie(@key, cookie.merge(options))
111 unless headers[HTTP_SET_COOKIE].blank?
112 headers[HTTP_SET_COOKIE] << "\n#{cookie}"
113 else
114 headers[HTTP_SET_COOKIE] = cookie
115 end
116 end
117
118 [status, headers, body]
119 end
120
121 private
122 # Should be in Rack::Utils soon
123 def build_cookie(key, value)
124 case value
125 when Hash
126 domain = "; domain=" + value[:domain] if value[:domain]
127 path = "; path=" + value[:path] if value[:path]
128 # According to RFC 2109, we need dashes here.
129 # N.B.: cgi.rb uses spaces...
130 expires = "; expires=" + value[:expires].clone.gmtime.
131 strftime("%a, %d-%b-%Y %H:%M:%S GMT") if value[:expires]
132 secure = "; secure" if value[:secure]
133 httponly = "; HttpOnly" if value[:httponly]
134 value = value[:value]
135 end
136 value = [value] unless Array === value
137 cookie = Rack::Utils.escape(key) + "=" +
138 value.map { |v| Rack::Utils.escape(v) }.join("&") +
139 "#{domain}#{path}#{expires}#{secure}#{httponly}"
140 end
141
142 def load_session(env)
143 request = Rack::Request.new(env)
144 session_data = request.cookies[@key]
145 data = unmarshal(session_data) || persistent_session_id!({})
146 [data[:session_id], data]
147 end
148
149 # Marshal a session hash into safe cookie data. Include an integrity hash.
150 def marshal(session)
151 @verifier.generate(persistent_session_id!(session))
152 end
153
154 # Unmarshal cookie data to a hash and verify its integrity.
155 def unmarshal(cookie)
156 persistent_session_id!(@verifier.verify(cookie)) if cookie
157 rescue ActiveSupport::MessageVerifier::InvalidSignature
158 nil
159 end
160
161 def ensure_session_key(key)
162 if key.blank?
163 raise ArgumentError, 'A key is required to write a ' +
164 'cookie containing the session data. Use ' +
165 'config.action_controller.session = { :key => ' +
166 '"_myapp_session", :secret => "some secret phrase" } in ' +
167 'config/environment.rb'
168 end
169 end
170
171 # To prevent users from using something insecure like "Password" we make sure that the
172 # secret they've provided is at least 30 characters in length.
173 def ensure_secret_secure(secret)
174 # There's no way we can do this check if they've provided a proc for the
175 # secret.
176 return true if secret.is_a?(Proc)
177
178 if secret.blank?
179 raise ArgumentError, "A secret is required to generate an " +
180 "integrity hash for cookie session data. Use " +
181 "config.action_controller.session = { :key => " +
182 "\"_myapp_session\", :secret => \"some secret phrase of at " +
183 "least #{SECRET_MIN_LENGTH} characters\" } " +
184 "in config/environment.rb"
185 end
186
187 if secret.length < SECRET_MIN_LENGTH
188 raise ArgumentError, "Secret should be something secure, " +
189 "like \"#{ActiveSupport::SecureRandom.hex(16)}\". The value you " +
190 "provided, \"#{secret}\", is shorter than the minimum length " +
191 "of #{SECRET_MIN_LENGTH} characters"
192 end
193 end
194
195 def verifier_for(secret, digest)
196 key = secret.respond_to?(:call) ? secret.call : secret
197 ActiveSupport::MessageVerifier.new(key, digest)
198 end
199
200 def generate_sid
201 ActiveSupport::SecureRandom.hex(16)
202 end
203
204 def persistent_session_id!(data)
205 (data ||= {}).merge!(inject_persistent_session_id(data))
206 end
207
208 def inject_persistent_session_id(data)
209 requires_session_id?(data) ? { :session_id => generate_sid } : {}
210 end
211
212 def requires_session_id?(data)
213 if data
214 data.respond_to?(:key?) && !data.key?(:session_id)
215 else
216 true
217 end
218 end
219 end
220 end
221 end