roda 3.9.0 → 3.10.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG +18 -0
- data/README.rdoc +43 -23
- data/doc/release_notes/3.10.0.txt +132 -0
- data/lib/roda.rb +3 -3
- data/lib/roda/plugins/assets.rb +2 -2
- data/lib/roda/plugins/flash.rb +8 -2
- data/lib/roda/plugins/json.rb +1 -3
- data/lib/roda/plugins/json_parser.rb +1 -2
- data/lib/roda/plugins/middleware.rb +12 -3
- data/lib/roda/plugins/route_csrf.rb +34 -32
- data/lib/roda/plugins/sessions.rb +451 -0
- data/lib/roda/plugins/typecast_params.rb +15 -2
- data/lib/roda/session_middleware.rb +175 -0
- data/lib/roda/version.rb +1 -1
- data/spec/plugin/csrf_spec.rb +2 -2
- data/spec/plugin/flash_spec.rb +17 -23
- data/spec/plugin/heartbeat_spec.rb +1 -1
- data/spec/plugin/middleware_spec.rb +15 -0
- data/spec/plugin/route_csrf_spec.rb +3 -2
- data/spec/plugin/sessions_spec.rb +371 -0
- data/spec/plugin/typecast_params_spec.rb +11 -0
- data/spec/session_middleware_spec.rb +129 -0
- data/spec/session_spec.rb +2 -2
- data/spec/spec_helper.rb +10 -1
- metadata +9 -3
@@ -0,0 +1,451 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
3
|
+
require 'openssl'
|
4
|
+
|
5
|
+
begin
|
6
|
+
OpenSSL::Cipher.new("aes-256-ctr")
|
7
|
+
rescue OpenSSL::Cipher::CipherError
|
8
|
+
# :nocov:
|
9
|
+
raise LoadError, "Roda sessions plugin requires the aes-256-ctr cipher"
|
10
|
+
# :nocov:
|
11
|
+
end
|
12
|
+
|
13
|
+
require 'base64'
|
14
|
+
require 'json'
|
15
|
+
require 'securerandom'
|
16
|
+
require 'zlib'
|
17
|
+
|
18
|
+
class Roda
|
19
|
+
module RodaPlugins
|
20
|
+
# The sessions plugin adds support for sessions using cookies. It is the recommended
|
21
|
+
# way to support sessions in Roda applications.
|
22
|
+
#
|
23
|
+
# The session cookies are encrypted with AES-256-CTR and then signed with HMAC-SHA-256.
|
24
|
+
# By default, session data over a certain size is compressed to reduced space, and
|
25
|
+
# is padded to reduce information leaked based on the session size.
|
26
|
+
#
|
27
|
+
# Sessions are serialized via JSON, so session information should only store data that
|
28
|
+
# allows roundtrips via JSON (String, Integer, Float, Array, Hash, true, false, and nil).
|
29
|
+
# In particular, note that Symbol does not round trip via JSON, so symbols should not be
|
30
|
+
# used in sessions when this plugin is used. This plugin sets the
|
31
|
+
# +:sessions_convert_symbols+ application option to +true+ if it hasn't been set yet,
|
32
|
+
# for better integration with plugins that can use either symbol or string session or
|
33
|
+
# flash keys. Unlike Rack::Session::Cookie, the session is stored as a plain ruby hash,
|
34
|
+
# and does not convert all keys to strings.
|
35
|
+
#
|
36
|
+
# All sessions are timestamped and session expiration is enabled by default, with sessions
|
37
|
+
# being valid for 30 days maximum and 7 days since last use. Session creation time is
|
38
|
+
# reset whenever the session is empty when serialized and also whenever +clear_session+
|
39
|
+
# is called while processing the request.
|
40
|
+
#
|
41
|
+
# Session secrets can be rotated, and if so both the cipher and HMAC secrets should be
|
42
|
+
# rotated at the same time. See options below.
|
43
|
+
#
|
44
|
+
# The sessions plugin can transparently upgrade sessions from Rack::Session::Cookie
|
45
|
+
# if the default Rack::Session::Cookie coder and HMAC are used, see options below.
|
46
|
+
# It is recommended to only enable transparent upgrades for a brief transition period,
|
47
|
+
# and remove support for them once old sessions have converted or timed out.
|
48
|
+
#
|
49
|
+
# While session data will be compressed by default for sessions over a certain size,
|
50
|
+
# if the final cookie is too large (>=4096 bytes), a Roda::RodaPlugins::Sessions::CookieTooLarge
|
51
|
+
# exception will be raised.
|
52
|
+
#
|
53
|
+
# If the flash plugin is used, the sessions plugin should be loaded after the flash
|
54
|
+
# plugin, so that the flash plugin rotates the flash in the session before the sessions
|
55
|
+
# plugin serializes the session.
|
56
|
+
#
|
57
|
+
# = Required Options
|
58
|
+
#
|
59
|
+
# The session cookies this plugin uses are both encrypted and signed, so two separate
|
60
|
+
# secrets are used internally. However, for ease of use, these secrets are combined into
|
61
|
+
# a single +:secret+ option. The +:secret+ option must be a string of at least 64 bytes
|
62
|
+
# and should be randomly generated. The first 32 bytes are used as the secret for the
|
63
|
+
# cipher, any remaining bytes are used for the secret for the HMAC.
|
64
|
+
#
|
65
|
+
# = Other Options
|
66
|
+
#
|
67
|
+
# :cookie_options :: Any cookie options to set on the session cookie. By default, uses
|
68
|
+
# <tt>httponly: true, path: '/', same_site: :lax</tt> so that the cookie is not accessible
|
69
|
+
# to javascript, allowed for all paths, and will not be used for cross-site non-GET requests
|
70
|
+
# that. If the +:secure+ option is not present in the hash, then
|
71
|
+
# <tt>secure: true</tt> is also set if the request is made over HTTPS. If this option is
|
72
|
+
# given, it will be merged into the default cookie options.
|
73
|
+
# :gzip_over :: For session data over this many bytes, compress it with the deflate algorithm (default: 128).
|
74
|
+
# :key :: The cookie name to use (default: <tt>'roda.session'</tt>)
|
75
|
+
# :max_seconds :: The maximum number of seconds to allow for total session lifetime, starting with when
|
76
|
+
# the session was originally created. Default is <tt>86400*30</tt> (30 days). Can be set to
|
77
|
+
# +nil+ to disable session lifetime checks.
|
78
|
+
# :max_idle_sessions :: The maximum number of seconds to allow since the session was last updated.
|
79
|
+
# Default is <tt>86400*7</tt> (7 days). Can be set to nil to disable session idleness
|
80
|
+
# checks.
|
81
|
+
# :old_secret :: The previous secret to use, allowing for secret rotation. Must be a string of at least 64
|
82
|
+
# bytes if given.
|
83
|
+
# :pad_size :: Pad session data (after possible compression, before encryption), to a multiple of this
|
84
|
+
# many bytes (default: 32). This can be between 2-4096 bytes, or +nil+ to disable padding.
|
85
|
+
# :parser :: The parser for the serialized session data (default: <tt>JSON.method(:parse)</tt>).
|
86
|
+
# :serializer :: The serializer for the session data (default +:to_json.to_proc+).
|
87
|
+
# :skip_within :: If the last update time for the session cookie is less than this number of seconds from the
|
88
|
+
# current time, and the session has not been modified, do not set a new session cookie
|
89
|
+
# (default: 3600).
|
90
|
+
# :upgrade_from_rack_session_cookie_key :: The cookie name to use for transparently upgrading from
|
91
|
+
# Rack::Session:Cookie (defaults to <tt>'rack.session'</tt>).
|
92
|
+
# :upgrade_from_rack_session_cookie_secret :: The secret for the HMAC-SHA1 signature when allowing
|
93
|
+
# transparent upgrades from Rack::Session::Cookie. Using this
|
94
|
+
# option is only recommended during a short transition period,
|
95
|
+
# and is not enabled by default as it lowers security.
|
96
|
+
# :upgrade_from_rack_session_cookie_options :: Options to pass when deleting the cookie used by
|
97
|
+
# Rack::Session::Cookie after converting it to use the session
|
98
|
+
# cookies used by this plugin.
|
99
|
+
#
|
100
|
+
# = Not a Rack Middleware
|
101
|
+
#
|
102
|
+
# Unlike some other approaches to sessions, the sessions plugin does not use
|
103
|
+
# a rack middleware, so session information is not available to other rack middleware,
|
104
|
+
# only to the application itself, with the session not being loaded from the cookie
|
105
|
+
# until the +session+ method is called.
|
106
|
+
#
|
107
|
+
# If you need rack middleware to access the session information, then
|
108
|
+
# <tt>require 'roda/session_middleware'</tt> and <tt>use RodaSessionMiddleware</tt>.
|
109
|
+
# <tt>RodaSessionMiddleware</tt> passes the options given to this plugin.
|
110
|
+
#
|
111
|
+
# = Session Cookie Cryptography/Format
|
112
|
+
#
|
113
|
+
# Session cookies created by this plugin use the following format:
|
114
|
+
#
|
115
|
+
# urlsafe_base64(version + IV + auth tag + encrypted session data + HMAC)
|
116
|
+
#
|
117
|
+
# where:
|
118
|
+
#
|
119
|
+
# version :: 1 byte, currently must be 0, other values reserved for future expansion.
|
120
|
+
# IV :: 16 bytes, initialization vector for AES-256-CTR cipher.
|
121
|
+
# encrypted session data :: >=12 bytes of data encrypted with AES-256-CTR cipher, see below.
|
122
|
+
# HMAC :: 32 bytes, HMAC-SHA-256 of all preceding data plus cookie key (so that a cookie value
|
123
|
+
# for a different key cannot be used even if the secret is the same).
|
124
|
+
#
|
125
|
+
# The encrypted session data uses the following format:
|
126
|
+
#
|
127
|
+
# bitmap + creation time + update time + padding + serialized data
|
128
|
+
#
|
129
|
+
# where:
|
130
|
+
#
|
131
|
+
# bitmap :: 2 bytes in little endian format, lower 12 bits storing number of padding
|
132
|
+
# bytes, 13th bit storing whether serialized data is compressed with deflate.
|
133
|
+
# Bits 14-16 reserved for future expansion.
|
134
|
+
# creation time :: 4 byte integer in unsigned little endian format, storing unix timestamp
|
135
|
+
# since session initially created.
|
136
|
+
# update time :: 4 byte integer in unsigned little endian format, storing unix timestamp
|
137
|
+
# since session last updated.
|
138
|
+
# padding :: >=0 padding bytes specified in bitmap, filled with random data, can be ignored.
|
139
|
+
# serialized data :: >=2 bytes of serialized data in JSON format. If the bitmap indicates
|
140
|
+
# deflate compression, this contains the deflate compressed data.
|
141
|
+
module Sessions
|
142
|
+
DEFAULT_COOKIE_OPTIONS = {:httponly=>true, :path=>'/'.freeze, :same_site=>:lax}.freeze
|
143
|
+
DEFAULT_OPTIONS = {:key => 'roda.session'.freeze, :max_seconds=>86400*30, :max_idle_seconds=>86400*7, :pad_size=>32, :gzip_over=>128, :skip_within=>3600}.freeze
|
144
|
+
DEFLATE_BIT = 0x1000
|
145
|
+
PADDING_MASK = 0x0fff
|
146
|
+
SESSION_CREATED_AT = 'roda.session.created_at'.freeze
|
147
|
+
SESSION_UPDATED_AT = 'roda.session.updated_at'.freeze
|
148
|
+
SESSION_SERIALIZED = 'roda.session.serialized'.freeze
|
149
|
+
SESSION_DELETE_RACK_COOKIE = 'roda.session.delete_rack_session_cookie'.freeze
|
150
|
+
|
151
|
+
# Exception class used when creating a session cookie that would exceed the
|
152
|
+
# allowable cookie size limit.
|
153
|
+
class CookieTooLarge < RodaError
|
154
|
+
end
|
155
|
+
|
156
|
+
# Split given secret into a cipher secret and an hmac secret.
|
157
|
+
def self.split_secret(name, secret)
|
158
|
+
raise RodaError, "sessions plugin :#{name} option must be a String" unless secret.is_a?(String)
|
159
|
+
raise RodaError, "invalid sessions plugin :#{name} option length: #{secret.bytesize}, must be >=64" unless secret.bytesize >= 64
|
160
|
+
hmac_secret = secret = secret.dup.force_encoding('BINARY')
|
161
|
+
cipher_secret = secret.slice!(0, 32)
|
162
|
+
[cipher_secret.freeze, hmac_secret.freeze]
|
163
|
+
end
|
164
|
+
|
165
|
+
# Configure the plugin, see Sessions for details on options.
|
166
|
+
def self.configure(app, opts=OPTS)
|
167
|
+
plugin_opts = opts
|
168
|
+
opts = (app.opts[:sessions] || DEFAULT_OPTIONS).merge(opts)
|
169
|
+
co = opts[:cookie_options] = DEFAULT_COOKIE_OPTIONS.merge(opts[:cookie_options] || OPTS).freeze
|
170
|
+
opts[:parser] ||= app.opts[:json_parser] || JSON.method(:parse)
|
171
|
+
opts[:serializer] ||= app.opts[:json_serializer] || :to_json.to_proc
|
172
|
+
|
173
|
+
if opts[:upgrade_from_rack_session_cookie_secret]
|
174
|
+
opts[:upgrade_from_rack_session_cookie_key] ||= 'rack.session'
|
175
|
+
rsco = opts[:upgrade_from_rack_session_cookie_options] = Hash[opts[:upgrade_from_rack_session_cookie_options] || OPTS]
|
176
|
+
rsco[:path] ||= co[:path]
|
177
|
+
rsco[:domain] ||= co[:domain]
|
178
|
+
end
|
179
|
+
|
180
|
+
opts[:cipher_secret], opts[:hmac_secret] = split_secret(:secret, opts[:secret])
|
181
|
+
opts[:old_cipher_secret], opts[:old_hmac_secret] = (split_secret(:old_secret, opts[:old_secret]) if opts[:old_secret])
|
182
|
+
|
183
|
+
case opts[:pad_size]
|
184
|
+
when nil
|
185
|
+
# no changes
|
186
|
+
when Integer
|
187
|
+
raise RodaError, "invalid :pad_size: #{opts[:pad_size]}, must be >=2, < 4096" unless opts[:pad_size] >= 2 && opts[:pad_size] < 4096
|
188
|
+
else
|
189
|
+
raise RodaError, "invalid :pad_size option: #{opts[:pad_size].inspect}, must be Integer or nil"
|
190
|
+
end
|
191
|
+
|
192
|
+
app.opts[:sessions] = opts.freeze
|
193
|
+
app.opts[:sessions_convert_symbols] = true unless app.opts.has_key?(:sessions_convert_symbols)
|
194
|
+
end
|
195
|
+
|
196
|
+
module InstanceMethods
|
197
|
+
# If session information has been set in the request environment,
|
198
|
+
# update the rack response headers to set the session cookie in
|
199
|
+
# the response.
|
200
|
+
def call
|
201
|
+
res = super
|
202
|
+
|
203
|
+
if session = env['rack.session']
|
204
|
+
@_request.persist_session(res[1], session)
|
205
|
+
end
|
206
|
+
|
207
|
+
res
|
208
|
+
end
|
209
|
+
|
210
|
+
# Clear data from the session, and update the request environment
|
211
|
+
# so that the session cookie will use a new creation timestamp
|
212
|
+
# instead of the previous creation timestamp.
|
213
|
+
def clear_session
|
214
|
+
session.clear
|
215
|
+
env.delete(SESSION_CREATED_AT)
|
216
|
+
env.delete(SESSION_UPDATED_AT)
|
217
|
+
nil
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
module RequestMethods
|
222
|
+
# Load the session information from the cookie. With the sessions
|
223
|
+
# plugin, you must call this method to get the session, instead of
|
224
|
+
# trying to access the session directly through the request environment.
|
225
|
+
# For maximum compatibility with other software that uses rack sessions,
|
226
|
+
# this method stores the session in 'rack.session' in the request environment,
|
227
|
+
# but that does not happen until this method is called.
|
228
|
+
def session
|
229
|
+
@env['rack.session'] ||= _load_session
|
230
|
+
end
|
231
|
+
|
232
|
+
# Persist the session data as a cookie. If transparently upgrading from
|
233
|
+
# Rack::Session::Cookie, mark the related cookie for expiration so it isn't
|
234
|
+
# sent in the future.
|
235
|
+
def persist_session(headers, session)
|
236
|
+
opts = roda_class.opts[:sessions]
|
237
|
+
|
238
|
+
if session.empty?
|
239
|
+
if env[SESSION_SERIALIZED]
|
240
|
+
# If session was submitted and is now empty, remove the cookie
|
241
|
+
Rack::Utils.delete_cookie_header!(headers, opts[:key])
|
242
|
+
# else
|
243
|
+
# If no session was submitted, and the session is empty
|
244
|
+
# then there is no need to do anything
|
245
|
+
end
|
246
|
+
elsif cookie_value = _serialize_session(session)
|
247
|
+
cookie = Hash[opts[:cookie_options]]
|
248
|
+
cookie[:value] = cookie_value
|
249
|
+
cookie[:secure] = true if !cookie.has_key?(:secure) && ssl?
|
250
|
+
Rack::Utils.set_cookie_header!(headers, opts[:key], cookie)
|
251
|
+
end
|
252
|
+
|
253
|
+
if env[SESSION_DELETE_RACK_COOKIE]
|
254
|
+
Rack::Utils.delete_cookie_header!(headers, opts[:upgrade_from_rack_session_cookie_key], opts[:upgrade_from_rack_session_cookie_options])
|
255
|
+
end
|
256
|
+
|
257
|
+
nil
|
258
|
+
end
|
259
|
+
|
260
|
+
private
|
261
|
+
|
262
|
+
# Load the session by looking for the appropriate cookie, or falling
|
263
|
+
# back to the rack session cookie if configured.
|
264
|
+
def _load_session
|
265
|
+
opts = roda_class.opts[:sessions]
|
266
|
+
cs = cookies
|
267
|
+
|
268
|
+
if data = cs[opts[:key]]
|
269
|
+
_deserialize_session(data)
|
270
|
+
elsif (key = opts[:upgrade_from_rack_session_cookie_key]) && (data = cs[key])
|
271
|
+
_deserialize_rack_session(data)
|
272
|
+
end || {}
|
273
|
+
end
|
274
|
+
|
275
|
+
# If 'rack.errors' is set, write the error message to it.
|
276
|
+
# This is used for errors that shouldn't be raised as exceptions,
|
277
|
+
# such as improper session cookies.
|
278
|
+
def _session_serialization_error(msg)
|
279
|
+
return unless error_stream = @env['rack.errors']
|
280
|
+
error_stream.puts(msg)
|
281
|
+
nil
|
282
|
+
end
|
283
|
+
|
284
|
+
# Interpret given cookie data as a Rack::Session::Cookie
|
285
|
+
# serialized session using the default Rack::Session::Cookie
|
286
|
+
# hmac and coder.
|
287
|
+
def _deserialize_rack_session(data)
|
288
|
+
opts = roda_class.opts[:sessions]
|
289
|
+
key = opts[:upgrade_from_rack_session_cookie_key]
|
290
|
+
secret = opts[:upgrade_from_rack_session_cookie_secret]
|
291
|
+
data, digest = data.split("--", 2)
|
292
|
+
unless digest
|
293
|
+
return _session_serialization_error("Not decoding Rack::Session::Cookie session: invalid format")
|
294
|
+
end
|
295
|
+
unless Rack::Utils.secure_compare(digest, OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA1.new, opts[:upgrade_from_rack_session_cookie_secret], data))
|
296
|
+
return _session_serialization_error("Not decoding Rack::Session::Cookie session: HMAC invalid")
|
297
|
+
end
|
298
|
+
|
299
|
+
begin
|
300
|
+
session = Marshal.load(data.unpack('m').first)
|
301
|
+
rescue
|
302
|
+
return _session_serialization_error("Error decoding Rack::Session::Cookie session: not base64 encoded marshal dump")
|
303
|
+
end
|
304
|
+
|
305
|
+
# Mark rack session cookie for deletion on success
|
306
|
+
env[SESSION_DELETE_RACK_COOKIE] = true
|
307
|
+
|
308
|
+
# Convert the rack session by roundtripping it through
|
309
|
+
# the parser and serializer, so that you would get the
|
310
|
+
# same result as you would if the session was handled
|
311
|
+
# by this plugin.
|
312
|
+
env[SESSION_SERIALIZED] = data = opts[:serializer].call(session)
|
313
|
+
env[SESSION_CREATED_AT] = Time.now.to_i
|
314
|
+
opts[:parser].call(data)
|
315
|
+
end
|
316
|
+
|
317
|
+
# Interpret given cookie data as a Rack::Session::Cookie
|
318
|
+
def _deserialize_session(data)
|
319
|
+
opts = roda_class.opts[:sessions]
|
320
|
+
|
321
|
+
begin
|
322
|
+
data = Base64.urlsafe_decode64(data)
|
323
|
+
rescue ArgumentError
|
324
|
+
return _session_serialization_error("Unable to decode session: invalid base64")
|
325
|
+
end
|
326
|
+
length = data.bytesize
|
327
|
+
if data.length < 61
|
328
|
+
# minimum length (1+16+12+32) (version+cipher_iv+minimum session+hmac)
|
329
|
+
# 1 : version
|
330
|
+
# 16 : cipher_iv
|
331
|
+
# 12 : minimum_session
|
332
|
+
# 2 : bitmap for gzip + padding info
|
333
|
+
# 4 : creation time
|
334
|
+
# 4 : update time
|
335
|
+
# 2 : data
|
336
|
+
# 32 : HMAC-SHA-256
|
337
|
+
return _session_serialization_error("Unable to decode session: data too short")
|
338
|
+
end
|
339
|
+
|
340
|
+
unless data.getbyte(0) == 0
|
341
|
+
# version marker
|
342
|
+
return _session_serialization_error("Unable to decode session: version marker unsupported")
|
343
|
+
end
|
344
|
+
|
345
|
+
encrypted_data = data.slice!(0, length-32)
|
346
|
+
unless Rack::Utils.secure_compare(data, OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new, opts[:hmac_secret], encrypted_data+opts[:key]))
|
347
|
+
if opts[:old_hmac_secret] && Rack::Utils.secure_compare(data, OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new, opts[:old_hmac_secret], encrypted_data+opts[:key]))
|
348
|
+
use_old_cipher_secret = true
|
349
|
+
else
|
350
|
+
return _session_serialization_error("Not decoding session: HMAC invalid")
|
351
|
+
end
|
352
|
+
end
|
353
|
+
|
354
|
+
encrypted_data.slice!(0)
|
355
|
+
cipher = OpenSSL::Cipher.new("aes-256-ctr")
|
356
|
+
|
357
|
+
# Not rescuing cipher errors. If there is an error in the decryption, that's
|
358
|
+
# either a bug in the plugin that needs to be fixed, or an attacker is already
|
359
|
+
# able to forge a valid HMAC, in which case the error should be raised to
|
360
|
+
# alert the application owner about the problem.
|
361
|
+
cipher.decrypt
|
362
|
+
cipher.key = opts[use_old_cipher_secret ? :old_cipher_secret : :cipher_secret]
|
363
|
+
cipher_iv = cipher.iv = encrypted_data.slice!(0, 16)
|
364
|
+
data = cipher.update(encrypted_data) << cipher.final
|
365
|
+
|
366
|
+
bitmap, created_at, updated_at = data.unpack('vVV')
|
367
|
+
padding_bytes = bitmap & PADDING_MASK
|
368
|
+
if (max = opts[:max_seconds]) && Time.now.to_i > created_at + max
|
369
|
+
return _session_serialization_error("Not returning session: maximum session time expired")
|
370
|
+
end
|
371
|
+
if (max = opts[:max_idle_seconds]) && Time.now.to_i > updated_at + max
|
372
|
+
return _session_serialization_error("Not returning session: maximum session idle time expired")
|
373
|
+
end
|
374
|
+
|
375
|
+
data = data.slice(10+padding_bytes, data.bytesize)
|
376
|
+
|
377
|
+
if bitmap & DEFLATE_BIT > 0
|
378
|
+
data = Zlib::Inflate.inflate(data)
|
379
|
+
end
|
380
|
+
|
381
|
+
env = @env
|
382
|
+
env[SESSION_CREATED_AT] = created_at
|
383
|
+
env[SESSION_UPDATED_AT] = updated_at
|
384
|
+
env[SESSION_SERIALIZED] = data
|
385
|
+
|
386
|
+
opts[:parser].call(data)
|
387
|
+
end
|
388
|
+
|
389
|
+
def _serialize_session(session)
|
390
|
+
opts = roda_class.opts[:sessions]
|
391
|
+
env = @env
|
392
|
+
now = Time.now.to_i
|
393
|
+
json_data = opts[:serializer].call(session).force_encoding('BINARY')
|
394
|
+
|
395
|
+
if (serialized_session = env[SESSION_SERIALIZED]) &&
|
396
|
+
(updated_at = env[SESSION_UPDATED_AT]) &&
|
397
|
+
(now - updated_at < opts[:skip_within]) &&
|
398
|
+
(serialized_session == json_data)
|
399
|
+
return
|
400
|
+
end
|
401
|
+
|
402
|
+
bitmap = 0
|
403
|
+
json_length = json_data.bytesize
|
404
|
+
|
405
|
+
if json_length > opts[:gzip_over]
|
406
|
+
json_data = Zlib.deflate(json_data)
|
407
|
+
json_length = json_data.bytesize
|
408
|
+
bitmap |= DEFLATE_BIT
|
409
|
+
end
|
410
|
+
|
411
|
+
# When calculating padding bytes to use, include 10 bytes for bitmap and
|
412
|
+
# session create/update times, so total size of encrypted data is a
|
413
|
+
# multiple of pad_size.
|
414
|
+
if (pad_size = opts[:pad_size]) && (padding_bytes = (json_length+10) % pad_size) != 0
|
415
|
+
padding_bytes = pad_size - padding_bytes
|
416
|
+
bitmap |= padding_bytes
|
417
|
+
padding_data = SecureRandom.random_bytes(padding_bytes)
|
418
|
+
end
|
419
|
+
|
420
|
+
session_create_time = env[SESSION_CREATED_AT]
|
421
|
+
serialized_data = [bitmap, session_create_time||now, now].pack('vVV')
|
422
|
+
|
423
|
+
serialized_data << padding_data if padding_data
|
424
|
+
serialized_data << json_data
|
425
|
+
|
426
|
+
cipher = OpenSSL::Cipher.new("aes-256-ctr")
|
427
|
+
cipher.encrypt
|
428
|
+
cipher.key = opts[:cipher_secret]
|
429
|
+
cipher_iv = cipher.random_iv
|
430
|
+
encrypted_data = cipher.update(serialized_data) << cipher.final
|
431
|
+
|
432
|
+
data = String.new
|
433
|
+
data << "\0" # version marker
|
434
|
+
data << cipher_iv
|
435
|
+
data << encrypted_data
|
436
|
+
data << OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new, opts[:hmac_secret], data+opts[:key])
|
437
|
+
|
438
|
+
data = Base64.urlsafe_encode64(data)
|
439
|
+
|
440
|
+
if data.bytesize >= 4096
|
441
|
+
raise CookieTooLarge, "attempted to create cookie larger than 4096 bytes"
|
442
|
+
end
|
443
|
+
|
444
|
+
data
|
445
|
+
end
|
446
|
+
end
|
447
|
+
end
|
448
|
+
|
449
|
+
register_plugin(:sessions, Sessions)
|
450
|
+
end
|
451
|
+
end
|
@@ -106,7 +106,9 @@ class Roda
|
|
106
106
|
# int :: Converts value to integer using +to_i+ (note that invalid input strings will be
|
107
107
|
# returned as 0)
|
108
108
|
# pos_int :: Converts value using +to_i+, but non-positive values are converted to +nil+
|
109
|
-
# Integer :: Converts value to integer using <tt>Kernel::Integer
|
109
|
+
# Integer :: Converts value to integer using <tt>Kernel::Integer</tt>, with base 10 for
|
110
|
+
# string inputs, and a check that the output value is equal to the input
|
111
|
+
# value for numeric inputs.
|
110
112
|
# float :: Converts value to float using +to_f+ (note that invalid input strings will be
|
111
113
|
# returned as 0.0)
|
112
114
|
# Float :: Converts value to float using <tt>Kernel::Float(value)</tt>
|
@@ -453,7 +455,18 @@ class Roda
|
|
453
455
|
end
|
454
456
|
|
455
457
|
handle_type(:Integer) do |v|
|
456
|
-
string_or_numeric!(v)
|
458
|
+
if string_or_numeric!(v)
|
459
|
+
case v
|
460
|
+
when String
|
461
|
+
::Kernel::Integer(v, 10)
|
462
|
+
when Integer
|
463
|
+
v
|
464
|
+
else
|
465
|
+
i = ::Kernel::Integer(v)
|
466
|
+
raise Error, "numeric value passed to Integer contains non-Integer part: #{v.inspect}" unless i == v
|
467
|
+
i
|
468
|
+
end
|
469
|
+
end
|
457
470
|
end
|
458
471
|
|
459
472
|
handle_type(:float) do |v|
|