actionpack 3.0.0.beta3 → 3.0.0.beta4
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of actionpack might be problematic. Click here for more details.
- data/CHANGELOG +19 -0
- data/lib/abstract_controller.rb +1 -1
- data/lib/abstract_controller/asset_paths.rb +9 -0
- data/lib/abstract_controller/base.rb +5 -13
- data/lib/abstract_controller/callbacks.rb +1 -1
- data/lib/abstract_controller/helpers.rb +0 -1
- data/lib/abstract_controller/layouts.rb +3 -3
- data/lib/abstract_controller/logger.rb +1 -1
- data/lib/abstract_controller/rendering.rb +1 -0
- data/lib/action_controller/base.rb +5 -1
- data/lib/action_controller/caching.rb +2 -3
- data/lib/action_controller/caching/actions.rb +1 -1
- data/lib/action_controller/caching/fragments.rb +1 -1
- data/lib/action_controller/caching/pages.rb +8 -8
- data/lib/action_controller/caching/sweeping.rb +1 -0
- data/lib/action_controller/deprecated/base.rb +10 -36
- data/lib/action_controller/metal.rb +45 -3
- data/lib/action_controller/metal/compatibility.rb +2 -2
- data/lib/action_controller/metal/helpers.rb +3 -3
- data/lib/action_controller/metal/http_authentication.rb +158 -0
- data/lib/action_controller/metal/instrumentation.rb +5 -5
- data/lib/action_controller/metal/rack_delegation.rb +4 -4
- data/lib/action_controller/metal/renderers.rb +3 -3
- data/lib/action_controller/metal/request_forgery_protection.rb +45 -74
- data/lib/action_controller/metal/responder.rb +1 -1
- data/lib/action_controller/metal/url_for.rb +8 -0
- data/lib/action_controller/railtie.rb +26 -39
- data/lib/action_controller/test_case.rb +147 -135
- data/lib/action_controller/vendor/html-scanner/html/tokenizer.rb +1 -0
- data/lib/action_dispatch.rb +0 -1
- data/lib/action_dispatch/http/parameters.rb +2 -1
- data/lib/action_dispatch/http/request.rb +19 -7
- data/lib/action_dispatch/http/response.rb +3 -33
- data/lib/action_dispatch/middleware/cookies.rb +44 -10
- data/lib/action_dispatch/middleware/flash.rb +11 -1
- data/lib/action_dispatch/middleware/params_parser.rb +3 -1
- data/lib/action_dispatch/middleware/session/abstract_store.rb +47 -83
- data/lib/action_dispatch/middleware/session/cookie_store.rb +19 -165
- data/lib/action_dispatch/middleware/session/mem_cache_store.rb +2 -2
- data/lib/action_dispatch/middleware/show_exceptions.rb +18 -12
- data/lib/action_dispatch/middleware/stack.rb +17 -67
- data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.erb +1 -1
- data/lib/action_dispatch/railtie.rb +0 -2
- data/lib/action_dispatch/routing/deprecated_mapper.rb +1 -0
- data/lib/action_dispatch/routing/mapper.rb +89 -23
- data/lib/action_dispatch/routing/route_set.rb +22 -16
- data/lib/action_dispatch/routing/url_for.rb +1 -1
- data/lib/action_dispatch/testing/assertions/routing.rb +1 -0
- data/lib/action_dispatch/testing/assertions/selector.rb +11 -7
- data/lib/action_dispatch/testing/test_process.rb +3 -2
- data/lib/action_pack/version.rb +1 -1
- data/lib/action_view.rb +5 -1
- data/lib/action_view/base.rb +10 -4
- data/lib/action_view/helpers/active_model_helper.rb +1 -8
- data/lib/action_view/helpers/asset_tag_helper.rb +7 -4
- data/lib/action_view/helpers/cache_helper.rb +14 -14
- data/lib/action_view/helpers/capture_helper.rb +25 -6
- data/lib/action_view/helpers/date_helper.rb +33 -44
- data/lib/action_view/helpers/form_helper.rb +47 -27
- data/lib/action_view/helpers/form_options_helper.rb +26 -3
- data/lib/action_view/helpers/form_tag_helper.rb +8 -4
- data/lib/action_view/helpers/number_helper.rb +5 -2
- data/lib/action_view/helpers/prototype_helper.rb +1 -1
- data/lib/action_view/helpers/tag_helper.rb +1 -1
- data/lib/action_view/helpers/text_helper.rb +55 -46
- data/lib/action_view/helpers/translation_helper.rb +19 -8
- data/lib/action_view/helpers/url_helper.rb +2 -4
- data/lib/action_view/locale/en.yml +14 -14
- data/lib/action_view/lookup_context.rb +52 -22
- data/lib/action_view/paths.rb +1 -0
- data/lib/action_view/render/layouts.rb +3 -12
- data/lib/action_view/render/partials.rb +21 -10
- data/lib/action_view/render/rendering.rb +1 -1
- data/lib/action_view/template.rb +172 -26
- data/lib/action_view/template/error.rb +25 -27
- data/lib/action_view/template/handlers.rb +1 -1
- data/lib/action_view/template/handlers/erb.rb +92 -45
- data/lib/action_view/template/resolver.rb +4 -1
- data/lib/action_view/test_case.rb +105 -72
- data/lib/action_view/testing/resolvers.rb +43 -0
- metadata +62 -20
- data/lib/abstract_controller/assigns.rb +0 -21
- data/lib/action_dispatch/middleware/cascade.rb +0 -29
@@ -1,6 +1,7 @@
|
|
1
1
|
require 'digest/md5'
|
2
2
|
require 'active_support/core_ext/module/delegation'
|
3
3
|
require 'active_support/core_ext/object/blank'
|
4
|
+
require 'active_support/core_ext/class/attribute_accessors'
|
4
5
|
|
5
6
|
module ActionDispatch # :nodoc:
|
6
7
|
# Represents an HTTP response generated by a controller action. One can use
|
@@ -139,7 +140,7 @@ module ActionDispatch # :nodoc:
|
|
139
140
|
def to_a
|
140
141
|
assign_default_content_type_and_charset!
|
141
142
|
handle_conditional_get!
|
142
|
-
self["Set-Cookie"] =
|
143
|
+
self["Set-Cookie"] = self["Set-Cookie"].join("\n") if self["Set-Cookie"].respond_to?(:join)
|
143
144
|
self["ETag"] = @_etag if @_etag
|
144
145
|
super
|
145
146
|
end
|
@@ -169,7 +170,7 @@ module ActionDispatch # :nodoc:
|
|
169
170
|
# assert_equal 'AuthorOfNewPage', r.cookies['author']
|
170
171
|
def cookies
|
171
172
|
cookies = {}
|
172
|
-
if header =
|
173
|
+
if header = self["Set-Cookie"]
|
173
174
|
header = header.split("\n") if header.respond_to?(:to_str)
|
174
175
|
header.each do |cookie|
|
175
176
|
if pair = cookie.split(';').first
|
@@ -181,37 +182,6 @@ module ActionDispatch # :nodoc:
|
|
181
182
|
cookies
|
182
183
|
end
|
183
184
|
|
184
|
-
def set_cookie(key, value)
|
185
|
-
case value
|
186
|
-
when Hash
|
187
|
-
domain = "; domain=" + value[:domain] if value[:domain]
|
188
|
-
path = "; path=" + value[:path] if value[:path]
|
189
|
-
# According to RFC 2109, we need dashes here.
|
190
|
-
# N.B.: cgi.rb uses spaces...
|
191
|
-
expires = "; expires=" + value[:expires].clone.gmtime.
|
192
|
-
strftime("%a, %d-%b-%Y %H:%M:%S GMT") if value[:expires]
|
193
|
-
secure = "; secure" if value[:secure]
|
194
|
-
httponly = "; HttpOnly" if value[:httponly]
|
195
|
-
value = value[:value]
|
196
|
-
end
|
197
|
-
value = [value] unless Array === value
|
198
|
-
cookie = Rack::Utils.escape(key) + "=" +
|
199
|
-
value.map { |v| Rack::Utils.escape v }.join("&") +
|
200
|
-
"#{domain}#{path}#{expires}#{secure}#{httponly}"
|
201
|
-
|
202
|
-
@cookie << cookie
|
203
|
-
end
|
204
|
-
|
205
|
-
def delete_cookie(key, value={})
|
206
|
-
@cookie.reject! { |cookie|
|
207
|
-
cookie =~ /\A#{Rack::Utils.escape(key)}=/
|
208
|
-
}
|
209
|
-
|
210
|
-
set_cookie(key,
|
211
|
-
{:value => '', :path => nil, :domain => nil,
|
212
|
-
:expires => Time.at(0) }.merge(value))
|
213
|
-
end
|
214
|
-
|
215
185
|
private
|
216
186
|
def assign_default_content_type_and_charset!
|
217
187
|
return if headers[CONTENT_TYPE].present?
|
@@ -52,9 +52,15 @@ module ActionDispatch
|
|
52
52
|
# * <tt>:httponly</tt> - Whether this cookie is accessible via scripting or
|
53
53
|
# only HTTP. Defaults to +false+.
|
54
54
|
class Cookies
|
55
|
+
HTTP_HEADER = "Set-Cookie".freeze
|
56
|
+
TOKEN_KEY = "action_dispatch.secret_token".freeze
|
57
|
+
|
58
|
+
# Raised when storing more than 4K of session data.
|
59
|
+
class CookieOverflow < StandardError; end
|
60
|
+
|
55
61
|
class CookieJar < Hash #:nodoc:
|
56
62
|
def self.build(request)
|
57
|
-
secret = request.env[
|
63
|
+
secret = request.env[TOKEN_KEY]
|
58
64
|
new(secret).tap do |hash|
|
59
65
|
hash.update(request.cookies)
|
60
66
|
end
|
@@ -134,9 +140,9 @@ module ActionDispatch
|
|
134
140
|
@signed ||= SignedCookieJar.new(self, @secret)
|
135
141
|
end
|
136
142
|
|
137
|
-
def write(
|
138
|
-
@set_cookies.each { |k, v|
|
139
|
-
@delete_cookies.each { |k, v|
|
143
|
+
def write(headers)
|
144
|
+
@set_cookies.each { |k, v| ::Rack::Utils.set_cookie_header!(headers, k, v) }
|
145
|
+
@delete_cookies.each { |k, v| ::Rack::Utils.delete_cookie_header!(headers, k, v) }
|
140
146
|
end
|
141
147
|
end
|
142
148
|
|
@@ -166,8 +172,11 @@ module ActionDispatch
|
|
166
172
|
end
|
167
173
|
|
168
174
|
class SignedCookieJar < CookieJar #:nodoc:
|
175
|
+
MAX_COOKIE_SIZE = 4096 # Cookies can typically store 4096 bytes.
|
176
|
+
SECRET_MIN_LENGTH = 30 # Characters
|
177
|
+
|
169
178
|
def initialize(parent_jar, secret)
|
170
|
-
|
179
|
+
ensure_secret_secure(secret)
|
171
180
|
@parent_jar = parent_jar
|
172
181
|
@verifier = ActiveSupport::MessageVerifier.new(secret)
|
173
182
|
end
|
@@ -176,6 +185,8 @@ module ActionDispatch
|
|
176
185
|
if signed_message = @parent_jar[name]
|
177
186
|
@verifier.verify(signed_message)
|
178
187
|
end
|
188
|
+
rescue ActiveSupport::MessageVerifier::InvalidSignature
|
189
|
+
nil
|
179
190
|
end
|
180
191
|
|
181
192
|
def []=(key, options)
|
@@ -186,12 +197,34 @@ module ActionDispatch
|
|
186
197
|
options = { :value => @verifier.generate(options) }
|
187
198
|
end
|
188
199
|
|
200
|
+
raise CookieOverflow if options[:value].size > MAX_COOKIE_SIZE
|
189
201
|
@parent_jar[key] = options
|
190
202
|
end
|
191
203
|
|
192
204
|
def method_missing(method, *arguments, &block)
|
193
205
|
@parent_jar.send(method, *arguments, &block)
|
194
206
|
end
|
207
|
+
|
208
|
+
protected
|
209
|
+
|
210
|
+
# To prevent users from using something insecure like "Password" we make sure that the
|
211
|
+
# secret they've provided is at least 30 characters in length.
|
212
|
+
def ensure_secret_secure(secret)
|
213
|
+
if secret.blank?
|
214
|
+
raise ArgumentError, "A secret is required to generate an " +
|
215
|
+
"integrity hash for cookie session data. Use " +
|
216
|
+
"config.secret_token = \"some secret phrase of at " +
|
217
|
+
"least #{SECRET_MIN_LENGTH} characters\"" +
|
218
|
+
"in config/application.rb"
|
219
|
+
end
|
220
|
+
|
221
|
+
if secret.length < SECRET_MIN_LENGTH
|
222
|
+
raise ArgumentError, "Secret should be something secure, " +
|
223
|
+
"like \"#{ActiveSupport::SecureRandom.hex(16)}\". The value you " +
|
224
|
+
"provided, \"#{secret}\", is shorter than the minimum length " +
|
225
|
+
"of #{SECRET_MIN_LENGTH} characters"
|
226
|
+
end
|
227
|
+
end
|
195
228
|
end
|
196
229
|
|
197
230
|
def initialize(app)
|
@@ -202,12 +235,13 @@ module ActionDispatch
|
|
202
235
|
status, headers, body = @app.call(env)
|
203
236
|
|
204
237
|
if cookie_jar = env['action_dispatch.cookies']
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
[status, headers, body]
|
238
|
+
cookie_jar.write(headers)
|
239
|
+
if headers[HTTP_HEADER].respond_to?(:join)
|
240
|
+
headers[HTTP_HEADER] = headers[HTTP_HEADER].join("\n")
|
241
|
+
end
|
210
242
|
end
|
243
|
+
|
244
|
+
[status, headers, body]
|
211
245
|
end
|
212
246
|
end
|
213
247
|
end
|
@@ -49,6 +49,16 @@ module ActionDispatch
|
|
49
49
|
def [](k)
|
50
50
|
@flash[k]
|
51
51
|
end
|
52
|
+
|
53
|
+
# Convenience accessor for flash.now[:alert]=
|
54
|
+
def alert=(message)
|
55
|
+
self[:alert] = message
|
56
|
+
end
|
57
|
+
|
58
|
+
# Convenience accessor for flash.now[:notice]=
|
59
|
+
def notice=(message)
|
60
|
+
self[:notice] = message
|
61
|
+
end
|
52
62
|
end
|
53
63
|
|
54
64
|
class FlashHash < Hash
|
@@ -166,7 +176,7 @@ module ActionDispatch
|
|
166
176
|
|
167
177
|
@app.call(env)
|
168
178
|
ensure
|
169
|
-
if (session = env['rack.session']) && (flash
|
179
|
+
if (session = env['rack.session']) && session.key?('flash') && session['flash'].empty?
|
170
180
|
session.delete('flash')
|
171
181
|
end
|
172
182
|
end
|
@@ -1,4 +1,6 @@
|
|
1
|
+
require 'active_support/core_ext/hash/conversions'
|
1
2
|
require 'action_dispatch/http/request'
|
3
|
+
require 'active_support/core_ext/hash/indifferent_access'
|
2
4
|
|
3
5
|
module ActionDispatch
|
4
6
|
class ParamsParser
|
@@ -36,7 +38,7 @@ module ActionDispatch
|
|
36
38
|
when Proc
|
37
39
|
strategy.call(request.raw_post)
|
38
40
|
when :xml_simple, :xml_node
|
39
|
-
data = Hash.from_xml(request.body) || {}
|
41
|
+
data = Hash.from_xml(request.body.read) || {}
|
40
42
|
request.body.rewind if request.body.respond_to?(:rewind)
|
41
43
|
data.with_indifferent_access
|
42
44
|
when :yaml
|
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'rack/utils'
|
2
2
|
require 'rack/request'
|
3
|
+
require 'action_dispatch/middleware/cookies'
|
3
4
|
require 'active_support/core_ext/object/blank'
|
4
5
|
|
5
6
|
module ActionDispatch
|
@@ -11,9 +12,6 @@ module ActionDispatch
|
|
11
12
|
ENV_SESSION_KEY = 'rack.session'.freeze
|
12
13
|
ENV_SESSION_OPTIONS_KEY = 'rack.session.options'.freeze
|
13
14
|
|
14
|
-
HTTP_COOKIE = 'HTTP_COOKIE'.freeze
|
15
|
-
SET_COOKIE = 'Set-Cookie'.freeze
|
16
|
-
|
17
15
|
class SessionHash < Hash
|
18
16
|
def initialize(by, env)
|
19
17
|
super()
|
@@ -22,13 +20,6 @@ module ActionDispatch
|
|
22
20
|
@loaded = false
|
23
21
|
end
|
24
22
|
|
25
|
-
def session_id
|
26
|
-
ActiveSupport::Deprecation.warn(
|
27
|
-
"ActionDispatch::Session::AbstractStore::SessionHash#session_id " +
|
28
|
-
"has been deprecated. Please use request.session_options[:id] instead.", caller)
|
29
|
-
@env[ENV_SESSION_OPTIONS_KEY][:id]
|
30
|
-
end
|
31
|
-
|
32
23
|
def [](key)
|
33
24
|
load! unless @loaded
|
34
25
|
super(key.to_s)
|
@@ -45,35 +36,14 @@ module ActionDispatch
|
|
45
36
|
h
|
46
37
|
end
|
47
38
|
|
48
|
-
def update(hash
|
49
|
-
|
50
|
-
|
51
|
-
replace({})
|
52
|
-
else
|
53
|
-
load! unless @loaded
|
54
|
-
super(hash.stringify_keys)
|
55
|
-
end
|
56
|
-
end
|
57
|
-
|
58
|
-
def delete(key = nil)
|
59
|
-
if key.nil?
|
60
|
-
ActiveSupport::Deprecation.warn('use clear instead', caller)
|
61
|
-
clear
|
62
|
-
else
|
63
|
-
load! unless @loaded
|
64
|
-
super(key.to_s)
|
65
|
-
end
|
66
|
-
end
|
67
|
-
|
68
|
-
def data
|
69
|
-
ActiveSupport::Deprecation.warn(
|
70
|
-
"ActionDispatch::Session::AbstractStore::SessionHash#data " +
|
71
|
-
"has been deprecated. Please use #to_hash instead.", caller)
|
72
|
-
to_hash
|
39
|
+
def update(hash)
|
40
|
+
load! unless @loaded
|
41
|
+
super(hash.stringify_keys)
|
73
42
|
end
|
74
43
|
|
75
|
-
def
|
76
|
-
|
44
|
+
def delete(key)
|
45
|
+
load! unless @loaded
|
46
|
+
super(key.to_s)
|
77
47
|
end
|
78
48
|
|
79
49
|
def inspect
|
@@ -81,11 +51,11 @@ module ActionDispatch
|
|
81
51
|
super
|
82
52
|
end
|
83
53
|
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
end
|
54
|
+
def loaded?
|
55
|
+
@loaded
|
56
|
+
end
|
88
57
|
|
58
|
+
private
|
89
59
|
def load!
|
90
60
|
stale_session_check! do
|
91
61
|
id, session = @by.send(:load_session, @env)
|
@@ -124,30 +94,15 @@ module ActionDispatch
|
|
124
94
|
}
|
125
95
|
|
126
96
|
def initialize(app, options = {})
|
127
|
-
# Process legacy CGI options
|
128
|
-
options = options.symbolize_keys
|
129
|
-
if options.has_key?(:session_path)
|
130
|
-
options[:path] = options.delete(:session_path)
|
131
|
-
end
|
132
|
-
if options.has_key?(:session_key)
|
133
|
-
options[:key] = options.delete(:session_key)
|
134
|
-
end
|
135
|
-
if options.has_key?(:session_http_only)
|
136
|
-
options[:httponly] = options.delete(:session_http_only)
|
137
|
-
end
|
138
|
-
|
139
97
|
@app = app
|
140
98
|
@default_options = DEFAULT_OPTIONS.merge(options)
|
141
|
-
@key = @default_options
|
142
|
-
@cookie_only = @default_options
|
99
|
+
@key = @default_options.delete(:key).freeze
|
100
|
+
@cookie_only = @default_options.delete(:cookie_only)
|
101
|
+
ensure_session_key!
|
143
102
|
end
|
144
103
|
|
145
104
|
def call(env)
|
146
|
-
|
147
|
-
|
148
|
-
env[ENV_SESSION_KEY] = session
|
149
|
-
env[ENV_SESSION_OPTIONS_KEY] = @default_options.dup
|
150
|
-
|
105
|
+
prepare!(env)
|
151
106
|
response = @app.call(env)
|
152
107
|
|
153
108
|
session_data = env[ENV_SESSION_KEY]
|
@@ -157,53 +112,62 @@ module ActionDispatch
|
|
157
112
|
session_data.send(:load!) if session_data.is_a?(AbstractStore::SessionHash) && !session_data.send(:loaded?)
|
158
113
|
|
159
114
|
sid = options[:id] || generate_sid
|
115
|
+
session_data = session_data.to_hash
|
160
116
|
|
161
|
-
|
162
|
-
|
163
|
-
end
|
117
|
+
value = set_session(env, sid, session_data)
|
118
|
+
return response unless value
|
164
119
|
|
165
|
-
cookie =
|
166
|
-
|
167
|
-
|
168
|
-
if options[:expire_after]
|
169
|
-
expiry = Time.now + options[:expire_after]
|
170
|
-
cookie << "; expires=#{expiry.httpdate}"
|
171
|
-
end
|
172
|
-
cookie << "; Secure" if options[:secure]
|
173
|
-
cookie << "; HttpOnly" if options[:httponly]
|
174
|
-
|
175
|
-
headers = response[1]
|
176
|
-
unless headers[SET_COOKIE].blank?
|
177
|
-
headers[SET_COOKIE] << "\n#{cookie}"
|
178
|
-
else
|
179
|
-
headers[SET_COOKIE] = cookie
|
120
|
+
cookie = { :value => value }
|
121
|
+
unless options[:expire_after].nil?
|
122
|
+
cookie[:expires] = Time.now + options.delete(:expire_after)
|
180
123
|
end
|
124
|
+
|
125
|
+
request = ActionDispatch::Request.new(env)
|
126
|
+
set_cookie(request, cookie.merge!(options))
|
181
127
|
end
|
182
128
|
|
183
129
|
response
|
184
130
|
end
|
185
131
|
|
186
132
|
private
|
133
|
+
|
134
|
+
def prepare!(env)
|
135
|
+
env[ENV_SESSION_KEY] = SessionHash.new(self, env)
|
136
|
+
env[ENV_SESSION_OPTIONS_KEY] = @default_options.dup
|
137
|
+
end
|
138
|
+
|
187
139
|
def generate_sid
|
188
140
|
ActiveSupport::SecureRandom.hex(16)
|
189
141
|
end
|
190
142
|
|
143
|
+
def set_cookie(request, options)
|
144
|
+
request.cookie_jar[@key] = options
|
145
|
+
end
|
146
|
+
|
191
147
|
def load_session(env)
|
192
148
|
request = Rack::Request.new(env)
|
193
|
-
sid
|
194
|
-
unless @cookie_only
|
195
|
-
sid ||= request.params[@key]
|
196
|
-
end
|
149
|
+
sid = request.cookies[@key]
|
150
|
+
sid ||= request.params[@key] unless @cookie_only
|
197
151
|
sid, session = get_session(env, sid)
|
198
152
|
[sid, session]
|
199
153
|
end
|
200
154
|
|
155
|
+
def ensure_session_key!
|
156
|
+
if @key.blank?
|
157
|
+
raise ArgumentError, 'A key is required to write a ' +
|
158
|
+
'cookie containing the session data. Use ' +
|
159
|
+
'config.session_store SESSION_STORE, { :key => ' +
|
160
|
+
'"_myapp_session" } in config/application.rb'
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
201
164
|
def get_session(env, sid)
|
202
165
|
raise '#get_session needs to be implemented.'
|
203
166
|
end
|
204
167
|
|
205
168
|
def set_session(env, sid, session_data)
|
206
|
-
raise '#set_session needs to be implemented
|
169
|
+
raise '#set_session needs to be implemented and should return ' <<
|
170
|
+
'the value to be stored in the cookie (usually the sid)'
|
207
171
|
end
|
208
172
|
end
|
209
173
|
end
|
@@ -38,23 +38,11 @@ module ActionDispatch
|
|
38
38
|
# "rake secret" and set the key in config/environment.rb.
|
39
39
|
#
|
40
40
|
# Note that changing digest or secret invalidates all existing sessions!
|
41
|
-
class CookieStore
|
42
|
-
# Cookies can typically store 4096 bytes.
|
43
|
-
MAX = 4096
|
44
|
-
SECRET_MIN_LENGTH = 30 # characters
|
45
|
-
|
46
|
-
DEFAULT_OPTIONS = {
|
47
|
-
:key => '_session_id',
|
48
|
-
:domain => nil,
|
49
|
-
:path => "/",
|
50
|
-
:expire_after => nil,
|
51
|
-
:httponly => true
|
52
|
-
}.freeze
|
53
|
-
|
41
|
+
class CookieStore < AbstractStore
|
54
42
|
class OptionsHash < Hash
|
55
43
|
def initialize(by, env, default_options)
|
56
|
-
@session_data = env[
|
57
|
-
default_options
|
44
|
+
@session_data = env[AbstractStore::ENV_SESSION_KEY]
|
45
|
+
merge!(default_options)
|
58
46
|
end
|
59
47
|
|
60
48
|
def [](key)
|
@@ -62,172 +50,38 @@ module ActionDispatch
|
|
62
50
|
end
|
63
51
|
end
|
64
52
|
|
65
|
-
ENV_SESSION_KEY = "rack.session".freeze
|
66
|
-
ENV_SESSION_OPTIONS_KEY = "rack.session.options".freeze
|
67
|
-
HTTP_SET_COOKIE = "Set-Cookie".freeze
|
68
|
-
|
69
|
-
# Raised when storing more than 4K of session data.
|
70
|
-
class CookieOverflow < StandardError; end
|
71
|
-
|
72
53
|
def initialize(app, options = {})
|
73
|
-
|
74
|
-
options = options.symbolize_keys
|
75
|
-
if options.has_key?(:session_path)
|
76
|
-
options[:path] = options.delete(:session_path)
|
77
|
-
end
|
78
|
-
if options.has_key?(:session_key)
|
79
|
-
options[:key] = options.delete(:session_key)
|
80
|
-
end
|
81
|
-
if options.has_key?(:session_http_only)
|
82
|
-
options[:httponly] = options.delete(:session_http_only)
|
83
|
-
end
|
84
|
-
|
85
|
-
@app = app
|
86
|
-
|
87
|
-
# The session_key option is required.
|
88
|
-
ensure_session_key(options[:key])
|
89
|
-
@key = options.delete(:key).freeze
|
90
|
-
|
91
|
-
# The secret option is required.
|
92
|
-
ensure_secret_secure(options[:secret])
|
93
|
-
@secret = options.delete(:secret).freeze
|
94
|
-
|
95
|
-
@digest = options.delete(:digest) || 'SHA1'
|
96
|
-
@verifier = verifier_for(@secret, @digest)
|
97
|
-
|
98
|
-
@default_options = DEFAULT_OPTIONS.merge(options).freeze
|
99
|
-
|
54
|
+
super(app, options.merge!(:cookie_only => true))
|
100
55
|
freeze
|
101
56
|
end
|
102
57
|
|
103
|
-
def call(env)
|
104
|
-
env[ENV_SESSION_KEY] = AbstractStore::SessionHash.new(self, env)
|
105
|
-
env[ENV_SESSION_OPTIONS_KEY] = OptionsHash.new(self, env, @default_options)
|
106
|
-
|
107
|
-
status, headers, body = @app.call(env)
|
108
|
-
|
109
|
-
session_data = env[ENV_SESSION_KEY]
|
110
|
-
options = env[ENV_SESSION_OPTIONS_KEY]
|
111
|
-
|
112
|
-
if !session_data.is_a?(AbstractStore::SessionHash) || session_data.send(:loaded?) || options[:expire_after]
|
113
|
-
session_data.send(:load!) if session_data.is_a?(AbstractStore::SessionHash) && !session_data.send(:loaded?)
|
114
|
-
session_data = marshal(session_data.to_hash)
|
115
|
-
|
116
|
-
raise CookieOverflow if session_data.size > MAX
|
117
|
-
|
118
|
-
cookie = Hash.new
|
119
|
-
cookie[:value] = session_data
|
120
|
-
unless options[:expire_after].nil?
|
121
|
-
cookie[:expires] = Time.now + options[:expire_after]
|
122
|
-
end
|
123
|
-
|
124
|
-
cookie = build_cookie(@key, cookie.merge(options))
|
125
|
-
unless headers[HTTP_SET_COOKIE].blank?
|
126
|
-
headers[HTTP_SET_COOKIE] << "\n#{cookie}"
|
127
|
-
else
|
128
|
-
headers[HTTP_SET_COOKIE] = cookie
|
129
|
-
end
|
130
|
-
end
|
131
|
-
|
132
|
-
[status, headers, body]
|
133
|
-
end
|
134
|
-
|
135
58
|
private
|
136
|
-
|
137
|
-
def
|
138
|
-
|
139
|
-
|
140
|
-
domain = "; domain=" + value[:domain] if value[:domain]
|
141
|
-
path = "; path=" + value[:path] if value[:path]
|
142
|
-
# According to RFC 2109, we need dashes here.
|
143
|
-
# N.B.: cgi.rb uses spaces...
|
144
|
-
expires = "; expires=" + value[:expires].clone.gmtime.
|
145
|
-
strftime("%a, %d-%b-%Y %H:%M:%S GMT") if value[:expires]
|
146
|
-
secure = "; secure" if value[:secure]
|
147
|
-
httponly = "; HttpOnly" if value[:httponly]
|
148
|
-
value = value[:value]
|
149
|
-
end
|
150
|
-
value = [value] unless Array === value
|
151
|
-
cookie = Rack::Utils.escape(key) + "=" +
|
152
|
-
value.map { |v| Rack::Utils.escape(v) }.join("&") +
|
153
|
-
"#{domain}#{path}#{expires}#{secure}#{httponly}"
|
59
|
+
|
60
|
+
def prepare!(env)
|
61
|
+
env[ENV_SESSION_KEY] = SessionHash.new(self, env)
|
62
|
+
env[ENV_SESSION_OPTIONS_KEY] = OptionsHash.new(self, env, @default_options)
|
154
63
|
end
|
155
64
|
|
156
65
|
def load_session(env)
|
157
|
-
request =
|
158
|
-
|
159
|
-
data =
|
66
|
+
request = ActionDispatch::Request.new(env)
|
67
|
+
data = request.cookie_jar.signed[@key]
|
68
|
+
data = persistent_session_id!(data)
|
160
69
|
data.stringify_keys!
|
161
70
|
[data["session_id"], data]
|
162
71
|
end
|
163
72
|
|
164
|
-
|
165
|
-
|
166
|
-
@verifier.generate(persistent_session_id!(session))
|
167
|
-
end
|
168
|
-
|
169
|
-
# Unmarshal cookie data to a hash and verify its integrity.
|
170
|
-
def unmarshal(cookie)
|
171
|
-
persistent_session_id!(@verifier.verify(cookie)) if cookie
|
172
|
-
rescue ActiveSupport::MessageVerifier::InvalidSignature
|
173
|
-
nil
|
174
|
-
end
|
175
|
-
|
176
|
-
def ensure_session_key(key)
|
177
|
-
if key.blank?
|
178
|
-
raise ArgumentError, 'A key is required to write a ' +
|
179
|
-
'cookie containing the session data. Use ' +
|
180
|
-
'config.action_controller.session_store :cookie_store, { :key => ' +
|
181
|
-
'"_myapp_session" } in config/application.rb'
|
182
|
-
end
|
183
|
-
end
|
184
|
-
|
185
|
-
# To prevent users from using something insecure like "Password" we make sure that the
|
186
|
-
# secret they've provided is at least 30 characters in length.
|
187
|
-
def ensure_secret_secure(secret)
|
188
|
-
# There's no way we can do this check if they've provided a proc for the
|
189
|
-
# secret.
|
190
|
-
return true if secret.is_a?(Proc)
|
191
|
-
|
192
|
-
if secret.blank?
|
193
|
-
raise ArgumentError, "A secret is required to generate an " +
|
194
|
-
"integrity hash for cookie session data. Use " +
|
195
|
-
"config.secret_token = \"some secret phrase of at " +
|
196
|
-
"least #{SECRET_MIN_LENGTH} characters\"" +
|
197
|
-
"in config/application.rb"
|
198
|
-
end
|
199
|
-
|
200
|
-
if secret.length < SECRET_MIN_LENGTH
|
201
|
-
raise ArgumentError, "Secret should be something secure, " +
|
202
|
-
"like \"#{ActiveSupport::SecureRandom.hex(16)}\". The value you " +
|
203
|
-
"provided, \"#{secret}\", is shorter than the minimum length " +
|
204
|
-
"of #{SECRET_MIN_LENGTH} characters"
|
205
|
-
end
|
206
|
-
end
|
207
|
-
|
208
|
-
def verifier_for(secret, digest)
|
209
|
-
key = secret.respond_to?(:call) ? secret.call : secret
|
210
|
-
ActiveSupport::MessageVerifier.new(key, digest)
|
211
|
-
end
|
212
|
-
|
213
|
-
def generate_sid
|
214
|
-
ActiveSupport::SecureRandom.hex(16)
|
215
|
-
end
|
216
|
-
|
217
|
-
def persistent_session_id!(data)
|
218
|
-
(data ||= {}).merge!(inject_persistent_session_id(data))
|
73
|
+
def set_cookie(request, options)
|
74
|
+
request.cookie_jar.signed[@key] = options
|
219
75
|
end
|
220
76
|
|
221
|
-
def
|
222
|
-
|
77
|
+
def set_session(env, sid, session_data)
|
78
|
+
persistent_session_id!(session_data, sid)
|
223
79
|
end
|
224
80
|
|
225
|
-
def
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
true
|
230
|
-
end
|
81
|
+
def persistent_session_id!(data, sid=nil)
|
82
|
+
data ||= {}
|
83
|
+
data["session_id"] ||= sid || generate_sid
|
84
|
+
data
|
231
85
|
end
|
232
86
|
end
|
233
87
|
end
|