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
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
# frozen-string-literal: true
|
|
2
2
|
|
|
3
|
-
require 'json'
|
|
4
3
|
require 'find'
|
|
5
4
|
|
|
6
5
|
class Roda
|
|
@@ -29,6 +28,7 @@ class Roda
|
|
|
29
28
|
# load_hash_public_cache_file
|
|
30
29
|
module HashPublicCache
|
|
31
30
|
def self.load_dependencies(app, _cache_file, opts = OPTS)
|
|
31
|
+
app.plugin :_json
|
|
32
32
|
app.plugin :hash_public, opts
|
|
33
33
|
end
|
|
34
34
|
|
|
@@ -40,13 +40,15 @@ class Roda
|
|
|
40
40
|
end
|
|
41
41
|
|
|
42
42
|
module ClassMethods
|
|
43
|
+
RodaPlugins.opt_attr_reader(self, :hash_public_cache_file)
|
|
44
|
+
|
|
43
45
|
# Load the hash public cache file, if it exists. This replaces the hash
|
|
44
46
|
# public cache with the values from the file.
|
|
45
47
|
def load_hash_public_cache_file
|
|
46
|
-
file =
|
|
48
|
+
file = hash_public_cache_file
|
|
47
49
|
return unless File.file?(file)
|
|
48
50
|
|
|
49
|
-
cache = opts[:hash_public_cache] =
|
|
51
|
+
cache = opts[:hash_public_cache] = json_parser.call(::File.read(file))
|
|
50
52
|
cache.each_value(&:freeze)
|
|
51
53
|
nil
|
|
52
54
|
end
|
|
@@ -56,13 +58,13 @@ class Roda
|
|
|
56
58
|
# If a block is given, it will only calculate the digest for the file
|
|
57
59
|
# if the block returns truthy.
|
|
58
60
|
def scan_hash_public_cache_dir
|
|
59
|
-
cache =
|
|
61
|
+
cache = hash_public_cache
|
|
60
62
|
|
|
61
63
|
# Public root doesn't have trailing slash even if given, as
|
|
62
64
|
# File.expand_path removes it.
|
|
63
|
-
root =
|
|
65
|
+
root = public_root + File::SEPARATOR
|
|
64
66
|
|
|
65
|
-
Find.find(
|
|
67
|
+
Find.find(public_root) do |file|
|
|
66
68
|
if File.file?(file)
|
|
67
69
|
file = file.sub(root, '')
|
|
68
70
|
next if cache[file]
|
|
@@ -80,7 +82,7 @@ class Roda
|
|
|
80
82
|
|
|
81
83
|
# Write the current hash public cache to the cache file.
|
|
82
84
|
def dump_hash_public_cache_file
|
|
83
|
-
File.write(
|
|
85
|
+
File.write(hash_public_cache_file, json_serializer.call(hash_public_cache))
|
|
84
86
|
nil
|
|
85
87
|
end
|
|
86
88
|
end
|
|
@@ -191,7 +191,7 @@ class Roda
|
|
|
191
191
|
def dispatch_from(namespace='', branch, &block)
|
|
192
192
|
ns = @namespace
|
|
193
193
|
if block
|
|
194
|
-
meth_hash = @roda.
|
|
194
|
+
meth_hash = @roda.hash_routes_methods
|
|
195
195
|
key = [:dispatch_from, namespace, branch].freeze
|
|
196
196
|
meth = meth_hash[key] = @roda.define_roda_method(meth_hash[key] || "hash_routes_dispatch_from_#{namespace}_#{branch}", 1, &block)
|
|
197
197
|
@roda.hash_branch(namespace, branch) do |r|
|
|
@@ -253,7 +253,7 @@ class Roda
|
|
|
253
253
|
# Returns 404 for requests for the path with a different request method.
|
|
254
254
|
def verb(verb, path, &block)
|
|
255
255
|
path = path == true ? "" : "/#{path}"
|
|
256
|
-
meth_hash = @roda.
|
|
256
|
+
meth_hash = @roda.hash_routes_methods
|
|
257
257
|
key = [@namespace, path].freeze
|
|
258
258
|
meth = meth_hash[key] = @roda.define_roda_method(meth_hash[key] || "hash_routes_#{@namespace}_#{path}", 0, &block)
|
|
259
259
|
@roda.hash_path(@namespace, path) do |r|
|
|
@@ -265,9 +265,11 @@ class Roda
|
|
|
265
265
|
end
|
|
266
266
|
|
|
267
267
|
module ClassMethods
|
|
268
|
+
RodaPlugins.opt_attr_reader(self, :hash_routes_methods)
|
|
269
|
+
|
|
268
270
|
# Freeze the hash_routes metadata when freezing the app.
|
|
269
271
|
def freeze
|
|
270
|
-
|
|
272
|
+
hash_routes_methods.freeze
|
|
271
273
|
super
|
|
272
274
|
end
|
|
273
275
|
|
|
@@ -16,7 +16,11 @@ class Roda
|
|
|
16
16
|
module Heartbeat
|
|
17
17
|
# Set the heartbeat path to the given path.
|
|
18
18
|
def self.configure(app, opts=OPTS)
|
|
19
|
-
app.opts[:heartbeat_path] = (opts[:path] || app.
|
|
19
|
+
app.opts[:heartbeat_path] = (opts[:path] || app.heartbeat_path || "/heartbeat").dup.freeze
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
module ClassMethods
|
|
23
|
+
RodaPlugins.opt_attr_reader(self, :heartbeat_path)
|
|
20
24
|
end
|
|
21
25
|
|
|
22
26
|
module InstanceMethods
|
|
@@ -24,7 +28,7 @@ class Roda
|
|
|
24
28
|
|
|
25
29
|
# If the request is for a heartbeat path, return the heartbeat response.
|
|
26
30
|
def _roda_before_20__heartbeat
|
|
27
|
-
if env['PATH_INFO'] ==
|
|
31
|
+
if env['PATH_INFO'] == self.class.heartbeat_path
|
|
28
32
|
response = @_response
|
|
29
33
|
response.status = 200
|
|
30
34
|
response[RodaResponseHeaders::CONTENT_TYPE] = 'text/plain'
|
|
@@ -231,6 +231,12 @@ class Roda
|
|
|
231
231
|
end
|
|
232
232
|
end
|
|
233
233
|
|
|
234
|
+
module ClassMethods
|
|
235
|
+
RodaPlugins.opt_attr_reader(self, :hmac_paths_secret)
|
|
236
|
+
RodaPlugins.opt_attr_reader(self, :hmac_paths_old_secret)
|
|
237
|
+
RodaPlugins.opt_attr_reader(self, :hmac_paths_namespace_session_key)
|
|
238
|
+
end
|
|
239
|
+
|
|
234
240
|
module InstanceMethods
|
|
235
241
|
# Return a path with an HMAC. Designed to be used with r.hmac_path, to make sure
|
|
236
242
|
# users can only request paths that they have been provided by the application
|
|
@@ -315,7 +321,7 @@ class Roda
|
|
|
315
321
|
# using the secret given in the plugin, for the given root and options.
|
|
316
322
|
# This always returns a hexidecimal string.
|
|
317
323
|
def hmac_path_hmac_secret(root, opts=OPTS)
|
|
318
|
-
secret = opts[:secret] || self.
|
|
324
|
+
secret = opts[:secret] || self.class.hmac_paths_secret
|
|
319
325
|
|
|
320
326
|
if namespace = hmac_path_namespace(opts)
|
|
321
327
|
secret = OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new, secret, namespace)
|
|
@@ -326,7 +332,7 @@ class Roda
|
|
|
326
332
|
|
|
327
333
|
# The default namespace to use for hmac_path, if a :namespace option is not provided.
|
|
328
334
|
def hmac_path_default_namespace
|
|
329
|
-
if (key =
|
|
335
|
+
if (key = self.class.hmac_paths_namespace_session_key) && (value = session[key])
|
|
330
336
|
value.to_s
|
|
331
337
|
end
|
|
332
338
|
end
|
|
@@ -346,43 +352,41 @@ class Roda
|
|
|
346
352
|
|
|
347
353
|
if submitted_hmac.bytesize == 64
|
|
348
354
|
on String do |flags|
|
|
349
|
-
if flags.
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
end
|
|
355
|
+
if flags.include?('n') ^ !scope.hmac_path_namespace(opts).nil?
|
|
356
|
+
# Namespace required and not provided, or provided and not required.
|
|
357
|
+
# Bail early to avoid unnecessary HMAC calculation.
|
|
358
|
+
@remaining_path = orig_path
|
|
359
|
+
return
|
|
360
|
+
end
|
|
356
361
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
362
|
+
if flags.include?('m')
|
|
363
|
+
rpath = "#{env['REQUEST_METHOD'].to_s.upcase}:#{rpath}"
|
|
364
|
+
end
|
|
360
365
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
366
|
+
if flags.include?('p')
|
|
367
|
+
rpath = "#{rpath}?#{env["QUERY_STRING"]}"
|
|
368
|
+
end
|
|
364
369
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
end
|
|
370
|
+
if hmac_path_valid?(mpath, rpath, submitted_hmac, opts)
|
|
371
|
+
if flags.include?('t')
|
|
372
|
+
on Integer do |int|
|
|
373
|
+
if int >= Time.now.to_i
|
|
374
|
+
always(&block)
|
|
375
|
+
else
|
|
376
|
+
# Return from method without matching
|
|
377
|
+
@remaining_path = orig_path
|
|
378
|
+
return
|
|
375
379
|
end
|
|
376
|
-
else
|
|
377
|
-
always(&block)
|
|
378
380
|
end
|
|
381
|
+
else
|
|
382
|
+
always(&block)
|
|
379
383
|
end
|
|
380
384
|
end
|
|
381
|
-
|
|
382
|
-
# Return from method without matching
|
|
383
|
-
@remaining_path = orig_path
|
|
384
|
-
return
|
|
385
385
|
end
|
|
386
|
+
|
|
387
|
+
# Return from method without matching
|
|
388
|
+
@remaining_path = orig_path
|
|
389
|
+
return
|
|
386
390
|
end
|
|
387
391
|
|
|
388
392
|
# Return from method without matching
|
|
@@ -397,7 +401,7 @@ class Roda
|
|
|
397
401
|
def hmac_path_valid?(root, path, hmac, opts=OPTS)
|
|
398
402
|
if Rack::Utils.secure_compare(scope.hmac_path_hmac(root, path, opts), hmac)
|
|
399
403
|
true
|
|
400
|
-
elsif old_secret = roda_class.
|
|
404
|
+
elsif old_secret = roda_class.hmac_paths_old_secret
|
|
401
405
|
opts = opts.dup
|
|
402
406
|
opts[:secret] = old_secret
|
|
403
407
|
Rack::Utils.secure_compare(scope.hmac_path_hmac(root, path, opts), hmac)
|
data/lib/roda/plugins/hooks.rb
CHANGED
|
@@ -39,21 +39,24 @@ class Roda
|
|
|
39
39
|
end
|
|
40
40
|
|
|
41
41
|
module ClassMethods
|
|
42
|
+
RodaPlugins.opt_attr_reader(self, :after_hooks)
|
|
43
|
+
RodaPlugins.opt_attr_reader(self, :before_hooks)
|
|
44
|
+
|
|
42
45
|
# Freeze the array of hook methods when freezing the app.
|
|
43
46
|
def freeze
|
|
44
|
-
|
|
45
|
-
|
|
47
|
+
after_hooks.freeze
|
|
48
|
+
before_hooks.freeze
|
|
46
49
|
|
|
47
50
|
super
|
|
48
51
|
end
|
|
49
52
|
|
|
50
53
|
# Add an after hook.
|
|
51
54
|
def after(&block)
|
|
52
|
-
|
|
53
|
-
if
|
|
54
|
-
class_eval("alias _roda_after_80__hooks #{
|
|
55
|
+
after_hooks << define_roda_method("after_hook", 1, &block)
|
|
56
|
+
if after_hooks.length == 1
|
|
57
|
+
class_eval("alias _roda_after_80__hooks #{after_hooks.first}", __FILE__, __LINE__)
|
|
55
58
|
else
|
|
56
|
-
class_eval("def _roda_after_80__hooks(res) #{
|
|
59
|
+
class_eval("def _roda_after_80__hooks(res) #{after_hooks.map{|m| "#{m}(res)"}.join(';')} end", __FILE__, __LINE__)
|
|
57
60
|
end
|
|
58
61
|
private :_roda_after_80__hooks
|
|
59
62
|
def_roda_after
|
|
@@ -62,11 +65,11 @@ class Roda
|
|
|
62
65
|
|
|
63
66
|
# Add a before hook.
|
|
64
67
|
def before(&block)
|
|
65
|
-
|
|
66
|
-
if
|
|
67
|
-
class_eval("alias _roda_before_10__hooks #{
|
|
68
|
+
before_hooks.unshift(define_roda_method("before_hook", 0, &block))
|
|
69
|
+
if before_hooks.length == 1
|
|
70
|
+
class_eval("alias _roda_before_10__hooks #{before_hooks.first}", __FILE__, __LINE__)
|
|
68
71
|
else
|
|
69
|
-
class_eval("def _roda_before_10__hooks; #{
|
|
72
|
+
class_eval("def _roda_before_10__hooks; #{before_hooks.join(';')} end", __FILE__, __LINE__)
|
|
70
73
|
end
|
|
71
74
|
private :_roda_before_10__hooks
|
|
72
75
|
def_roda_before
|
|
@@ -101,6 +101,11 @@ class Roda
|
|
|
101
101
|
end
|
|
102
102
|
end
|
|
103
103
|
|
|
104
|
+
module ClassMethods
|
|
105
|
+
RodaPlugins.opt_attr_reader(self, :host_authorization_host)
|
|
106
|
+
RodaPlugins.opt_attr_reader(self, :host_authorization_check_forwarded, name: :host_authorization_check_forwarded?)
|
|
107
|
+
end
|
|
108
|
+
|
|
104
109
|
module InstanceMethods
|
|
105
110
|
# Check whether the host is authorized. If not authorized, return a response
|
|
106
111
|
# immediately based on the plugin block.
|
|
@@ -108,7 +113,7 @@ class Roda
|
|
|
108
113
|
r = @_request
|
|
109
114
|
return if host_authorized?(_convert_host_for_authorization(r.env["HTTP_HOST"].to_s.dup))
|
|
110
115
|
|
|
111
|
-
if
|
|
116
|
+
if self.class.host_authorization_check_forwarded? && (host = r.env["HTTP_X_FORWARDED_HOST"])
|
|
112
117
|
if i = host.rindex(',')
|
|
113
118
|
host = host[i+1, 10000000].to_s
|
|
114
119
|
end
|
|
@@ -133,7 +138,7 @@ class Roda
|
|
|
133
138
|
end
|
|
134
139
|
|
|
135
140
|
# Whether the host given is one of the authorized hosts for this application.
|
|
136
|
-
def host_authorized?(host, authorized_host =
|
|
141
|
+
def host_authorized?(host, authorized_host = self.class.host_authorization_host)
|
|
137
142
|
case authorized_host
|
|
138
143
|
when Array
|
|
139
144
|
authorized_host.any?{|auth_host| host_authorized?(host, auth_host)}
|
|
@@ -68,8 +68,29 @@ class Roda
|
|
|
68
68
|
# end
|
|
69
69
|
# end
|
|
70
70
|
#
|
|
71
|
-
# This plugin uses the host method on the request to get the hostname (this method
|
|
72
|
-
# is defined by Rack).
|
|
71
|
+
# This plugin uses the +host+ method on the request to get the hostname (this method
|
|
72
|
+
# is defined by Rack). Note that as of Rack 3.2, Rack's default approach to determine
|
|
73
|
+
# the hostname is insecure in many cases, since:
|
|
74
|
+
#
|
|
75
|
+
# * It will use a forwarded header instead of the +host+ header by default if the
|
|
76
|
+
# request was from a trusted proxy.
|
|
77
|
+
# * Rack trust proxies from any private IP address range by default
|
|
78
|
+
# (this can be disabled using <tt>Rack::Request.ip_filter=</tt>).
|
|
79
|
+
# * Rack supports both +forwarded+ and <tt>x-forwarded-host</tt> headers, will check
|
|
80
|
+
# both by default, and the order of checking depends on Rack's configuration
|
|
81
|
+
# (this can be configured using <tt>Rack::Request.forwarded_priority</tt>).
|
|
82
|
+
# * If the application can be reached via an arbitrary +host+ header, the attacker
|
|
83
|
+
# has full control over the <tt>Rack::Request#host</tt> value even when forwarded
|
|
84
|
+
# host usage is disabled.
|
|
85
|
+
#
|
|
86
|
+
# You should configure Rack appropriately for your deployment, not allowing the use
|
|
87
|
+
# of forwarded information unless your deployment is using a reverse proxy, and only
|
|
88
|
+
# allowing the specific forwarded header set by the expected reverse proxy if a
|
|
89
|
+
# reverse proxy is used. Rack 3.3 should include more control in this area,
|
|
90
|
+
# and Rack 4 should ship with more secure defaults in regard to forwarding headers.
|
|
91
|
+
#
|
|
92
|
+
# It is recommended to use the host_authorization plugin to check that the requested
|
|
93
|
+
# host is allowed when using this plugin.
|
|
73
94
|
module HostRouting
|
|
74
95
|
REQUEST_INSTANCE_VARIABLES = [:@_host_routing_host].freeze
|
|
75
96
|
|
|
@@ -140,7 +161,7 @@ class Roda
|
|
|
140
161
|
# Treat all given hostnames as routing to the give host.
|
|
141
162
|
def to(host, *hostnames)
|
|
142
163
|
hostnames.each do |hostname|
|
|
143
|
-
@host_hash[hostname] = host
|
|
164
|
+
@host_hash[hostname.downcase.freeze] = host
|
|
144
165
|
end
|
|
145
166
|
end
|
|
146
167
|
|
|
@@ -155,6 +176,11 @@ class Roda
|
|
|
155
176
|
end
|
|
156
177
|
private_constant :DSL
|
|
157
178
|
|
|
179
|
+
module ClassMethods
|
|
180
|
+
RodaPlugins.opt_attr_reader(self, :host_routing_hash)
|
|
181
|
+
RodaPlugins.opt_attr_reader(self, :host_routing_default_host)
|
|
182
|
+
end
|
|
183
|
+
|
|
158
184
|
module InstanceMethods
|
|
159
185
|
# Handle case where plugin is used without providing a block to
|
|
160
186
|
# +hosts.default+. This returns nil, ensuring that the hostname
|
|
@@ -180,11 +206,15 @@ class Roda
|
|
|
180
206
|
# * The return value of the +hosts.default+ block, if given
|
|
181
207
|
# * The default value provided in the +hosts.default+ call
|
|
182
208
|
def _get_host_routing_host
|
|
183
|
-
host = self.host
|
|
209
|
+
if host = self.host
|
|
210
|
+
host = host.downcase
|
|
211
|
+
else
|
|
212
|
+
host = ""
|
|
213
|
+
end
|
|
184
214
|
|
|
185
|
-
roda_class.
|
|
215
|
+
roda_class.host_routing_hash[host] ||
|
|
186
216
|
scope._host_routing_default(host) ||
|
|
187
|
-
roda_class.
|
|
217
|
+
roda_class.host_routing_default_host
|
|
188
218
|
end
|
|
189
219
|
end
|
|
190
220
|
end
|
|
@@ -37,13 +37,25 @@ class Roda
|
|
|
37
37
|
# # To treat the exception raised as a submitted parameter
|
|
38
38
|
# {body_error: exception}
|
|
39
39
|
# end
|
|
40
|
+
#
|
|
41
|
+
# You can use the +rescue_classes+ argument when loading the plugin to only rescue
|
|
42
|
+
# specific exception classes:
|
|
43
|
+
#
|
|
44
|
+
# plugin :invalid_request_body, :empty_hash, rescue_classes: Rack::BadRequest
|
|
45
|
+
#
|
|
46
|
+
# However, this can be risky, because there have been many cases in the past where
|
|
47
|
+
# Rack can rescue classes such as +ArgumentError+ or +NoMethodError+ when parsing
|
|
48
|
+
# invalid request bodies. The default for +rescue_classes+ is +StandardError+.
|
|
40
49
|
module InvalidRequestBody
|
|
41
50
|
# Exception class raised for invalid request bodies.
|
|
42
51
|
Error = Class.new(RodaError)
|
|
43
52
|
|
|
44
53
|
# Set the action to use (:empty_400, :empty_hash, :raise) for invalid request bodies,
|
|
45
|
-
# or use a block for custom behavior.
|
|
46
|
-
|
|
54
|
+
# or use a block for custom behavior. The rescue_classes argument is an exception
|
|
55
|
+
# class or array of exception classes to rescue.
|
|
56
|
+
def self.configure(app, action=nil, rescue_classes: StandardError, &block)
|
|
57
|
+
app.opts[:invalid_response_body_rescue_classes] = Array(rescue_classes).dup.freeze
|
|
58
|
+
|
|
47
59
|
if action
|
|
48
60
|
if block
|
|
49
61
|
raise RodaError, "cannot provide both block and action when loading invalid_request_body plugin"
|
|
@@ -56,7 +68,7 @@ class Roda
|
|
|
56
68
|
|
|
57
69
|
app::RodaRequest.send(:alias_method, :handle_invalid_request_body, method)
|
|
58
70
|
elsif block
|
|
59
|
-
app::RodaRequest.
|
|
71
|
+
app::RodaRequest.class_exec do
|
|
60
72
|
define_method(:handle_invalid_request_body, &block)
|
|
61
73
|
alias handle_invalid_request_body handle_invalid_request_body
|
|
62
74
|
end
|
|
@@ -67,12 +79,18 @@ class Roda
|
|
|
67
79
|
app::RodaRequest.send(:private, :handle_invalid_request_body)
|
|
68
80
|
end
|
|
69
81
|
|
|
82
|
+
module ClassMethods
|
|
83
|
+
RodaPlugins.opt_attr_reader(self, :invalid_response_body_rescue_classes)
|
|
84
|
+
end
|
|
85
|
+
|
|
70
86
|
module RequestMethods
|
|
71
87
|
# Handle invalid request bodies as configured if the default behavior
|
|
72
88
|
# raises an exception.
|
|
73
89
|
def POST
|
|
74
90
|
super
|
|
75
|
-
rescue
|
|
91
|
+
rescue RodaError
|
|
92
|
+
raise
|
|
93
|
+
rescue *roda_class.invalid_response_body_rescue_classes => e
|
|
76
94
|
handle_invalid_request_body(e)
|
|
77
95
|
end
|
|
78
96
|
|
|
@@ -20,11 +20,15 @@ class Roda
|
|
|
20
20
|
app.opts[:ip_from_header_env_key] = "HTTP_#{header.upcase.tr('-', '_')}".freeze
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
+
module ClassMethods
|
|
24
|
+
RodaPlugins.opt_attr_reader(self, :ip_from_header_env_key)
|
|
25
|
+
end
|
|
26
|
+
|
|
23
27
|
module RequestMethods
|
|
24
28
|
# Return the IP address continained in the configured header, if present.
|
|
25
29
|
# Fallback to the default behavior if not present.
|
|
26
30
|
def ip
|
|
27
|
-
@env[roda_class.
|
|
31
|
+
@env[roda_class.ip_from_header_env_key] || super
|
|
28
32
|
end
|
|
29
33
|
end
|
|
30
34
|
end
|
data/lib/roda/plugins/json.rb
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen-string-literal: true
|
|
2
2
|
|
|
3
|
-
require 'json'
|
|
4
|
-
|
|
5
3
|
class Roda
|
|
6
4
|
module RodaPlugins
|
|
7
5
|
# The json plugin allows match blocks to return
|
|
@@ -57,6 +55,7 @@ class Roda
|
|
|
57
55
|
# not support treating String, FalseClass, or NilClass values as JSON.
|
|
58
56
|
module Json
|
|
59
57
|
def self.load_dependencies(app, opts=OPTS)
|
|
58
|
+
app.plugin :_json
|
|
60
59
|
app.plugin :custom_block_results
|
|
61
60
|
end
|
|
62
61
|
|
|
@@ -69,10 +68,10 @@ class Roda
|
|
|
69
68
|
classes.uniq!
|
|
70
69
|
classes.freeze
|
|
71
70
|
classes.each do |klass|
|
|
72
|
-
app.
|
|
71
|
+
app.custom_block_results[klass] = :handle_json_block_result
|
|
73
72
|
end
|
|
74
73
|
|
|
75
|
-
app.opts[:json_result_serializer] = opts[:serializer] || app.
|
|
74
|
+
app.opts[:json_result_serializer] = opts[:serializer] || app.json_result_serializer || app.json_serializer
|
|
76
75
|
|
|
77
76
|
app.opts[:json_result_include_request] = opts[:include_request] if opts.has_key?(:include_request)
|
|
78
77
|
|
|
@@ -80,6 +79,10 @@ class Roda
|
|
|
80
79
|
end
|
|
81
80
|
|
|
82
81
|
module ClassMethods
|
|
82
|
+
RodaPlugins.opt_attr_reader(self, :json_result_serializer)
|
|
83
|
+
RodaPlugins.opt_attr_reader(self, :json_result_include_request, name: :json_result_include_request?)
|
|
84
|
+
RodaPlugins.opt_attr_reader(self, :json_result_content_type)
|
|
85
|
+
|
|
83
86
|
# The classes that should be automatically converted to json
|
|
84
87
|
def json_result_classes
|
|
85
88
|
# RODA4: remove, only used by previous implementation.
|
|
@@ -91,7 +94,7 @@ class Roda
|
|
|
91
94
|
# Handle a result for one of the registered JSON result classes
|
|
92
95
|
# by converting the result to JSON.
|
|
93
96
|
def handle_json_block_result(result)
|
|
94
|
-
@_response[RodaResponseHeaders::CONTENT_TYPE] ||=
|
|
97
|
+
@_response[RodaResponseHeaders::CONTENT_TYPE] ||= self.class.json_result_content_type
|
|
95
98
|
@_request.send(:convert_to_json, result)
|
|
96
99
|
end
|
|
97
100
|
end
|
|
@@ -102,10 +105,9 @@ class Roda
|
|
|
102
105
|
# Convert the given object to JSON. Uses to_json by default,
|
|
103
106
|
# but can use a custom serializer passed to the plugin.
|
|
104
107
|
def convert_to_json(result)
|
|
105
|
-
|
|
106
|
-
serializer = opts[:json_result_serializer]
|
|
108
|
+
serializer = roda_class.json_result_serializer
|
|
107
109
|
|
|
108
|
-
if
|
|
110
|
+
if roda_class.json_result_include_request?
|
|
109
111
|
serializer.call(result, self)
|
|
110
112
|
else
|
|
111
113
|
serializer.call(result)
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen-string-literal: true
|
|
2
2
|
|
|
3
|
-
require 'json'
|
|
4
|
-
|
|
5
3
|
class Roda
|
|
6
4
|
module RodaPlugins
|
|
7
5
|
# The json_parser plugin parses request bodies in JSON format
|
|
@@ -17,7 +15,23 @@ class Roda
|
|
|
17
15
|
module JsonParser
|
|
18
16
|
DEFAULT_ERROR_HANDLER = proc{|r| r.halt [400, {}, []]}
|
|
19
17
|
|
|
18
|
+
# simplecov:disable
|
|
19
|
+
MATCH_METHOD = RUBY_VERSION >= '2.4' ? :match? : :match
|
|
20
|
+
# simplecov:enable
|
|
21
|
+
private_constant :MATCH_METHOD
|
|
22
|
+
|
|
23
|
+
def self.load_dependencies(app, opts=OPTS)
|
|
24
|
+
app.plugin :_json
|
|
25
|
+
end
|
|
26
|
+
|
|
20
27
|
# Handle options for the json_parser plugin:
|
|
28
|
+
# :content_type_regexp :: A regexp used to determine if the request's
|
|
29
|
+
# content type is JSON. For backwards compatibility,
|
|
30
|
+
# uses the insecure /json/ if not provided. Warns
|
|
31
|
+
# if this option is not provided. The recommended
|
|
32
|
+
# value to use is /\Aapplication\/json\b/i or
|
|
33
|
+
# /\Aapplication\/(?:vnd\.api\+)?json\b/i. Roda 4
|
|
34
|
+
# will default to the latter.
|
|
21
35
|
# :error_handler :: A proc to call if an exception is raised when
|
|
22
36
|
# parsing a JSON request body. The proc is called
|
|
23
37
|
# with the request object, and should probably call
|
|
@@ -34,9 +48,16 @@ class Roda
|
|
|
34
48
|
# :always will wrap all values, and a value of :unless_hash will
|
|
35
49
|
# only wrap values that are not already hashes.
|
|
36
50
|
def self.configure(app, opts=OPTS)
|
|
37
|
-
app.opts[:json_parser_error_handler] = opts[:error_handler] || app.
|
|
38
|
-
app.opts[:json_parser_parser] = opts[:parser] || app.
|
|
51
|
+
app.opts[:json_parser_error_handler] = opts[:error_handler] || app.json_parser_error_handler || DEFAULT_ERROR_HANDLER
|
|
52
|
+
app.opts[:json_parser_parser] = opts[:parser] || app.json_parser_parser || app.json_parser
|
|
39
53
|
app.opts[:json_parser_include_request] = opts[:include_request] if opts.has_key?(:include_request)
|
|
54
|
+
app.opts[:json_parser_content_type_regexp] = opts[:content_type_regexp] || app.json_parser_content_type_regexp
|
|
55
|
+
|
|
56
|
+
unless app.json_parser_content_type_regexp
|
|
57
|
+
# RODA4: Switch to /\Aapplication\/(?:vnd\.api\+)?json\b/i by default
|
|
58
|
+
RodaPlugins.warn(':content_type_regexp option not provided to json_parser plugin, using /json/ for backwards compatibility, which may be insecure, using /\Aapplication\/json\b/i or /\Aapplication\/(?:vnd\.api\+)?json\b/i is recommended')
|
|
59
|
+
app.opts[:json_parser_content_type_regexp] = /json/
|
|
60
|
+
end
|
|
40
61
|
|
|
41
62
|
case opts[:wrap]
|
|
42
63
|
when :unless_hash, :always
|
|
@@ -48,6 +69,14 @@ class Roda
|
|
|
48
69
|
end
|
|
49
70
|
end
|
|
50
71
|
|
|
72
|
+
module ClassMethods
|
|
73
|
+
RodaPlugins.opt_attr_reader(self, :json_parser_error_handler)
|
|
74
|
+
RodaPlugins.opt_attr_reader(self, :json_parser_parser)
|
|
75
|
+
RodaPlugins.opt_attr_reader(self, :json_parser_include_request, name: :json_parser_include_request?)
|
|
76
|
+
RodaPlugins.opt_attr_reader(self, :json_parser_content_type_regexp)
|
|
77
|
+
RodaPlugins.opt_attr_reader(self, :json_parser_wrap)
|
|
78
|
+
end
|
|
79
|
+
|
|
51
80
|
module RequestMethods
|
|
52
81
|
# If the Content-Type header in the request includes "json",
|
|
53
82
|
# parse the request body as JSON. Ignore an empty request body.
|
|
@@ -57,7 +86,7 @@ class Roda
|
|
|
57
86
|
return post_params
|
|
58
87
|
end
|
|
59
88
|
|
|
60
|
-
unless (input = env["rack.input"]) && (content_type = self.content_type) &&
|
|
89
|
+
unless (input = env["rack.input"]) && (content_type = self.content_type) && roda_class.json_parser_content_type_regexp.send(MATCH_METHOD, content_type)
|
|
61
90
|
return super
|
|
62
91
|
end
|
|
63
92
|
|
|
@@ -66,10 +95,10 @@ class Roda
|
|
|
66
95
|
begin
|
|
67
96
|
json_params = parse_json(str)
|
|
68
97
|
rescue
|
|
69
|
-
roda_class.
|
|
98
|
+
roda_class.json_parser_error_handler.call(self)
|
|
70
99
|
end
|
|
71
100
|
|
|
72
|
-
wrap = roda_class.
|
|
101
|
+
wrap = roda_class.json_parser_wrap
|
|
73
102
|
if wrap == :always || (wrap == :unless_hash && !json_params.is_a?(Hash))
|
|
74
103
|
json_params = {"_json"=>json_params}
|
|
75
104
|
end
|
|
@@ -80,11 +109,14 @@ class Roda
|
|
|
80
109
|
private
|
|
81
110
|
|
|
82
111
|
def parse_json(str)
|
|
83
|
-
|
|
84
|
-
args << self if roda_class.opts[:json_parser_include_request]
|
|
85
|
-
roda_class.opts[:json_parser_parser].call(*args)
|
|
86
|
-
end
|
|
112
|
+
parser = roda_class.json_parser_parser
|
|
87
113
|
|
|
114
|
+
if roda_class.json_parser_include_request?
|
|
115
|
+
parser.call(str, self)
|
|
116
|
+
else
|
|
117
|
+
parser.call(str)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
88
120
|
|
|
89
121
|
# Rack 3 dropped requirement that input be rewindable
|
|
90
122
|
if Rack.release >= '3'
|