roda 3.109.0 → 3.110.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/roda/cache.rb +0 -2
- data/lib/roda/opts.rb +1 -1
- data/lib/roda/plugins/_symbol_class_matchers.rb +2 -4
- data/lib/roda/plugins/additional_view_directories.rb +2 -2
- data/lib/roda/plugins/assets.rb +173 -142
- data/lib/roda/plugins/assets_preloading.rb +26 -10
- data/lib/roda/plugins/backtracking_array.rb +2 -2
- data/lib/roda/plugins/branch_locals.rb +4 -4
- data/lib/roda/plugins/break.rb +2 -2
- data/lib/roda/plugins/caching.rb +27 -17
- data/lib/roda/plugins/capture_erb.rb +15 -12
- data/lib/roda/plugins/chunked.rb +32 -30
- data/lib/roda/plugins/class_level_routing.rb +5 -2
- data/lib/roda/plugins/class_matchers.rb +11 -6
- data/lib/roda/plugins/common_logger.rb +21 -3
- data/lib/roda/plugins/conditional_sessions.rb +3 -3
- data/lib/roda/plugins/content_for.rb +7 -6
- data/lib/roda/plugins/content_security_policy.rb +11 -12
- data/lib/roda/plugins/cookie_flags.rb +12 -6
- data/lib/roda/plugins/cookies.rb +7 -5
- data/lib/roda/plugins/csrf.rb +2 -1
- data/lib/roda/plugins/custom_block_results.rb +6 -7
- data/lib/roda/plugins/default_headers.rb +2 -2
- data/lib/roda/plugins/delay_build.rb +1 -1
- data/lib/roda/plugins/delegate.rb +7 -6
- data/lib/roda/plugins/direct_call.rb +5 -1
- data/lib/roda/plugins/disallow_file_uploads.rb +2 -3
- data/lib/roda/plugins/drop_body.rb +3 -6
- data/lib/roda/plugins/each_part.rb +4 -6
- data/lib/roda/plugins/early_hints.rb +4 -4
- data/lib/roda/plugins/empty_root.rb +5 -5
- data/lib/roda/plugins/environments.rb +8 -8
- data/lib/roda/plugins/erb_h.rb +4 -4
- data/lib/roda/plugins/error_email.rb +16 -17
- data/lib/roda/plugins/error_handler.rb +24 -32
- data/lib/roda/plugins/error_mail.rb +26 -15
- data/lib/roda/plugins/exception_page.rb +41 -35
- data/lib/roda/plugins/filter_common_logger.rb +6 -4
- data/lib/roda/plugins/flash.rb +16 -13
- data/lib/roda/plugins/freeze_request_response.rb +24 -0
- data/lib/roda/plugins/hash_routes.rb +1 -1
- data/lib/roda/plugins/permissions_policy.rb +14 -16
- data/lib/roda/plugins/public.rb +11 -0
- data/lib/roda/plugins/recheck_precompiled_assets.rb +30 -8
- data/lib/roda/plugins/render.rb +3 -0
- data/lib/roda/plugins/render_locals.rb +1 -1
- data/lib/roda/plugins/sessions.rb +158 -137
- data/lib/roda/plugins/symbol_matchers.rb +8 -5
- data/lib/roda/plugins/unescape_path.rb +22 -4
- data/lib/roda/plugins.rb +24 -0
- data/lib/roda/request.rb +8 -7
- data/lib/roda/response.rb +9 -7
- data/lib/roda/version.rb +1 -1
- data/lib/roda.rb +12 -9
- metadata +2 -1
|
@@ -29,8 +29,8 @@ class Roda
|
|
|
29
29
|
# used in sessions when this plugin is used. This plugin sets the
|
|
30
30
|
# +:sessions_convert_symbols+ application option to +true+ if it hasn't been set yet,
|
|
31
31
|
# for better integration with plugins that can use either symbol or string session or
|
|
32
|
-
# flash keys. Unlike
|
|
33
|
-
# and does not convert
|
|
32
|
+
# flash keys. Unlike some other session implementations, the session is stored as a plain ruby hash,
|
|
33
|
+
# and does not convert keys to strings.
|
|
34
34
|
#
|
|
35
35
|
# All sessions are timestamped and session expiration is enabled by default, with sessions
|
|
36
36
|
# being valid for 30 days maximum and 7 days since last use by default. Session creation time is
|
|
@@ -39,67 +39,43 @@ class Roda
|
|
|
39
39
|
#
|
|
40
40
|
# Session secrets can be rotated. See options below.
|
|
41
41
|
#
|
|
42
|
-
# The sessions plugin can transparently upgrade sessions from versions of Rack::Session::Cookie
|
|
43
|
-
# shipped with Rack before Rack 3,
|
|
44
|
-
# if the default Rack::Session::Cookie coder and HMAC are used, see options below.
|
|
45
|
-
# It is recommended to only enable transparent upgrades for a brief transition period,
|
|
46
|
-
# and remove support for them once old sessions have converted or timed out.
|
|
47
|
-
#
|
|
48
42
|
# If the final cookie is too large (>=4096 bytes), a Roda::RodaPlugins::Sessions::CookieTooLarge
|
|
49
43
|
# exception will be raised.
|
|
50
44
|
#
|
|
51
|
-
# = Required
|
|
45
|
+
# = Required Keyword Arguments
|
|
52
46
|
#
|
|
53
47
|
# The session cookies this plugin uses are both encrypted and signed, so two separate
|
|
54
48
|
# secrets are used internally. However, for ease of use, these secrets are combined into
|
|
55
|
-
# a single
|
|
49
|
+
# a single +secret+ argument. The +secret+ argument must be a string of at least 64 bytes
|
|
56
50
|
# and should be randomly generated. The first 32 bytes are used as the secret for the
|
|
57
51
|
# cipher, any remaining bytes are used for the secret for the HMAC.
|
|
58
52
|
#
|
|
59
|
-
# =
|
|
53
|
+
# = Optional Keyword Arguments
|
|
60
54
|
#
|
|
61
|
-
#
|
|
62
|
-
#
|
|
63
|
-
#
|
|
64
|
-
#
|
|
65
|
-
#
|
|
66
|
-
#
|
|
67
|
-
#
|
|
68
|
-
#
|
|
69
|
-
#
|
|
70
|
-
#
|
|
71
|
-
#
|
|
72
|
-
#
|
|
73
|
-
#
|
|
74
|
-
#
|
|
75
|
-
#
|
|
76
|
-
#
|
|
77
|
-
#
|
|
78
|
-
#
|
|
79
|
-
#
|
|
80
|
-
#
|
|
81
|
-
# :pad_size :: Pad session data (after possible compression, before encryption), to a multiple of this
|
|
82
|
-
# many bytes (default: 32). This can be between 2-4096 bytes, or +nil+ to disable padding.
|
|
83
|
-
# :per_cookie_cipher_secret :: Uses a separate cipher key for every cookie, with the key used generated using
|
|
84
|
-
# HMAC-SHA-256 of 32 bytes of random data with the default cipher secret. This
|
|
85
|
-
# offers additional protection in case the random initialization vector used when
|
|
86
|
-
# encrypting the session data has been reused. Odds of that are 1 in 2**64 if
|
|
87
|
-
# initialization vector is truly random, but weaknesses in the random number
|
|
88
|
-
# generator could make the odds much higher. Default is +true+.
|
|
89
|
-
# :parser :: The parser for the serialized session data (default: <tt>JSON.method(:parse)</tt>).
|
|
55
|
+
# +cookie_options+ :: Any cookie options to set on the session cookie. By default, uses
|
|
56
|
+
# <tt>httponly: true, path: '/', same_site: :lax</tt> so that the cookie is not accessible
|
|
57
|
+
# to javascript, allowed for all paths, and will not be used for cross-site non-GET requests.
|
|
58
|
+
# If the +:secure+ option is not present in the hash, then
|
|
59
|
+
# <tt>secure: true</tt> is also set if the request is made over HTTPS. If this option is
|
|
60
|
+
# given, it will be merged into the default cookie options.
|
|
61
|
+
# +env_key+ :: The key in `env` where the session should be located. Defaults to <tt>"rack.session"</tt>, the
|
|
62
|
+
# default key for sessions in Rack.
|
|
63
|
+
# +key+ :: The cookie name to use (default: <tt>'roda.session'</tt>)
|
|
64
|
+
# +max_idle_seconds+ :: The maximum number of seconds to allow since the session was last updated.
|
|
65
|
+
# Default is <tt>86400*7</tt> (7 days). Can be set to nil to disable session idleness
|
|
66
|
+
# checks.
|
|
67
|
+
# +max_seconds+ :: The maximum number of seconds to allow for total session lifetime, starting with when
|
|
68
|
+
# the session was originally created. Default is <tt>86400*30</tt> (30 days). Can be set to
|
|
69
|
+
# +nil+ to disable session lifetime checks.
|
|
70
|
+
# +old_secret+ :: The previous secret to use, allowing for secret rotation. Must be a string of at least 64
|
|
71
|
+
# bytes if given.
|
|
72
|
+
# +pad_size+ :: Pad session data to a multiple of this many bytes (default: 32). This can be between
|
|
73
|
+
# 2-4096 bytes, or +nil+ to disable padding.
|
|
74
|
+
# +parser+ :: The parser for the serialized session data (default: <tt>JSON.method(:parse)</tt>).
|
|
90
75
|
# :serializer :: The serializer for the session data (default +:to_json.to_proc+).
|
|
91
76
|
# :skip_within :: If the last update time for the session cookie is less than this number of seconds from the
|
|
92
77
|
# current time, and the session has not been modified, do not set a new session cookie
|
|
93
78
|
# (default: 3600).
|
|
94
|
-
# :upgrade_from_rack_session_cookie_key :: The cookie name to use for transparently upgrading from
|
|
95
|
-
# Rack::Session:Cookie (defaults to <tt>'rack.session'</tt>).
|
|
96
|
-
# :upgrade_from_rack_session_cookie_secret (deprecated) :: The secret for the HMAC-SHA1 signature when allowing
|
|
97
|
-
# transparent upgrades from Rack::Session::Cookie. Using this
|
|
98
|
-
# option is only recommended during a short transition period,
|
|
99
|
-
# and is not enabled by default as it lowers security.
|
|
100
|
-
# :upgrade_from_rack_session_cookie_options :: Options to pass when deleting the cookie used by
|
|
101
|
-
# Rack::Session::Cookie after converting it to use the session
|
|
102
|
-
# cookies used by this plugin.
|
|
103
79
|
#
|
|
104
80
|
# = Not a Rack Middleware
|
|
105
81
|
#
|
|
@@ -118,13 +94,9 @@ class Roda
|
|
|
118
94
|
#
|
|
119
95
|
# urlsafe_base64("\1" + random_data + IV + encrypted session data + HMAC)
|
|
120
96
|
#
|
|
121
|
-
# If +:per_cookie_cipher_secret+ option is set to +false+, an older format is used:
|
|
122
|
-
#
|
|
123
|
-
# urlsafe_base64("\0" + IV + encrypted session data + HMAC)
|
|
124
|
-
#
|
|
125
97
|
# where:
|
|
126
98
|
#
|
|
127
|
-
# version :: 1 byte, currently must be 1
|
|
99
|
+
# version :: 1 byte, currently must be 1, other values reserved for future expansion.
|
|
128
100
|
# random_data :: 32 bytes, used for generating the per-cookie secret
|
|
129
101
|
# IV :: 16 bytes, initialization vector for AES-256-CTR cipher.
|
|
130
102
|
# encrypted session data :: >=12 bytes of data encrypted with AES-256-CTR cipher, see below.
|
|
@@ -138,15 +110,13 @@ class Roda
|
|
|
138
110
|
# where:
|
|
139
111
|
#
|
|
140
112
|
# bitmap :: 2 bytes in little endian format, lower 12 bits storing number of padding
|
|
141
|
-
# bytes
|
|
142
|
-
# Bits 14-16 reserved for future expansion.
|
|
113
|
+
# bytes. Bits 13-16 reserved for future expansion.
|
|
143
114
|
# creation time :: 4 byte integer in unsigned little endian format, storing unix timestamp
|
|
144
115
|
# since session initially created.
|
|
145
116
|
# update time :: 4 byte integer in unsigned little endian format, storing unix timestamp
|
|
146
117
|
# since session last updated.
|
|
147
118
|
# padding :: >=0 padding bytes specified in bitmap, filled with random data, can be ignored.
|
|
148
|
-
# serialized data :: >=2 bytes of serialized data in JSON format.
|
|
149
|
-
# deflate compression, this contains the deflate compressed data.
|
|
119
|
+
# serialized data :: >=2 bytes of serialized data in JSON format.
|
|
150
120
|
module Sessions
|
|
151
121
|
DEFAULT_COOKIE_OPTIONS = {:httponly=>true, :path=>'/'.freeze, :same_site=>:lax}.freeze
|
|
152
122
|
DEFAULT_OPTIONS = {:key => 'roda.session'.freeze, :max_seconds=>86400*30, :max_idle_seconds=>86400*7, :pad_size=>32, :gzip_over=>nil, :skip_within=>3600, :env_key=>'rack.session'}.freeze
|
|
@@ -158,6 +128,56 @@ class Roda
|
|
|
158
128
|
SESSION_VERSION_NUM = 'roda.session.version'.freeze
|
|
159
129
|
SESSION_DELETE_RACK_COOKIE = 'roda.session.delete_rack_session_cookie'.freeze
|
|
160
130
|
|
|
131
|
+
%i[
|
|
132
|
+
DEFAULT_COOKIE_OPTIONS
|
|
133
|
+
DEFAULT_OPTIONS
|
|
134
|
+
DEFLATE_BIT
|
|
135
|
+
PADDING_MASK
|
|
136
|
+
SESSION_CREATED_AT
|
|
137
|
+
SESSION_UPDATED_AT
|
|
138
|
+
SESSION_SERIALIZED
|
|
139
|
+
SESSION_VERSION_NUM
|
|
140
|
+
SESSION_DELETE_RACK_COOKIE].each do |sym|
|
|
141
|
+
RodaPlugins.deprecate_constant self, sym
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
PLUGIN_ARGUMENTS = %i[
|
|
145
|
+
cookie_options
|
|
146
|
+
env_key
|
|
147
|
+
gzip_over
|
|
148
|
+
key
|
|
149
|
+
max_idle_seconds
|
|
150
|
+
max_seconds
|
|
151
|
+
old_secret
|
|
152
|
+
pad_size
|
|
153
|
+
parser
|
|
154
|
+
per_cookie_cipher_secret
|
|
155
|
+
serializer
|
|
156
|
+
secret
|
|
157
|
+
skip_within
|
|
158
|
+
upgrade_from_rack_session_cookie_key
|
|
159
|
+
upgrade_from_rack_session_cookie_secret
|
|
160
|
+
upgrade_from_rack_session_cookie_options
|
|
161
|
+
].freeze
|
|
162
|
+
private_constant :PLUGIN_ARGUMENTS
|
|
163
|
+
|
|
164
|
+
deprecated_plugin_arguments = %i[
|
|
165
|
+
gzip_over
|
|
166
|
+
per_cookie_cipher_secret
|
|
167
|
+
upgrade_from_rack_session_cookie_key
|
|
168
|
+
upgrade_from_rack_session_cookie_secret
|
|
169
|
+
upgrade_from_rack_session_cookie_options
|
|
170
|
+
]
|
|
171
|
+
|
|
172
|
+
Config = Struct.new(*(PLUGIN_ARGUMENTS - deprecated_plugin_arguments - %i[secret old_secret]),
|
|
173
|
+
:remove_cookie_options,
|
|
174
|
+
:cipher_secret,
|
|
175
|
+
:hmac_secret,
|
|
176
|
+
:old_cipher_secret,
|
|
177
|
+
:old_hmac_secret
|
|
178
|
+
)
|
|
179
|
+
private_constant :Config
|
|
180
|
+
|
|
161
181
|
# Exception class used when creating a session cookie that would exceed the
|
|
162
182
|
# allowable cookie size limit.
|
|
163
183
|
class CookieTooLarge < RodaError
|
|
@@ -167,57 +187,71 @@ class Roda
|
|
|
167
187
|
def self.split_secret(name, secret)
|
|
168
188
|
raise RodaError, "sessions plugin :#{name} option must be a String" unless secret.is_a?(String)
|
|
169
189
|
raise RodaError, "invalid sessions plugin :#{name} option length: #{secret.bytesize}, must be >=64" unless secret.bytesize >= 64
|
|
170
|
-
hmac_secret = secret = secret.
|
|
190
|
+
hmac_secret = secret = secret.b
|
|
171
191
|
cipher_secret = secret.slice!(0, 32)
|
|
172
192
|
[cipher_secret.freeze, hmac_secret.freeze]
|
|
173
193
|
end
|
|
174
194
|
|
|
175
|
-
def self.load_dependencies(app, opts=OPTS)
|
|
195
|
+
def self.load_dependencies(app, opts=OPTS, **)
|
|
176
196
|
app.plugin :_base64
|
|
177
197
|
app.plugin :_json
|
|
178
198
|
end
|
|
179
199
|
|
|
180
200
|
# Configure the plugin, see Sessions for details on options.
|
|
181
201
|
def self.configure(app, opts=OPTS)
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
opts
|
|
185
|
-
|
|
186
|
-
|
|
202
|
+
RodaPlugins.check_supported_opts("the sessions plugin", opts, PLUGIN_ARGUMENTS)
|
|
203
|
+
|
|
204
|
+
if opts.has_key?(:per_cookie_cipher_secret)
|
|
205
|
+
RodaPlugins.warn("Roda 4 will default to per cookie cipher secrets, remove the per_cookie_cipher_secret argument", uplevel: 1)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
if opts.has_key?(:upgrade_from_rack_session_cookie_secret) || opts.has_key?(:upgrade_from_rack_session_cookie_key) || opts.has_key?(:upgrade_from_rack_session_cookie_options)
|
|
209
|
+
RodaPlugins.warn("Support for upgrading rack session cookie sessions will be removed in Roda 4, remove the upgrade_from_rack_session_cookie_* arguments.", uplevel: 1)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
if opts.has_key?(:gzip_over)
|
|
213
|
+
RodaPlugins.warn("Support for compressing session data will be removed in Roda 4, remove the gzip_over argument.", uplevel: 1)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
opts = (app.sessions_opts || {:key => 'roda.session'.freeze, :max_seconds=>86400*30, :max_idle_seconds=>86400*7, :pad_size=>32, :skip_within=>3600, :env_key=>'rack.session'}).merge(opts)
|
|
217
|
+
config = Config.new(*opts.values_at(*Config.members))
|
|
218
|
+
co = config.cookie_options = {:httponly=>true, :path=>'/'.freeze, :same_site=>:lax}.merge(config.cookie_options || OPTS).freeze
|
|
219
|
+
config.remove_cookie_options = co.merge(:max_age=>'0', :expires=>Time.at(0))
|
|
220
|
+
config.parser ||= app.json_parser
|
|
221
|
+
config.serializer ||= app.json_serializer
|
|
187
222
|
|
|
188
223
|
opts[:per_cookie_cipher_secret] = true unless opts.has_key?(:per_cookie_cipher_secret)
|
|
189
|
-
opts[:session_version_num] = opts[:per_cookie_cipher_secret] ? 1 : 0
|
|
190
224
|
|
|
191
225
|
if opts[:upgrade_from_rack_session_cookie_secret]
|
|
192
|
-
RodaPlugins.warn("Support for upgrading rack session cookie sessions will be removed in Roda 4.")
|
|
193
226
|
opts[:upgrade_from_rack_session_cookie_key] ||= 'rack.session'
|
|
194
227
|
rsco = opts[:upgrade_from_rack_session_cookie_options] = Hash[opts[:upgrade_from_rack_session_cookie_options] || OPTS]
|
|
195
228
|
rsco[:path] ||= co[:path]
|
|
196
229
|
rsco[:domain] ||= co[:domain]
|
|
197
230
|
end
|
|
198
231
|
|
|
199
|
-
|
|
200
|
-
|
|
232
|
+
secret = opts[:secret]
|
|
233
|
+
config.cipher_secret, config.hmac_secret = split_secret(:secret, secret)
|
|
234
|
+
if old_secret = opts[:old_secret]
|
|
235
|
+
config.old_cipher_secret, config.old_hmac_secret = split_secret(:old_secret, old_secret)
|
|
201
236
|
end
|
|
202
237
|
|
|
203
|
-
|
|
204
|
-
opts[:old_cipher_secret], opts[:old_hmac_secret] = (split_secret(:old_secret, opts[:old_secret]) if opts[:old_secret])
|
|
205
|
-
|
|
206
|
-
case opts[:pad_size]
|
|
238
|
+
case config.pad_size
|
|
207
239
|
when nil
|
|
208
240
|
# no changes
|
|
209
241
|
when Integer
|
|
210
|
-
raise RodaError, "invalid :pad_size: #{
|
|
242
|
+
raise RodaError, "invalid :pad_size: #{config.pad_size}, must be >=2, < 4096" unless config.pad_size >= 2 && config.pad_size < 4096
|
|
211
243
|
else
|
|
212
|
-
raise RodaError, "invalid
|
|
244
|
+
raise RodaError, "invalid pad_size argument: #{config.pad_size.inspect}, must be Integer or nil"
|
|
213
245
|
end
|
|
214
246
|
|
|
215
|
-
app.opts[:sessions] = opts.freeze
|
|
247
|
+
app.opts[:sessions] = config.to_h.merge(opts).freeze
|
|
248
|
+
app.opts[:sessions_config] = config.freeze
|
|
216
249
|
app.opts[:sessions_convert_symbols] = true unless app.opts.has_key?(:sessions_convert_symbols)
|
|
217
250
|
end
|
|
218
251
|
|
|
219
252
|
module ClassMethods
|
|
220
253
|
RodaPlugins.opt_attr_reader(self, :sessions, name: :sessions_opts)
|
|
254
|
+
RodaPlugins.opt_attr_reader(self, :sessions_config)
|
|
221
255
|
end
|
|
222
256
|
|
|
223
257
|
module InstanceMethods
|
|
@@ -226,8 +260,8 @@ class Roda
|
|
|
226
260
|
# instead of the previous creation timestamp.
|
|
227
261
|
def clear_session
|
|
228
262
|
session.clear
|
|
229
|
-
env.delete(
|
|
230
|
-
env.delete(
|
|
263
|
+
env.delete('roda.session.created_at')
|
|
264
|
+
env.delete('roda.session.updated_at')
|
|
231
265
|
nil
|
|
232
266
|
end
|
|
233
267
|
|
|
@@ -237,7 +271,7 @@ class Roda
|
|
|
237
271
|
# update the rack response headers to set the session cookie in
|
|
238
272
|
# the response.
|
|
239
273
|
def _roda_after_50__sessions(res)
|
|
240
|
-
if res && (session = env[self.class.
|
|
274
|
+
if res && (session = env[self.class.sessions_config.env_key])
|
|
241
275
|
@_request.persist_session(res[1], session)
|
|
242
276
|
end
|
|
243
277
|
end
|
|
@@ -251,37 +285,35 @@ class Roda
|
|
|
251
285
|
# this method stores the session in 'rack.session' in the request environment,
|
|
252
286
|
# but that does not happen until this method is called.
|
|
253
287
|
def session
|
|
254
|
-
@env[roda_class.
|
|
288
|
+
@env[roda_class.sessions_config.env_key] ||= _load_session
|
|
255
289
|
end
|
|
256
290
|
|
|
257
291
|
# The time the session was originally created. nil if there is no active session.
|
|
258
292
|
def session_created_at
|
|
259
293
|
session
|
|
260
|
-
Time.at(@env[
|
|
294
|
+
Time.at(@env['roda.session.created_at']) if @env['roda.session.serialized']
|
|
261
295
|
end
|
|
262
296
|
|
|
263
297
|
# The time the session was last updated. nil if there is no active session.
|
|
264
298
|
def session_updated_at
|
|
265
299
|
session
|
|
266
|
-
Time.at(@env[
|
|
300
|
+
Time.at(@env['roda.session.updated_at']) if @env['roda.session.serialized']
|
|
267
301
|
end
|
|
268
302
|
|
|
269
|
-
# Persist the session data as a cookie.
|
|
270
|
-
# Rack::Session::Cookie, mark the related cookie for expiration so it isn't
|
|
271
|
-
# sent in the future.
|
|
303
|
+
# Persist the session data as a cookie.
|
|
272
304
|
def persist_session(headers, session)
|
|
273
|
-
|
|
305
|
+
config = roda_class.sessions_config
|
|
274
306
|
|
|
275
307
|
if session.empty?
|
|
276
|
-
if env[
|
|
308
|
+
if env['roda.session.serialized']
|
|
277
309
|
# If session was submitted and is now empty, remove the cookie
|
|
278
|
-
Rack::Utils.delete_cookie_header!(headers,
|
|
310
|
+
Rack::Utils.delete_cookie_header!(headers, config.key, config.remove_cookie_options)
|
|
279
311
|
# else
|
|
280
312
|
# If no session was submitted, and the session is empty
|
|
281
313
|
# then there is no need to do anything
|
|
282
314
|
end
|
|
283
315
|
elsif cookie_value = _serialize_session(session)
|
|
284
|
-
cookie = Hash[
|
|
316
|
+
cookie = Hash[config.cookie_options]
|
|
285
317
|
cookie[:value] = cookie_value
|
|
286
318
|
cookie[:secure] = true if !cookie.has_key?(:secure) && ssl?
|
|
287
319
|
|
|
@@ -291,7 +323,7 @@ class Roda
|
|
|
291
323
|
0
|
|
292
324
|
end
|
|
293
325
|
|
|
294
|
-
Rack::Utils.set_cookie_header!(headers,
|
|
326
|
+
Rack::Utils.set_cookie_header!(headers, config.key, cookie)
|
|
295
327
|
|
|
296
328
|
cookie_size = case set_cookie_after = headers[RodaResponseHeaders::SET_COOKIE]
|
|
297
329
|
when String
|
|
@@ -309,7 +341,8 @@ class Roda
|
|
|
309
341
|
end
|
|
310
342
|
end
|
|
311
343
|
|
|
312
|
-
if env[
|
|
344
|
+
if env['roda.session.delete_rack_session_cookie']
|
|
345
|
+
opts = roda_class.sessions_opts
|
|
313
346
|
Rack::Utils.delete_cookie_header!(headers, opts[:upgrade_from_rack_session_cookie_key], opts[:upgrade_from_rack_session_cookie_options])
|
|
314
347
|
end
|
|
315
348
|
|
|
@@ -321,12 +354,11 @@ class Roda
|
|
|
321
354
|
# Load the session by looking for the appropriate cookie, or falling
|
|
322
355
|
# back to the rack session cookie if configured.
|
|
323
356
|
def _load_session
|
|
324
|
-
opts = roda_class.sessions_opts
|
|
325
357
|
cs = cookies
|
|
326
358
|
|
|
327
|
-
if data = cs[
|
|
359
|
+
if data = cs[roda_class.sessions_config.key]
|
|
328
360
|
_deserialize_session(data)
|
|
329
|
-
elsif (key =
|
|
361
|
+
elsif (key = roda_class.sessions_opts[:upgrade_from_rack_session_cookie_key]) && (data = cs[key])
|
|
330
362
|
_deserialize_rack_session(data)
|
|
331
363
|
end || {}
|
|
332
364
|
end
|
|
@@ -340,9 +372,6 @@ class Roda
|
|
|
340
372
|
nil
|
|
341
373
|
end
|
|
342
374
|
|
|
343
|
-
# Interpret given cookie data as a Rack::Session::Cookie
|
|
344
|
-
# serialized session using the default Rack::Session::Cookie
|
|
345
|
-
# hmac and coder.
|
|
346
375
|
def _deserialize_rack_session(data)
|
|
347
376
|
opts = roda_class.sessions_opts
|
|
348
377
|
data, digest = data.split("--", 2)
|
|
@@ -359,26 +388,18 @@ class Roda
|
|
|
359
388
|
return _session_serialization_error("Error decoding Rack::Session::Cookie session: not base64 encoded marshal dump")
|
|
360
389
|
end
|
|
361
390
|
|
|
362
|
-
|
|
363
|
-
env[SESSION_DELETE_RACK_COOKIE] = true
|
|
391
|
+
env['roda.session.delete_rack_session_cookie'] = true
|
|
364
392
|
|
|
365
|
-
# Delete the session id before serializing it. Starting in rack 2.0.8,
|
|
366
|
-
# this is an object and not just a string, and calling to_s on it raises
|
|
367
|
-
# a RuntimeError.
|
|
368
393
|
session.delete("session_id")
|
|
369
394
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
# same result as you would if the session was handled
|
|
373
|
-
# by this plugin.
|
|
374
|
-
env[SESSION_SERIALIZED] = data = opts[:serializer].call(session)
|
|
375
|
-
env[SESSION_CREATED_AT] = Time.now.to_i
|
|
395
|
+
env['roda.session.serialized'] = data = opts[:serializer].call(session)
|
|
396
|
+
env['roda.session.created_at'] = Time.now.to_i
|
|
376
397
|
opts[:parser].call(data)
|
|
377
398
|
end
|
|
378
399
|
|
|
379
|
-
# Interpret given cookie data as a
|
|
400
|
+
# Interpret given cookie data as a session
|
|
380
401
|
def _deserialize_session(data)
|
|
381
|
-
|
|
402
|
+
config = roda_class.sessions_config
|
|
382
403
|
|
|
383
404
|
begin
|
|
384
405
|
data = Base64_.urlsafe_decode64(data)
|
|
@@ -402,7 +423,6 @@ class Roda
|
|
|
402
423
|
min_data_length = 93
|
|
403
424
|
when 0
|
|
404
425
|
per_cookie_secret = false
|
|
405
|
-
# minimum length (1+16+12+32) (version+cipher_iv+minimum session+hmac)
|
|
406
426
|
min_data_length = 61
|
|
407
427
|
when nil
|
|
408
428
|
return _session_serialization_error("Unable to decode session: no data")
|
|
@@ -416,9 +436,11 @@ class Roda
|
|
|
416
436
|
end
|
|
417
437
|
|
|
418
438
|
encrypted_data = data.slice!(0, length-32)
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
439
|
+
hmac_data = encrypted_data+config.key
|
|
440
|
+
cipher_secret = config.cipher_secret
|
|
441
|
+
unless Rack::Utils.secure_compare(data, OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new, config.hmac_secret, hmac_data))
|
|
442
|
+
if config.old_hmac_secret && Rack::Utils.secure_compare(data, OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new, config.old_hmac_secret, hmac_data))
|
|
443
|
+
cipher_secret = config.old_cipher_secret
|
|
422
444
|
else
|
|
423
445
|
return _session_serialization_error("Not decoding session: HMAC invalid")
|
|
424
446
|
end
|
|
@@ -427,7 +449,6 @@ class Roda
|
|
|
427
449
|
# Remove version
|
|
428
450
|
encrypted_data.slice!(0)
|
|
429
451
|
|
|
430
|
-
cipher_secret = opts[use_old_cipher_secret ? :old_cipher_secret : :cipher_secret]
|
|
431
452
|
if per_cookie_secret
|
|
432
453
|
cipher_secret = OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new, cipher_secret, encrypted_data.slice!(0, 32))
|
|
433
454
|
end
|
|
@@ -445,71 +466,71 @@ class Roda
|
|
|
445
466
|
data = cipher.update(encrypted_data) << cipher.final
|
|
446
467
|
|
|
447
468
|
bitmap, created_at, updated_at = data.unpack('vVV')
|
|
448
|
-
padding_bytes = bitmap &
|
|
469
|
+
padding_bytes = bitmap & 0x0fff
|
|
449
470
|
now = Time.now.to_i
|
|
450
|
-
if (max =
|
|
471
|
+
if (max = config.max_seconds) && now > created_at + max
|
|
451
472
|
return _session_serialization_error("Not returning session: maximum session time expired")
|
|
452
473
|
end
|
|
453
|
-
if (max =
|
|
474
|
+
if (max = config.max_idle_seconds) && now > updated_at + max
|
|
454
475
|
return _session_serialization_error("Not returning session: maximum session idle time expired")
|
|
455
476
|
end
|
|
456
477
|
|
|
457
478
|
data = data.slice(10+padding_bytes, data.bytesize)
|
|
458
479
|
|
|
459
|
-
if bitmap &
|
|
480
|
+
if bitmap & 0x1000 > 0
|
|
460
481
|
data = Zlib::Inflate.inflate(data)
|
|
461
482
|
end
|
|
462
483
|
|
|
463
484
|
env = @env
|
|
464
|
-
env[
|
|
465
|
-
env[
|
|
466
|
-
env[
|
|
467
|
-
env[
|
|
485
|
+
env['roda.session.created_at'] = created_at
|
|
486
|
+
env['roda.session.updated_at'] = updated_at
|
|
487
|
+
env['roda.session.serialized'] = data
|
|
488
|
+
env['roda.session.version'] = version
|
|
468
489
|
|
|
469
|
-
|
|
490
|
+
config.parser.call(data)
|
|
470
491
|
end
|
|
471
492
|
|
|
472
493
|
def _serialize_session(session)
|
|
473
|
-
|
|
494
|
+
config = roda_class.sessions_config
|
|
474
495
|
env = @env
|
|
475
496
|
now = Time.now.to_i
|
|
476
|
-
json_data =
|
|
497
|
+
json_data = config.serializer.call(session).force_encoding(Encoding::ASCII_8BIT)
|
|
477
498
|
|
|
478
|
-
if (serialized_session = env[
|
|
479
|
-
(
|
|
480
|
-
(updated_at = env[
|
|
481
|
-
(now - updated_at <
|
|
499
|
+
if (serialized_session = env['roda.session.serialized']) &&
|
|
500
|
+
((roda_class.sessions_opts[:per_cookie_cipher_secret] ? 1 : 0) == env['roda.session.version']) &&
|
|
501
|
+
(updated_at = env['roda.session.updated_at']) &&
|
|
502
|
+
(now - updated_at < config.skip_within) &&
|
|
482
503
|
(serialized_session == json_data)
|
|
483
504
|
return
|
|
484
505
|
end
|
|
485
506
|
|
|
486
507
|
bitmap = 0
|
|
487
508
|
json_length = json_data.bytesize
|
|
488
|
-
gzip_over =
|
|
509
|
+
gzip_over = roda_class.sessions_opts[:gzip_over]
|
|
489
510
|
|
|
490
511
|
if gzip_over && json_length > gzip_over
|
|
491
512
|
json_data = Zlib.deflate(json_data)
|
|
492
513
|
json_length = json_data.bytesize
|
|
493
|
-
bitmap |=
|
|
514
|
+
bitmap |= 0x1000
|
|
494
515
|
end
|
|
495
516
|
|
|
496
517
|
# When calculating padding bytes to use, include 10 bytes for bitmap and
|
|
497
518
|
# session create/update times, so total size of encrypted data is a
|
|
498
519
|
# multiple of pad_size.
|
|
499
|
-
if (pad_size =
|
|
520
|
+
if (pad_size = config.pad_size) && (padding_bytes = (json_length+10) % pad_size) != 0
|
|
500
521
|
padding_bytes = pad_size - padding_bytes
|
|
501
522
|
bitmap |= padding_bytes
|
|
502
523
|
padding_data = SecureRandom.random_bytes(padding_bytes)
|
|
503
524
|
end
|
|
504
525
|
|
|
505
|
-
session_create_time = env[
|
|
526
|
+
session_create_time = env['roda.session.created_at']
|
|
506
527
|
serialized_data = [bitmap, session_create_time||now, now].pack('vVV')
|
|
507
528
|
|
|
508
529
|
serialized_data << padding_data if padding_data
|
|
509
530
|
serialized_data << json_data
|
|
510
531
|
|
|
511
|
-
cipher_secret =
|
|
512
|
-
if
|
|
532
|
+
cipher_secret = config.cipher_secret
|
|
533
|
+
if roda_class.sessions_opts[:per_cookie_cipher_secret]
|
|
513
534
|
version = "\1"
|
|
514
535
|
per_cookie_secret_base = SecureRandom.random_bytes(32)
|
|
515
536
|
cipher_secret = OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new, cipher_secret, per_cookie_secret_base)
|
|
@@ -528,7 +549,7 @@ class Roda
|
|
|
528
549
|
data << per_cookie_secret_base if per_cookie_secret_base
|
|
529
550
|
data << cipher_iv
|
|
530
551
|
data << encrypted_data
|
|
531
|
-
data << OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new,
|
|
552
|
+
data << OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new, config.hmac_secret, data+config.key)
|
|
532
553
|
|
|
533
554
|
data = Base64_.urlsafe_encode64(data)
|
|
534
555
|
|
|
@@ -19,7 +19,7 @@ class Roda
|
|
|
19
19
|
# Then the route will only if the path is +/foobar123+, but not if it is
|
|
20
20
|
# +/foo+, +/FooBar123+, or +/foobar_123+.
|
|
21
21
|
#
|
|
22
|
-
# You can provide a <tt>segment: true</tt>
|
|
22
|
+
# You can provide a <tt>segment: true</tt> argument to symbol_matcher to speed up
|
|
23
23
|
# the matching on Ruby 2.4+:
|
|
24
24
|
#
|
|
25
25
|
# symbol_matcher :username, /([a-z0-9]{6,20})/, segment: true
|
|
@@ -131,10 +131,13 @@ class Roda
|
|
|
131
131
|
# If providing a registered symbol or class, the block will be called with the
|
|
132
132
|
# captures returned by the block for the registered symbol or class, or the regexp
|
|
133
133
|
# captures if no block was registered with the symbol or class. In either case,
|
|
134
|
-
# if a block is given, it should return an
|
|
135
|
-
# the match block.
|
|
136
|
-
|
|
137
|
-
|
|
134
|
+
# if a block is given, it should return an object to yield to the match block,
|
|
135
|
+
# or an array to yield multiple arguments to the match block. If the block returns
|
|
136
|
+
# nil or false, it will be treated as not matching.
|
|
137
|
+
def symbol_matcher(s, matcher, opts=(no_positional_opts = true; OPTS), segment: opts[:segment], **kw, &block)
|
|
138
|
+
RodaPlugins.check_keywords(__callee__, no_positional_opts, opts, kw)
|
|
139
|
+
|
|
140
|
+
_symbol_class_matcher(Symbol, s, matcher, block, segment: segment) do |meth, array|
|
|
138
141
|
define_method(meth){array}
|
|
139
142
|
end
|
|
140
143
|
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen-string-literal: true
|
|
2
2
|
|
|
3
|
-
require 'rack/utils'
|
|
4
|
-
|
|
5
3
|
#
|
|
6
4
|
class Roda
|
|
7
5
|
module RodaPlugins
|
|
@@ -24,14 +22,34 @@ class Roda
|
|
|
24
22
|
# of the remaining path.
|
|
25
23
|
def matched_path
|
|
26
24
|
e = @env
|
|
27
|
-
|
|
25
|
+
_unescape_path(e["SCRIPT_NAME"] + e["PATH_INFO"]).chomp(@remaining_path)
|
|
28
26
|
end
|
|
29
27
|
|
|
30
28
|
private
|
|
31
29
|
|
|
32
30
|
# Unescape the path.
|
|
33
31
|
def _remaining_path(env)
|
|
34
|
-
|
|
32
|
+
_unescape_path(super)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
if RUBY_VERSION >= "4.0"
|
|
36
|
+
require "cgi/escape"
|
|
37
|
+
|
|
38
|
+
# Use CGI.unescapeURIComponent, which unescapes percent encoding
|
|
39
|
+
# but not +.
|
|
40
|
+
def _unescape_path(path)
|
|
41
|
+
CGI.unescapeURIComponent(path)
|
|
42
|
+
end
|
|
43
|
+
# simplecov:disable
|
|
44
|
+
else
|
|
45
|
+
require 'rack/utils'
|
|
46
|
+
|
|
47
|
+
# Rack::Utils.unescape decodes + as a space, which is only
|
|
48
|
+
# correct for form data, so escape + first to keep it.
|
|
49
|
+
def _unescape_path(path)
|
|
50
|
+
Rack::Utils.unescape(path.gsub('+', '%2B'))
|
|
51
|
+
end
|
|
52
|
+
# simplecov:enable
|
|
35
53
|
end
|
|
36
54
|
end
|
|
37
55
|
end
|
data/lib/roda/plugins.rb
CHANGED
|
@@ -42,6 +42,30 @@ class Roda
|
|
|
42
42
|
nil
|
|
43
43
|
end
|
|
44
44
|
|
|
45
|
+
# Warn if the opts given contain unsupported keys.
|
|
46
|
+
def self.check_supported_opts(desc, opts, supported_keys)
|
|
47
|
+
extra_keys = opts.keys - supported_keys
|
|
48
|
+
|
|
49
|
+
unless extra_keys.empty?
|
|
50
|
+
warn("Unsupported options given to #{desc}: #{extra_keys.map(&:inspect).join(", ")}", uplevel: 2)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
nil
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Handle positional argument to keyword argument conversion warnings in plugins.
|
|
57
|
+
def self.check_keywords(method, no_positional_opts, opts, kw)
|
|
58
|
+
unless no_positional_opts
|
|
59
|
+
warn("A positional options hash is now deprecated for #{method}. Support for this will be removed in Roda 4. Switch to passing keywords arguments. Keys given in positional options hash: #{opts.keys.map(&:inspect).join(", ")}", uplevel: 2)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
unless kw.empty?
|
|
63
|
+
warn("Unrecognized keywords given to #{method}: #{kw.keys.join(", ")}. Backwards compatibility for this will be removed in Roda 4.", uplevel: 2)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
nil
|
|
67
|
+
end
|
|
68
|
+
|
|
45
69
|
# Register the given plugin with Roda, so that it can be loaded using #plugin
|
|
46
70
|
# with a symbol. Should be used by plugin files. Example:
|
|
47
71
|
#
|