roda 3.107.0 → 3.109.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/opts.rb +22 -0
- data/lib/roda/plugins/Integer_matcher_max.rb +1 -1
- data/lib/roda/plugins/_add_script_name.rb +14 -0
- data/lib/roda/plugins/_base64.rb +2 -2
- data/lib/roda/plugins/_json.rb +22 -0
- data/lib/roda/plugins/_optimized_matching.rb +1 -0
- data/lib/roda/plugins/_symbol_class_matchers.rb +6 -6
- data/lib/roda/plugins/additional_render_engines.rb +5 -1
- data/lib/roda/plugins/additional_view_directories.rb +5 -1
- data/lib/roda/plugins/all_verbs.rb +2 -2
- data/lib/roda/plugins/assets.rb +11 -14
- data/lib/roda/plugins/autoload_hash_branches.rb +9 -3
- data/lib/roda/plugins/autoload_named_routes.rb +9 -3
- data/lib/roda/plugins/bearer_token.rb +2 -2
- data/lib/roda/plugins/break.rb +9 -1
- data/lib/roda/plugins/caching.rb +12 -12
- data/lib/roda/plugins/capture_erb.rb +5 -1
- data/lib/roda/plugins/chunked.rb +11 -5
- data/lib/roda/plugins/class_level_routing.rb +5 -3
- data/lib/roda/plugins/class_matchers.rb +3 -1
- data/lib/roda/plugins/common_logger.rb +11 -7
- data/lib/roda/plugins/conditional_sessions.rb +1 -1
- data/lib/roda/plugins/content_for.rb +5 -1
- data/lib/roda/plugins/content_security_policy.rb +6 -2
- data/lib/roda/plugins/cookie_flags.rb +8 -4
- data/lib/roda/plugins/cookies.rb +7 -3
- data/lib/roda/plugins/custom_block_results.rb +5 -3
- data/lib/roda/plugins/custom_matchers.rb +4 -2
- data/lib/roda/plugins/default_headers.rb +2 -5
- data/lib/roda/plugins/default_status.rb +1 -0
- data/lib/roda/plugins/each_part.rb +23 -27
- data/lib/roda/plugins/environments.rb +3 -7
- data/lib/roda/plugins/error_email.rb +11 -7
- data/lib/roda/plugins/error_handler.rb +5 -3
- data/lib/roda/plugins/error_mail.rb +6 -2
- data/lib/roda/plugins/exception_page.rb +2 -2
- data/lib/roda/plugins/h.rb +4 -4
- data/lib/roda/plugins/hash_branch_view_subdir.rb +6 -4
- data/lib/roda/plugins/hash_branches.rb +15 -9
- data/lib/roda/plugins/hash_paths.rb +11 -7
- data/lib/roda/plugins/hash_public.rb +13 -9
- data/lib/roda/plugins/hash_public_cache.rb +9 -7
- data/lib/roda/plugins/hash_routes.rb +5 -3
- data/lib/roda/plugins/heartbeat.rb +6 -2
- data/lib/roda/plugins/hmac_paths.rb +36 -32
- data/lib/roda/plugins/hooks.rb +13 -10
- data/lib/roda/plugins/host_authorization.rb +7 -2
- data/lib/roda/plugins/host_routing.rb +36 -6
- data/lib/roda/plugins/invalid_request_body.rb +22 -4
- data/lib/roda/plugins/ip_from_header.rb +5 -1
- data/lib/roda/plugins/json.rb +10 -8
- data/lib/roda/plugins/json_parser.rb +43 -11
- data/lib/roda/plugins/mail_processor.rb +11 -8
- data/lib/roda/plugins/mailer.rb +9 -7
- data/lib/roda/plugins/match_affix.rb +6 -1
- data/lib/roda/plugins/match_hook_args.rb +7 -5
- data/lib/roda/plugins/middleware.rb +13 -7
- data/lib/roda/plugins/module_include.rb +1 -1
- data/lib/roda/plugins/multi_public.rb +6 -2
- data/lib/roda/plugins/multi_route.rb +1 -1
- data/lib/roda/plugins/multi_run.rb +13 -17
- data/lib/roda/plugins/named_routes.rb +8 -6
- data/lib/roda/plugins/named_templates.rb +5 -3
- data/lib/roda/plugins/optimized_segment_matchers.rb +13 -6
- data/lib/roda/plugins/optimized_string_matchers.rb +6 -3
- data/lib/roda/plugins/params_capturing.rb +16 -7
- data/lib/roda/plugins/params_capturing_restore.rb +72 -0
- data/lib/roda/plugins/part.rb +4 -4
- data/lib/roda/plugins/pass.rb +4 -1
- data/lib/roda/plugins/path.rb +22 -20
- data/lib/roda/plugins/path_matchers.rb +5 -1
- data/lib/roda/plugins/path_rewriter.rb +8 -5
- data/lib/roda/plugins/permissions_policy.rb +7 -3
- data/lib/roda/plugins/public.rb +15 -14
- data/lib/roda/plugins/recheck_precompiled_assets.rb +1 -1
- data/lib/roda/plugins/redirect_http_to_https.rb +6 -3
- data/lib/roda/plugins/render.rb +58 -61
- data/lib/roda/plugins/render_coverage.rb +12 -8
- data/lib/roda/plugins/render_each.rb +2 -2
- data/lib/roda/plugins/render_locals.rb +12 -6
- data/lib/roda/plugins/request_aref.rb +1 -1
- data/lib/roda/plugins/response_content_type.rb +7 -3
- data/lib/roda/plugins/route_csrf.rb +42 -5
- data/lib/roda/plugins/run_append_slash.rb +12 -4
- data/lib/roda/plugins/sec_fetch_site_csrf.rb +11 -4
- data/lib/roda/plugins/sessions.rb +17 -13
- data/lib/roda/plugins/shape_friendly.rb +7 -5
- data/lib/roda/plugins/sinatra_helpers.rb +7 -3
- data/lib/roda/plugins/static.rb +3 -0
- data/lib/roda/plugins/status_handler.rb +5 -3
- data/lib/roda/plugins/strip_path_prefix.rb +4 -2
- data/lib/roda/plugins/symbol_matchers.rb +3 -1
- data/lib/roda/plugins/symbol_views.rb +1 -1
- data/lib/roda/plugins/timestamp_public.rb +8 -4
- data/lib/roda/plugins/type_routing.rb +10 -6
- data/lib/roda/plugins/typecast_params.rb +2 -2
- data/lib/roda/plugins/typecast_params_sized_integers.rb +1 -1
- data/lib/roda/plugins/url_escape.rb +2 -14
- data/lib/roda/plugins/view_options.rb +3 -5
- data/lib/roda/plugins.rb +14 -4
- data/lib/roda/request.rb +25 -9
- data/lib/roda/version.rb +1 -1
- data/lib/roda.rb +32 -21
- metadata +7 -3
|
@@ -32,14 +32,20 @@ class Roda
|
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
def self.configure(app, opts=OPTS)
|
|
35
|
-
app.opts[:render_locals] = (app.
|
|
36
|
-
app.opts[:layout_locals] = (app.
|
|
35
|
+
app.opts[:render_locals] = (app.render_locals || {}).merge(opts[:render]||{}).freeze
|
|
36
|
+
app.opts[:layout_locals] = (app.layout_locals || {}).merge(opts[:layout]||{}).freeze
|
|
37
37
|
if opts.has_key?(:merge)
|
|
38
38
|
app.opts[:merge_locals] = opts[:merge]
|
|
39
|
-
app.opts[:layout_locals] = app.
|
|
39
|
+
app.opts[:layout_locals] = app.render_locals.merge(app.layout_locals).freeze
|
|
40
40
|
end
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
+
module ClassMethods
|
|
44
|
+
RodaPlugins.opt_attr_reader(self, :render_locals)
|
|
45
|
+
RodaPlugins.opt_attr_reader(self, :layout_locals)
|
|
46
|
+
RodaPlugins.opt_attr_reader(self, :merge_locals, name: :merge_locals?)
|
|
47
|
+
end
|
|
48
|
+
|
|
43
49
|
module InstanceMethods
|
|
44
50
|
private
|
|
45
51
|
|
|
@@ -56,11 +62,11 @@ class Roda
|
|
|
56
62
|
end
|
|
57
63
|
|
|
58
64
|
def render_locals
|
|
59
|
-
|
|
65
|
+
self.class.render_locals
|
|
60
66
|
end
|
|
61
67
|
|
|
62
68
|
def layout_locals
|
|
63
|
-
|
|
69
|
+
self.class.layout_locals
|
|
64
70
|
end
|
|
65
71
|
|
|
66
72
|
# If this isn't the layout template, then use the plugin's render locals as the default locals.
|
|
@@ -79,7 +85,7 @@ class Roda
|
|
|
79
85
|
# If using a layout, then use the plugin's layout locals as the default locals.
|
|
80
86
|
def view_layout_opts(opts)
|
|
81
87
|
if layout_opts = super
|
|
82
|
-
merge_locals = layout_opts.
|
|
88
|
+
merge_locals = layout_opts.fetch(:merge_locals){self.class.merge_locals?}
|
|
83
89
|
|
|
84
90
|
locals = {}
|
|
85
91
|
locals.merge!(layout_locals)
|
|
@@ -20,7 +20,7 @@ class Roda
|
|
|
20
20
|
def self.configure(app, setting)
|
|
21
21
|
case setting
|
|
22
22
|
when :allow, :raise, :warn
|
|
23
|
-
app::RodaRequest.
|
|
23
|
+
app::RodaRequest.class_exec do
|
|
24
24
|
alias_method(:[], :"request_aref_#{setting}")
|
|
25
25
|
alias_method(:[]=, :"request_aset_#{setting}")
|
|
26
26
|
public :[], :[]=
|
|
@@ -50,12 +50,16 @@ class Roda
|
|
|
50
50
|
else
|
|
51
51
|
mime_types.dup
|
|
52
52
|
end
|
|
53
|
-
app.opts[:
|
|
53
|
+
app.opts[:response_content_types] = mime_types.freeze
|
|
54
54
|
else
|
|
55
|
-
app.opts[:
|
|
55
|
+
app.opts[:response_content_types] ||= {}
|
|
56
56
|
end
|
|
57
57
|
end
|
|
58
58
|
|
|
59
|
+
module ClassMethods
|
|
60
|
+
RodaPlugins.opt_attr_reader(self, :response_content_types)
|
|
61
|
+
end
|
|
62
|
+
|
|
59
63
|
module ResponseMethods
|
|
60
64
|
# Return the content-type of the response. Will be nil if it has
|
|
61
65
|
# not yet been explicitly set.
|
|
@@ -68,7 +72,7 @@ class Roda
|
|
|
68
72
|
# type with the given file extension. If the symbol is not
|
|
69
73
|
# a recognized mime type, raises KeyError.
|
|
70
74
|
def content_type=(mime_type)
|
|
71
|
-
mime_type = roda_class.
|
|
75
|
+
mime_type = roda_class.response_content_types.fetch(mime_type) if mime_type.is_a?(Symbol)
|
|
72
76
|
@headers[RodaResponseHeaders::CONTENT_TYPE] = mime_type
|
|
73
77
|
end
|
|
74
78
|
end
|
|
@@ -57,13 +57,18 @@ class Roda
|
|
|
57
57
|
# :csrf_failure :: The action to taken if a request fails the CSRF check (default: :raise). Options:
|
|
58
58
|
# :raise :: raise a Roda::RodaPlugins::RouteCsrf::InvalidToken exception
|
|
59
59
|
# :empty_403 :: return a blank 403 page (rack_csrf's default behavior)
|
|
60
|
-
# :clear_session :: Clear the current session
|
|
60
|
+
# :clear_session :: Clear the current session (deprecated)
|
|
61
61
|
# Proc :: Treated as a routing block, called with request object
|
|
62
62
|
# :check_header :: Whether the HTTP header should be checked for the token value (default: false).
|
|
63
63
|
# If true, checks the HTTP header after checking for the form input parameter.
|
|
64
64
|
# If :only, only checks the HTTP header and doesn't check the form input parameter.
|
|
65
65
|
# :check_request_methods :: Which request methods require CSRF protection
|
|
66
66
|
# (default: <tt>['POST', 'DELETE', 'PATCH', 'PUT']</tt>)
|
|
67
|
+
# :exempt_request_methods :: If given, specifies which request methods do not require CSRF protection.
|
|
68
|
+
# It is recommended to set this so an allow list approach is used, as
|
|
69
|
+
# :check_request_methods is the equivalent of a deny list
|
|
70
|
+
# (recommended: <tt>['GET', 'HEAD', 'OPTIONS', 'QUERY']</tt>, which
|
|
71
|
+
# will be the default in Roda 4)
|
|
67
72
|
# :upgrade_from_rack_csrf_key :: If provided, the session key that should be checked for the
|
|
68
73
|
# rack_csrf raw token. If the session key is present, the value
|
|
69
74
|
# will be checked against the submitted token, and if it matches,
|
|
@@ -155,6 +160,19 @@ class Roda
|
|
|
155
160
|
#
|
|
156
161
|
# By including random data in the HMAC for all tokens, different tokens are generated
|
|
157
162
|
# each time, mitigating compression ratio attacks such as BREACH.
|
|
163
|
+
#
|
|
164
|
+
# == REQUEST_METHOD modifications
|
|
165
|
+
#
|
|
166
|
+
# It's possible to use Rack middleware that modify REQUEST_METHOD in the env hash,
|
|
167
|
+
# such that it does not reflect that actual request method. One example of this is
|
|
168
|
+
# Rack::MethodOverride. Using such middleware is generally bad from a security perspective,
|
|
169
|
+
# but if you want to do so anyway, you need to override the request method that the
|
|
170
|
+
# route_csrf plugin will use so it reflects the actual request method. If you are using
|
|
171
|
+
# Rack::MethodOverride, you can use:
|
|
172
|
+
#
|
|
173
|
+
# def csrf_request_method
|
|
174
|
+
# env['rack.methodoverride.original_method'] || super
|
|
175
|
+
# end
|
|
158
176
|
module RouteCsrf
|
|
159
177
|
# Default CSRF option values
|
|
160
178
|
DEFAULTS = {
|
|
@@ -165,6 +183,7 @@ class Roda
|
|
|
165
183
|
:require_request_specific_tokens => true,
|
|
166
184
|
:csrf_failure => :raise,
|
|
167
185
|
:check_header => false,
|
|
186
|
+
# RODA4: Remove and set :exempt_request_methods by default
|
|
168
187
|
:check_request_methods => %w'POST DELETE PATCH PUT'.freeze.each(&:freeze)
|
|
169
188
|
}.freeze
|
|
170
189
|
|
|
@@ -177,7 +196,7 @@ class Roda
|
|
|
177
196
|
end
|
|
178
197
|
|
|
179
198
|
def self.configure(app, opts=OPTS, &block)
|
|
180
|
-
options = app.opts[:route_csrf] = (app.
|
|
199
|
+
options = app.opts[:route_csrf] = (app.route_csrf_opts || DEFAULTS).merge(opts)
|
|
181
200
|
if block || opts[:csrf_failure].is_a?(Proc)
|
|
182
201
|
if block && opts[:csrf_failure]
|
|
183
202
|
raise RodaError, "Cannot specify both route_csrf plugin block and :csrf_failure option"
|
|
@@ -186,10 +205,17 @@ class Roda
|
|
|
186
205
|
options[:csrf_failure] = :csrf_failure_method
|
|
187
206
|
app.define_roda_method(:_roda_route_csrf_failure, 1, &app.send(:convert_route_block, block))
|
|
188
207
|
end
|
|
208
|
+
if opts[:csrf_failure] == :clear_session
|
|
209
|
+
RodaPlugins.warn "Passing :clear_session as the :csrf_failure option to the route_csrf plugin is deprecated"
|
|
210
|
+
end
|
|
189
211
|
options[:env_header] = "HTTP_#{options[:header].to_s.tr('-', '_').upcase}".freeze
|
|
190
212
|
options.freeze
|
|
191
213
|
end
|
|
192
214
|
|
|
215
|
+
module ClassMethods
|
|
216
|
+
RodaPlugins.opt_attr_reader(self, :route_csrf, name: :route_csrf_opts)
|
|
217
|
+
end
|
|
218
|
+
|
|
193
219
|
module InstanceMethods
|
|
194
220
|
# Check that the submitted CSRF token is valid, if the request requires a CSRF token.
|
|
195
221
|
# If the CSRF token is valid or the request does not require a CSRF token, return nil.
|
|
@@ -212,6 +238,7 @@ class Roda
|
|
|
212
238
|
headers[RodaResponseHeaders::CONTENT_LENGTH] ='0'
|
|
213
239
|
throw :halt, @_response.finish_with_body([])
|
|
214
240
|
when :clear_session
|
|
241
|
+
RodaPlugins.warn "Passing :clear_session as the :csrf_failure option to check_csrf! is deprecated"
|
|
215
242
|
session.clear
|
|
216
243
|
when :csrf_failure_method
|
|
217
244
|
@_request.on{_roda_route_csrf_failure(@_request)}
|
|
@@ -297,13 +324,22 @@ class Roda
|
|
|
297
324
|
|
|
298
325
|
private
|
|
299
326
|
|
|
327
|
+
# The request method to use to determine whether to perform a CSRF check.
|
|
328
|
+
# defaults to REQUEST_METHOD in the environment, can be overridden if
|
|
329
|
+
# that isn't the actual request method.
|
|
330
|
+
def csrf_request_method
|
|
331
|
+
@_request.env['REQUEST_METHOD']
|
|
332
|
+
end
|
|
333
|
+
|
|
300
334
|
# Returns error message string if the CSRF token is not valid.
|
|
301
335
|
# Returns nil if the CSRF token is valid.
|
|
302
336
|
def csrf_invalid_message(opts)
|
|
303
337
|
opts = opts.empty? ? csrf_options : csrf_options.merge(opts)
|
|
304
|
-
method =
|
|
338
|
+
method = csrf_request_method
|
|
305
339
|
|
|
306
|
-
|
|
340
|
+
if exempt = opts[:exempt_request_methods]
|
|
341
|
+
return if exempt.include?(method)
|
|
342
|
+
elsif !opts[:check_request_methods].include?(method)
|
|
307
343
|
return
|
|
308
344
|
end
|
|
309
345
|
|
|
@@ -326,6 +362,7 @@ class Roda
|
|
|
326
362
|
end
|
|
327
363
|
|
|
328
364
|
if (rack_csrf_key = opts[:upgrade_from_rack_csrf_key]) && (rack_csrf_value = session[rack_csrf_key]) && csrf_compare(rack_csrf_value, encoded_token)
|
|
365
|
+
# RODA4: Remove :upgrade_from_rack_csrf_key support
|
|
329
366
|
return
|
|
330
367
|
end
|
|
331
368
|
|
|
@@ -360,7 +397,7 @@ class Roda
|
|
|
360
397
|
|
|
361
398
|
# Helper for getting the plugin options.
|
|
362
399
|
def csrf_options
|
|
363
|
-
|
|
400
|
+
self.class.route_csrf_opts
|
|
364
401
|
end
|
|
365
402
|
|
|
366
403
|
# Perform a constant-time comparison of the two strings, returning true if they match and false otherwise.
|
|
@@ -22,20 +22,28 @@ class Roda
|
|
|
22
22
|
# # GET /a/ => App gets "/" as PATH_INFO
|
|
23
23
|
module RunAppendSlash
|
|
24
24
|
# Set plugin specific options. Options:
|
|
25
|
-
# :use_redirects :: Whether to issue
|
|
26
|
-
#
|
|
25
|
+
# :use_redirects :: Whether to issue a redirect to the path with the slash
|
|
26
|
+
# appended, instead of calling the app directly with the
|
|
27
|
+
# trailing slash. Note that if the current path starts
|
|
28
|
+
# with +//+, it will call the app directly instead of
|
|
29
|
+
# redirecting.
|
|
27
30
|
def self.configure(app, opts=OPTS)
|
|
28
31
|
app.opts[:run_append_slash_redirect] = !!opts[:use_redirects]
|
|
29
32
|
end
|
|
30
33
|
|
|
34
|
+
module ClassMethods
|
|
35
|
+
RodaPlugins.opt_attr_reader(self, :run_append_slash_redirect, name: :run_append_slash_redirect?)
|
|
36
|
+
end
|
|
37
|
+
|
|
31
38
|
module RequestMethods
|
|
32
39
|
# Calls the given rack app. If the path matches the root of the app but
|
|
33
40
|
# does not contain a trailing slash, a trailing slash is appended to the
|
|
34
41
|
# path internally, or a redirect is issued when configured with
|
|
35
|
-
# <tt>use_redirects: true</tt>.
|
|
42
|
+
# <tt>use_redirects: true</tt>. If the path starts with +//+,
|
|
43
|
+
# the redirect approach will not be used.
|
|
36
44
|
def run(*)
|
|
37
45
|
if @remaining_path.empty?
|
|
38
|
-
if
|
|
46
|
+
if roda_class.run_append_slash_redirect? && !(path = self.path).start_with?('//')
|
|
39
47
|
redirect("#{path}/")
|
|
40
48
|
else
|
|
41
49
|
@remaining_path += '/'
|
|
@@ -54,7 +54,7 @@ class Roda
|
|
|
54
54
|
# :raise :: raise a Roda::RodaPlugins::SecFetchSiteCsrf::CsrfFailure
|
|
55
55
|
# exception
|
|
56
56
|
# :empty_403 :: return a blank 403 page
|
|
57
|
-
# :clear_session :: clear the current session
|
|
57
|
+
# :clear_session :: clear the current session (deprecated)
|
|
58
58
|
#
|
|
59
59
|
# The plugin also supports a block, in which case failures will call the block
|
|
60
60
|
# as a routing block (the block should accept the request object).
|
|
@@ -69,7 +69,7 @@ class Roda
|
|
|
69
69
|
class CsrfFailure < RodaError; end
|
|
70
70
|
|
|
71
71
|
def self.configure(app, opts=OPTS, &block)
|
|
72
|
-
options = app.opts[:sec_fetch_site_csrf] = (app.
|
|
72
|
+
options = app.opts[:sec_fetch_site_csrf] = (app.sec_fetch_site_csrf_opts || DEFAULTS).merge(opts)
|
|
73
73
|
|
|
74
74
|
allowed_values = options[:allowed_values] = ["same-origin"]
|
|
75
75
|
allowed_values << "same-site" if opts[:allow_same_site]
|
|
@@ -83,8 +83,10 @@ class Roda
|
|
|
83
83
|
end
|
|
84
84
|
|
|
85
85
|
case options[:csrf_failure]
|
|
86
|
-
when :raise, :empty_403, :
|
|
86
|
+
when :raise, :empty_403, :method
|
|
87
87
|
# nothing
|
|
88
|
+
when :clear_session
|
|
89
|
+
RodaPlugins.warn "Passing :clear_session as the :csrf_failure option to the sec_fetch_site_csrf plugin is deprecated"
|
|
88
90
|
else
|
|
89
91
|
raise RodaError, "Unsupported :csrf_failure plugin option: #{options[:csrf_failure].inspect}"
|
|
90
92
|
end
|
|
@@ -92,6 +94,10 @@ class Roda
|
|
|
92
94
|
options.freeze
|
|
93
95
|
end
|
|
94
96
|
|
|
97
|
+
module ClassMethods
|
|
98
|
+
RodaPlugins.opt_attr_reader(self, :sec_fetch_site_csrf, name: :sec_fetch_site_csrf_opts)
|
|
99
|
+
end
|
|
100
|
+
|
|
95
101
|
module InstanceMethods
|
|
96
102
|
# Check that the Sec-Fetch-Site header is valid, if the request requires it.
|
|
97
103
|
# If the header is valid or the request does not require the header, return nil.
|
|
@@ -99,7 +105,7 @@ class Roda
|
|
|
99
105
|
# if a block is not given, use the plugin :csrf_failure option to determine how to
|
|
100
106
|
# handle it.
|
|
101
107
|
def check_sec_fetch_site!(&block)
|
|
102
|
-
plugin_opts = self.class.
|
|
108
|
+
plugin_opts = self.class.sec_fetch_site_csrf_opts
|
|
103
109
|
return unless plugin_opts[:check_request_methods].include?(request.request_method)
|
|
104
110
|
|
|
105
111
|
sec_fetch_site = env["HTTP_SEC_FETCH_SITE"]
|
|
@@ -118,6 +124,7 @@ class Roda
|
|
|
118
124
|
headers[RodaResponseHeaders::CONTENT_LENGTH] ='0'
|
|
119
125
|
throw :halt, @_response.finish_with_body([])
|
|
120
126
|
when :clear_session
|
|
127
|
+
# RODA4: Remove
|
|
121
128
|
session.clear
|
|
122
129
|
else # when :method
|
|
123
130
|
@_request.on{_roda_sec_fetch_site_csrf_failure(@_request)}
|
|
@@ -5,12 +5,11 @@ require 'openssl'
|
|
|
5
5
|
begin
|
|
6
6
|
OpenSSL::Cipher.new("aes-256-ctr")
|
|
7
7
|
rescue OpenSSL::Cipher::CipherError
|
|
8
|
-
# :
|
|
8
|
+
# simplecov:disable
|
|
9
9
|
raise LoadError, "Roda sessions plugin requires the aes-256-ctr cipher"
|
|
10
|
-
# :
|
|
10
|
+
# simplecov:enable
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
-
require 'json'
|
|
14
13
|
require 'securerandom'
|
|
15
14
|
require 'zlib'
|
|
16
15
|
require 'rack/utils'
|
|
@@ -175,15 +174,16 @@ class Roda
|
|
|
175
174
|
|
|
176
175
|
def self.load_dependencies(app, opts=OPTS)
|
|
177
176
|
app.plugin :_base64
|
|
177
|
+
app.plugin :_json
|
|
178
178
|
end
|
|
179
179
|
|
|
180
180
|
# Configure the plugin, see Sessions for details on options.
|
|
181
181
|
def self.configure(app, opts=OPTS)
|
|
182
|
-
opts = (app.
|
|
182
|
+
opts = (app.sessions_opts || DEFAULT_OPTIONS).merge(opts)
|
|
183
183
|
co = opts[:cookie_options] = DEFAULT_COOKIE_OPTIONS.merge(opts[:cookie_options] || OPTS).freeze
|
|
184
184
|
opts[:remove_cookie_options] = co.merge(:max_age=>'0', :expires=>Time.at(0))
|
|
185
|
-
opts[:parser] ||= app.
|
|
186
|
-
opts[:serializer] ||= app.
|
|
185
|
+
opts[:parser] ||= app.json_parser
|
|
186
|
+
opts[:serializer] ||= app.json_serializer
|
|
187
187
|
|
|
188
188
|
opts[:per_cookie_cipher_secret] = true unless opts.has_key?(:per_cookie_cipher_secret)
|
|
189
189
|
opts[:session_version_num] = opts[:per_cookie_cipher_secret] ? 1 : 0
|
|
@@ -216,6 +216,10 @@ class Roda
|
|
|
216
216
|
app.opts[:sessions_convert_symbols] = true unless app.opts.has_key?(:sessions_convert_symbols)
|
|
217
217
|
end
|
|
218
218
|
|
|
219
|
+
module ClassMethods
|
|
220
|
+
RodaPlugins.opt_attr_reader(self, :sessions, name: :sessions_opts)
|
|
221
|
+
end
|
|
222
|
+
|
|
219
223
|
module InstanceMethods
|
|
220
224
|
# Clear data from the session, and update the request environment
|
|
221
225
|
# so that the session cookie will use a new creation timestamp
|
|
@@ -233,7 +237,7 @@ class Roda
|
|
|
233
237
|
# update the rack response headers to set the session cookie in
|
|
234
238
|
# the response.
|
|
235
239
|
def _roda_after_50__sessions(res)
|
|
236
|
-
if res && (session = env[self.class.
|
|
240
|
+
if res && (session = env[self.class.sessions_opts[:env_key]])
|
|
237
241
|
@_request.persist_session(res[1], session)
|
|
238
242
|
end
|
|
239
243
|
end
|
|
@@ -247,7 +251,7 @@ class Roda
|
|
|
247
251
|
# this method stores the session in 'rack.session' in the request environment,
|
|
248
252
|
# but that does not happen until this method is called.
|
|
249
253
|
def session
|
|
250
|
-
@env[roda_class.
|
|
254
|
+
@env[roda_class.sessions_opts[:env_key]] ||= _load_session
|
|
251
255
|
end
|
|
252
256
|
|
|
253
257
|
# The time the session was originally created. nil if there is no active session.
|
|
@@ -266,7 +270,7 @@ class Roda
|
|
|
266
270
|
# Rack::Session::Cookie, mark the related cookie for expiration so it isn't
|
|
267
271
|
# sent in the future.
|
|
268
272
|
def persist_session(headers, session)
|
|
269
|
-
opts = roda_class.
|
|
273
|
+
opts = roda_class.sessions_opts
|
|
270
274
|
|
|
271
275
|
if session.empty?
|
|
272
276
|
if env[SESSION_SERIALIZED]
|
|
@@ -317,7 +321,7 @@ class Roda
|
|
|
317
321
|
# Load the session by looking for the appropriate cookie, or falling
|
|
318
322
|
# back to the rack session cookie if configured.
|
|
319
323
|
def _load_session
|
|
320
|
-
opts = roda_class.
|
|
324
|
+
opts = roda_class.sessions_opts
|
|
321
325
|
cs = cookies
|
|
322
326
|
|
|
323
327
|
if data = cs[opts[:key]]
|
|
@@ -340,7 +344,7 @@ class Roda
|
|
|
340
344
|
# serialized session using the default Rack::Session::Cookie
|
|
341
345
|
# hmac and coder.
|
|
342
346
|
def _deserialize_rack_session(data)
|
|
343
|
-
opts = roda_class.
|
|
347
|
+
opts = roda_class.sessions_opts
|
|
344
348
|
data, digest = data.split("--", 2)
|
|
345
349
|
unless digest
|
|
346
350
|
return _session_serialization_error("Not decoding Rack::Session::Cookie session: invalid format")
|
|
@@ -374,7 +378,7 @@ class Roda
|
|
|
374
378
|
|
|
375
379
|
# Interpret given cookie data as a Rack::Session::Cookie
|
|
376
380
|
def _deserialize_session(data)
|
|
377
|
-
opts = roda_class.
|
|
381
|
+
opts = roda_class.sessions_opts
|
|
378
382
|
|
|
379
383
|
begin
|
|
380
384
|
data = Base64_.urlsafe_decode64(data)
|
|
@@ -466,7 +470,7 @@ class Roda
|
|
|
466
470
|
end
|
|
467
471
|
|
|
468
472
|
def _serialize_session(session)
|
|
469
|
-
opts = roda_class.
|
|
473
|
+
opts = roda_class.sessions_opts
|
|
470
474
|
env = @env
|
|
471
475
|
now = Time.now.to_i
|
|
472
476
|
json_data = opts[:serializer].call(session).force_encoding('BINARY')
|
|
@@ -74,6 +74,8 @@ class Roda
|
|
|
74
74
|
end
|
|
75
75
|
|
|
76
76
|
module ClassMethods
|
|
77
|
+
RodaPlugins.opt_attr_reader(self, :shape_friendly_scope_instance_variables)
|
|
78
|
+
|
|
77
79
|
# Automatically refresh the instance variables used if the plugin
|
|
78
80
|
# sets instance variables.
|
|
79
81
|
def plugin(plugin, *args, &block)
|
|
@@ -90,9 +92,9 @@ class Roda
|
|
|
90
92
|
end
|
|
91
93
|
nil
|
|
92
94
|
end
|
|
93
|
-
# :
|
|
95
|
+
# simplecov:disable
|
|
94
96
|
ruby2_keywords(:plugin) if respond_to?(:ruby2_keywords, true)
|
|
95
|
-
# :
|
|
97
|
+
# simplecov:enable
|
|
96
98
|
|
|
97
99
|
private
|
|
98
100
|
|
|
@@ -107,7 +109,7 @@ class Roda
|
|
|
107
109
|
ivs.concat(mod.const_get(const)) if mod.const_defined?(const)
|
|
108
110
|
end
|
|
109
111
|
|
|
110
|
-
if const == :SCOPE_INSTANCE_VARIABLES && (scope_ivs =
|
|
112
|
+
if const == :SCOPE_INSTANCE_VARIABLES && (scope_ivs = shape_friendly_scope_instance_variables)
|
|
111
113
|
ivs.concat(scope_ivs)
|
|
112
114
|
end
|
|
113
115
|
|
|
@@ -132,9 +134,9 @@ class Roda
|
|
|
132
134
|
end
|
|
133
135
|
end
|
|
134
136
|
|
|
135
|
-
# :
|
|
137
|
+
# simplecov:disable
|
|
136
138
|
if RUBY_VERSION >= '4.0'
|
|
137
|
-
# :
|
|
139
|
+
# simplecov:enable
|
|
138
140
|
module InstanceMethods
|
|
139
141
|
private
|
|
140
142
|
|
|
@@ -256,6 +256,11 @@ class Roda
|
|
|
256
256
|
end
|
|
257
257
|
end
|
|
258
258
|
|
|
259
|
+
module ClassMethods
|
|
260
|
+
RodaPlugins.opt_attr_reader(self, :absolute_redirects, name: :absolute_redirects?)
|
|
261
|
+
RodaPlugins.opt_attr_reader(self, :prefixed_redirects, name: :prefixed_redirects?)
|
|
262
|
+
end
|
|
263
|
+
|
|
259
264
|
module RequestMethods
|
|
260
265
|
# Alias for referrer
|
|
261
266
|
def back
|
|
@@ -285,9 +290,8 @@ class Roda
|
|
|
285
290
|
# If the absolute_redirects or :prefixed_redirects roda class options has been set, respect those
|
|
286
291
|
# and update the path.
|
|
287
292
|
def redirect(path=(no_add_script_name = true; default_redirect_path), status=default_redirect_status)
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
prefixed_redirects = no_add_script_name ? false : opts[:prefixed_redirects]
|
|
293
|
+
absolute_redirects = roda_class.absolute_redirects?
|
|
294
|
+
prefixed_redirects = no_add_script_name ? false : roda_class.prefixed_redirects?
|
|
291
295
|
path = uri(path, absolute_redirects, prefixed_redirects) if absolute_redirects || prefixed_redirects
|
|
292
296
|
super(path, status)
|
|
293
297
|
end
|
data/lib/roda/plugins/static.rb
CHANGED
|
@@ -21,6 +21,9 @@ class Roda
|
|
|
21
21
|
# uses a separate option for it.
|
|
22
22
|
#
|
|
23
23
|
# Users of this plugin may want to consider using the public plugin instead.
|
|
24
|
+
# As this only loads a middleware, it does not integrate with other Roda
|
|
25
|
+
# security features, such as the content_security_policy and host_authorization
|
|
26
|
+
# plugins.
|
|
24
27
|
#
|
|
25
28
|
# Examples:
|
|
26
29
|
#
|
|
@@ -37,6 +37,8 @@ class Roda
|
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
module ClassMethods
|
|
40
|
+
RodaPlugins.opt_attr_reader(self, :status_handler, name: :status_handlers)
|
|
41
|
+
|
|
40
42
|
# Install the given block as a status handler for the given HTTP response code.
|
|
41
43
|
def status_handler(code, opts=OPTS, &block)
|
|
42
44
|
# For backwards compatibility, pass request argument if block accepts argument
|
|
@@ -54,7 +56,7 @@ class Roda
|
|
|
54
56
|
end
|
|
55
57
|
|
|
56
58
|
meth = define_roda_method(:"_roda_status_handler__#{code}", arity, &block)
|
|
57
|
-
|
|
59
|
+
status_handlers[code] = define_roda_method(:"_roda_status_handler_#{code}", 1) do |result|
|
|
58
60
|
res = @_response
|
|
59
61
|
res.status = result[0]
|
|
60
62
|
handle_headers.call(res.headers)
|
|
@@ -64,7 +66,7 @@ class Roda
|
|
|
64
66
|
|
|
65
67
|
# Freeze the hash of status handlers so that there can be no thread safety issues at runtime.
|
|
66
68
|
def freeze
|
|
67
|
-
|
|
69
|
+
status_handlers.freeze
|
|
68
70
|
super
|
|
69
71
|
end
|
|
70
72
|
end
|
|
@@ -74,7 +76,7 @@ class Roda
|
|
|
74
76
|
|
|
75
77
|
# If routing returns a response we have a handler for, call that handler.
|
|
76
78
|
def _roda_after_20__status_handler(result)
|
|
77
|
-
if result && (meth =
|
|
79
|
+
if result && (meth = self.class.status_handlers[result[0]]) && (v = result[2]).is_a?(Array) && v.empty?
|
|
78
80
|
send(meth, result)
|
|
79
81
|
end
|
|
80
82
|
end
|
|
@@ -21,9 +21,11 @@ class Roda
|
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
module ClassMethods
|
|
24
|
+
RodaPlugins.opt_attr_reader(self, :strip_path_prefix, name: :strip_path_prefix_regexp)
|
|
25
|
+
|
|
24
26
|
# Strip the path prefix from the gien path if it starts with the prefix.
|
|
25
|
-
def expand_path(path, root=
|
|
26
|
-
super.sub(
|
|
27
|
+
def expand_path(path, root=app_root)
|
|
28
|
+
super.sub(strip_path_prefix_regexp, '')
|
|
27
29
|
end
|
|
28
30
|
end
|
|
29
31
|
end
|
|
@@ -121,6 +121,8 @@ class Roda
|
|
|
121
121
|
end
|
|
122
122
|
|
|
123
123
|
module ClassMethods
|
|
124
|
+
RodaPlugins.opt_attr_reader(self, :symbol_matchers)
|
|
125
|
+
|
|
124
126
|
# Set the matcher and block to use for the given class.
|
|
125
127
|
# The matcher can be a regexp, registered symbol matcher, or registered class
|
|
126
128
|
# matcher (if using the class_matchers plugin).
|
|
@@ -141,7 +143,7 @@ class Roda
|
|
|
141
143
|
|
|
142
144
|
# Freeze the class_matchers hash when freezing the app.
|
|
143
145
|
def freeze
|
|
144
|
-
|
|
146
|
+
symbol_matchers.freeze
|
|
145
147
|
super
|
|
146
148
|
end
|
|
147
149
|
end
|
|
@@ -46,7 +46,11 @@ class Roda
|
|
|
46
46
|
#
|
|
47
47
|
# The options given are also passed to the public plugin.
|
|
48
48
|
def self.configure(app, opts=OPTS)
|
|
49
|
-
app.opts[:timestamp_public_prefix] = (opts[:prefix] || app.
|
|
49
|
+
app.opts[:timestamp_public_prefix] = (opts[:prefix] || app.timestamp_public_prefix || "static").dup.freeze
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
module ClassMethods
|
|
53
|
+
RodaPlugins.opt_attr_reader(self, :timestamp_public_prefix)
|
|
50
54
|
end
|
|
51
55
|
|
|
52
56
|
module InstanceMethods
|
|
@@ -54,8 +58,8 @@ class Roda
|
|
|
54
58
|
# This does not check the file is inside the directory for performance reasons,
|
|
55
59
|
# so this should not be called with untrusted input.
|
|
56
60
|
def timestamp_path(file)
|
|
57
|
-
mtime = File.mtime(File.join(
|
|
58
|
-
"/#{
|
|
61
|
+
mtime = File.mtime(File.join(self.class.public_root, file))
|
|
62
|
+
"/#{self.class.timestamp_public_prefix}/#{sprintf("%i%06i", mtime.to_i, mtime.usec)}/#{file}"
|
|
59
63
|
end
|
|
60
64
|
end
|
|
61
65
|
|
|
@@ -65,7 +69,7 @@ class Roda
|
|
|
65
69
|
# a integer segment for the timestamp, and this is a GET request.
|
|
66
70
|
def timestamp_public
|
|
67
71
|
if is_get?
|
|
68
|
-
on roda_class.
|
|
72
|
+
on roda_class.timestamp_public_prefix, Integer do |_|
|
|
69
73
|
public
|
|
70
74
|
end
|
|
71
75
|
end
|
|
@@ -110,7 +110,7 @@ class Roda
|
|
|
110
110
|
}.freeze
|
|
111
111
|
|
|
112
112
|
def self.configure(app, opts = {})
|
|
113
|
-
config = (app.
|
|
113
|
+
config = (app.type_routing_opts || CONFIGURATION).dup
|
|
114
114
|
[:use_extension, :use_header, :default_type].each do |key|
|
|
115
115
|
config[key] = opts[key] if opts.has_key?(key)
|
|
116
116
|
end
|
|
@@ -149,12 +149,16 @@ class Roda
|
|
|
149
149
|
app.opts[:type_routing] = config.freeze
|
|
150
150
|
end
|
|
151
151
|
|
|
152
|
+
module ClassMethods
|
|
153
|
+
RodaPlugins.opt_attr_reader(self, :type_routing, name: :type_routing_opts)
|
|
154
|
+
end
|
|
155
|
+
|
|
152
156
|
module RequestMethods
|
|
153
157
|
# Yields if the given +type+ matches the requested data type and halts
|
|
154
158
|
# the request afterwards, returning the result of the block.
|
|
155
159
|
def on_type(type, &block)
|
|
156
160
|
return unless type == requested_type
|
|
157
|
-
response[RodaResponseHeaders::CONTENT_TYPE] ||=
|
|
161
|
+
response[RodaResponseHeaders::CONTENT_TYPE] ||= roda_class.type_routing_opts[:types][type]
|
|
158
162
|
always(&block)
|
|
159
163
|
end
|
|
160
164
|
|
|
@@ -162,7 +166,7 @@ class Roda
|
|
|
162
166
|
def requested_type
|
|
163
167
|
return @requested_type if @requested_type
|
|
164
168
|
|
|
165
|
-
opts =
|
|
169
|
+
opts = roda_class.type_routing_opts
|
|
166
170
|
@requested_type = accept_response_type if opts[:use_header]
|
|
167
171
|
@requested_type ||= opts[:default_type]
|
|
168
172
|
end
|
|
@@ -182,7 +186,7 @@ class Roda
|
|
|
182
186
|
# Removes a trailing file extension from the path, and sets
|
|
183
187
|
# the requested type if so.
|
|
184
188
|
def _remaining_path(env)
|
|
185
|
-
opts =
|
|
189
|
+
opts = roda_class.type_routing_opts
|
|
186
190
|
path = super
|
|
187
191
|
|
|
188
192
|
if opts[:use_extension]
|
|
@@ -197,12 +201,12 @@ class Roda
|
|
|
197
201
|
|
|
198
202
|
# The response type indicated by the Accept request header.
|
|
199
203
|
def accept_response_type
|
|
200
|
-
mimes =
|
|
204
|
+
mimes = roda_class.type_routing_opts[:mimes]
|
|
205
|
+
response[RodaResponseHeaders::VARY] = (vary = response[RodaResponseHeaders::VARY]) ? "#{vary}, Accept" : 'Accept'
|
|
201
206
|
|
|
202
207
|
@env['HTTP_ACCEPT'].to_s.split(/\s*,\s*/).map do |part|
|
|
203
208
|
mime, _= part.split(/\s*;\s*/, 2)
|
|
204
209
|
if sym = mimes[mime]
|
|
205
|
-
response[RodaResponseHeaders::VARY] = (vary = response[RodaResponseHeaders::VARY]) ? "#{vary}, Accept" : 'Accept'
|
|
206
210
|
return sym
|
|
207
211
|
end
|
|
208
212
|
end
|