roda 3.40.0 → 3.44.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG +30 -0
- data/doc/release_notes/3.41.0.txt +9 -0
- data/doc/release_notes/3.42.0.txt +21 -0
- data/doc/release_notes/3.43.0.txt +34 -0
- data/doc/release_notes/3.44.0.txt +23 -0
- data/lib/roda.rb +6 -0
- data/lib/roda/plugins/assets.rb +31 -10
- data/lib/roda/plugins/common_logger.rb +3 -2
- data/lib/roda/plugins/custom_matchers.rb +2 -0
- data/lib/roda/plugins/default_headers.rb +15 -14
- data/lib/roda/plugins/host_authorization.rb +156 -0
- data/lib/roda/plugins/optimized_segment_matchers.rb +53 -0
- data/lib/roda/plugins/optimized_string_matchers.rb +2 -1
- data/lib/roda/plugins/recheck_precompiled_assets.rb +107 -0
- data/lib/roda/plugins/render.rb +19 -4
- data/lib/roda/plugins/render_locals.rb +4 -0
- data/lib/roda/plugins/sinatra_helpers.rb +24 -2
- data/lib/roda/request.rb +2 -2
- data/lib/roda/version.rb +1 -1
- metadata +14 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b60182ef0483279ac1d1983495ba020c1803d6c1fe0fa84af882142f816f72bc
|
4
|
+
data.tar.gz: 20f6053b7c73fed454dc3f39e55a516d7a5461a4005356b5788480b8aab36f14
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 89359f0c972c236bee671606fc7254f4999c6fc03f1b0ef8ff7c9691b581b6e796197b9fbee6fd109ae4d1026624f041d2e64c6182dbf8c80818756543923c9d
|
7
|
+
data.tar.gz: 2d786d8641f5195a65874f3326abe07057748e4de2df7d0a8e8cd9439b6afceb08e65b6a3733e15b0289e273cc070033c1dc466e1909880410b4d05541fa04ae
|
data/CHANGELOG
CHANGED
@@ -1,3 +1,33 @@
|
|
1
|
+
= 3.44.0 (2021-05-12)
|
2
|
+
|
3
|
+
* Add optimized_segment_matchers plugin for optimized matchers for a single String class argument (jeremyevans)
|
4
|
+
|
5
|
+
* Use RFC 5987 UTF-8 and ISO-8859-1 encoded filenames when using send_file and attachment in the sinatra_helpers plugin (jeremyevans)
|
6
|
+
|
7
|
+
= 3.43.1 (2021-04-13)
|
8
|
+
|
9
|
+
* [SECURITY] Fix issue where loading content_security_policy plugin after default_headers plugin had no effect (jeremyevans)
|
10
|
+
|
11
|
+
= 3.43.0 (2021-04-12)
|
12
|
+
|
13
|
+
* Add host_authorization plugin, for checking that requests are submitted using an approved host (jeremyevans)
|
14
|
+
|
15
|
+
= 3.42.0 (2021-03-12)
|
16
|
+
|
17
|
+
* Make Roda.plugin support plugins using keyword arguments in Ruby 3 (jeremyevans)
|
18
|
+
|
19
|
+
* Make Roda.use support middleware using keyword arguments in Ruby 3 (pat) (#207)
|
20
|
+
|
21
|
+
* Support common_logger plugin :method option for specifying the method to call on the logger (fnordfish, jeremyevans) (#206)
|
22
|
+
|
23
|
+
* Add recheck_precompiled_assets plugin for checking for updates to the precompiled asset metadata file (jeremyevans)
|
24
|
+
|
25
|
+
* Make compile_assets class method in assets plugin use an atomic approach to writing precompiled metadata file (jeremyevans)
|
26
|
+
|
27
|
+
= 3.41.0 (2021-02-17)
|
28
|
+
|
29
|
+
* Improve view performance with :content option up to 3x by calling compiled template methods directly (jeremyevans)
|
30
|
+
|
1
31
|
= 3.40.0 (2021-01-14)
|
2
32
|
|
3
33
|
* Add freeze_template_caches! to the precompile_templates plugin, which ensures all templates are precompiled, and speeds up template access (jeremyevans)
|
@@ -0,0 +1,9 @@
|
|
1
|
+
= Improvements
|
2
|
+
|
3
|
+
* The performance of the render plugin's view method when passed the
|
4
|
+
:content option and no other options or arguments has been improved
|
5
|
+
by about 3x, by calling compiled template methods directly.
|
6
|
+
|
7
|
+
* The compiled template method for the layout is cleared when the
|
8
|
+
render plugin is loaded again, which can fix issues when it is
|
9
|
+
loaded with different options that affect the layout.
|
@@ -0,0 +1,21 @@
|
|
1
|
+
= New Features
|
2
|
+
|
3
|
+
* A recheck_precompiled_assets plugin has been added, which allows
|
4
|
+
for checking for updates to the precompiled asset metadata file,
|
5
|
+
and automatically using the updated data.
|
6
|
+
|
7
|
+
* The common_logger plugin now supports a :method plugin option to
|
8
|
+
specify the method to call on the logger.
|
9
|
+
|
10
|
+
= Other Improvements
|
11
|
+
|
12
|
+
* Plugins and middleware that use keyword arguments are now supported
|
13
|
+
in Ruby 3.
|
14
|
+
|
15
|
+
* The compile_assets class method in the assets plugin now uses an
|
16
|
+
atomic approach to writing the precompiled asset metadata file.
|
17
|
+
|
18
|
+
* Minor method visibility issues have been fixed. The custom_matchers
|
19
|
+
plugin no longer makes the unsupported_matcher request method
|
20
|
+
public, and the render plugin no longer makes the _layout_method
|
21
|
+
public when the application is frozen.
|
@@ -0,0 +1,34 @@
|
|
1
|
+
= New Features
|
2
|
+
|
3
|
+
* A host_authorization plugin has been added to verify the requested
|
4
|
+
Host header is authorized. Using it can prevent DNS rebinding
|
5
|
+
attacks in cases where the application can receive requests for
|
6
|
+
arbitrary hosts.
|
7
|
+
|
8
|
+
To check for authorized hosts in your routing tree, you call the
|
9
|
+
check_host_authorization! method. For example, if you want to
|
10
|
+
check for authorized hosts after serving requests for public
|
11
|
+
files, you could do:
|
12
|
+
|
13
|
+
plugin :public
|
14
|
+
plugin :host_authorization, 'my-domain-name.example.com'
|
15
|
+
|
16
|
+
route do |r|
|
17
|
+
r.public
|
18
|
+
check_host_authorized!
|
19
|
+
|
20
|
+
# ... rest of routing tree
|
21
|
+
end
|
22
|
+
|
23
|
+
In addition to handling single domain names via a string, you can
|
24
|
+
provide an array of domain names, a regexp to match again, or a
|
25
|
+
proc.
|
26
|
+
|
27
|
+
By default, requests using unauthorized hosts receive an empty 403
|
28
|
+
response. If you would like to customize the response, you can
|
29
|
+
pass a block when loading the plugin:
|
30
|
+
|
31
|
+
plugin :host_authorization, 'my-domain-name.example.com' do |r|
|
32
|
+
response.status = 403
|
33
|
+
"Response Body Here"
|
34
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
= New Features
|
2
|
+
|
3
|
+
* An optimized_segment_matchers plugin has been added that offers
|
4
|
+
very fast matchers for arbitrary segments (the same segments
|
5
|
+
that would be matched by the String class matcher). The
|
6
|
+
on_segment method it offers accepts no arguments and yields
|
7
|
+
the next segment if there is a segment. The is_segment method
|
8
|
+
is similar, but only yields if the next segment is the final
|
9
|
+
segment.
|
10
|
+
|
11
|
+
= Other Improvements
|
12
|
+
|
13
|
+
* The send_file and attachment methods in the sinatra_helpers plugin
|
14
|
+
now support RFC 5987 UTF-8 and ISO-8859-1 encoded filenames,
|
15
|
+
allowing modern browsers to save files with encoded chracters. For
|
16
|
+
older browsers that do not support RFC 5987, unsupported characters
|
17
|
+
in filenames are replaced with dashes. This is considered to be an
|
18
|
+
improvement over the previous behavior of using Ruby's inspect
|
19
|
+
output for the filename, which could contain backslashes (backslash
|
20
|
+
is not an allowed chracter in Windows filenames).
|
21
|
+
|
22
|
+
* The performance of the String class matcher has been slightly
|
23
|
+
improved.
|
data/lib/roda.rb
CHANGED
@@ -292,6 +292,9 @@ class Roda
|
|
292
292
|
plugin.configure(self, *args, &block) if plugin.respond_to?(:configure)
|
293
293
|
@app = nil
|
294
294
|
end
|
295
|
+
# :nocov:
|
296
|
+
ruby2_keywords(:plugin) if respond_to?(:ruby2_keywords, true)
|
297
|
+
# :nocov:
|
295
298
|
|
296
299
|
# Setup routing tree for the current Roda application, and build the
|
297
300
|
# underlying rack application using the stored middleware. Requires
|
@@ -327,6 +330,9 @@ class Roda
|
|
327
330
|
@middleware << [args, block].freeze
|
328
331
|
@app = nil
|
329
332
|
end
|
333
|
+
# :nocov:
|
334
|
+
ruby2_keywords(:use) if respond_to?(:ruby2_keywords, true)
|
335
|
+
# :nocov:
|
330
336
|
|
331
337
|
private
|
332
338
|
|
data/lib/roda/plugins/assets.rb
CHANGED
@@ -376,7 +376,7 @@ class Roda
|
|
376
376
|
|
377
377
|
if opts[:precompiled] && !opts[:compiled] && ::File.exist?(opts[:precompiled])
|
378
378
|
require 'json'
|
379
|
-
opts[:compiled] =
|
379
|
+
opts[:compiled] = app.send(:_precompiled_asset_metadata, opts[:precompiled])
|
380
380
|
end
|
381
381
|
|
382
382
|
if opts[:early_hints]
|
@@ -455,7 +455,7 @@ class Roda
|
|
455
455
|
require 'fileutils'
|
456
456
|
|
457
457
|
unless assets_opts[:compiled]
|
458
|
-
opts[:assets] = assets_opts.merge(:compiled =>
|
458
|
+
opts[:assets] = assets_opts.merge(:compiled => _compiled_assets_initial_hash).freeze
|
459
459
|
end
|
460
460
|
|
461
461
|
if type == nil
|
@@ -465,10 +465,12 @@ class Roda
|
|
465
465
|
_compile_assets(type)
|
466
466
|
end
|
467
467
|
|
468
|
-
if assets_opts[:precompiled]
|
468
|
+
if precompile_file = assets_opts[:precompiled]
|
469
469
|
require 'json'
|
470
|
-
::FileUtils.mkdir_p(File.dirname(
|
471
|
-
|
470
|
+
::FileUtils.mkdir_p(File.dirname(precompile_file))
|
471
|
+
tmp_file = "#{precompile_file}.tmp"
|
472
|
+
::File.open(tmp_file, 'wb'){|f| f.write((opts[:json_serializer] || :to_json.to_proc).call(assets_opts[:compiled]))}
|
473
|
+
::File.rename(tmp_file, precompile_file)
|
472
474
|
end
|
473
475
|
|
474
476
|
assets_opts[:compiled]
|
@@ -476,6 +478,11 @@ class Roda
|
|
476
478
|
|
477
479
|
private
|
478
480
|
|
481
|
+
# The initial hash to use to store compiled asset metadata.
|
482
|
+
def _compiled_assets_initial_hash
|
483
|
+
{}
|
484
|
+
end
|
485
|
+
|
479
486
|
# Internals of compile_assets, handling recursive calls for loading
|
480
487
|
# all asset groups under the given type.
|
481
488
|
def _compile_assets(type)
|
@@ -493,6 +500,11 @@ class Roda
|
|
493
500
|
end
|
494
501
|
end
|
495
502
|
|
503
|
+
# The precompiled asset metadata stored in the given file
|
504
|
+
def _precompiled_asset_metadata(file)
|
505
|
+
(opts[:json_parser] || ::JSON.method(:parse)).call(::File.read(file))
|
506
|
+
end
|
507
|
+
|
496
508
|
# Compile each array of files for the given type into a single
|
497
509
|
# file. Dirs should be an array of asset group names, if these
|
498
510
|
# are files in an asset group.
|
@@ -794,23 +806,32 @@ class Roda
|
|
794
806
|
# handled.
|
795
807
|
def assets_matchers
|
796
808
|
@assets_matchers ||= [:css, :js].map do |t|
|
797
|
-
|
809
|
+
if regexp = assets_regexp(t)
|
810
|
+
[t, regexp].freeze
|
811
|
+
end
|
798
812
|
end.compact.freeze
|
799
813
|
end
|
800
814
|
|
801
815
|
private
|
802
816
|
|
817
|
+
# A string for the asset filename for the asset type, key, and digest.
|
818
|
+
def _asset_regexp(type, key, digest)
|
819
|
+
"#{key.sub(/\A#{type}/, '')}.#{digest}.#{type}"
|
820
|
+
end
|
821
|
+
|
803
822
|
# The regexp matcher to use for the given type. This handles any asset groups
|
804
823
|
# for the asset types.
|
805
824
|
def assets_regexp(type)
|
806
825
|
o = roda_class.assets_opts
|
807
826
|
if compiled = o[:compiled]
|
808
|
-
assets = compiled.
|
809
|
-
|
810
|
-
|
827
|
+
assets = compiled.
|
828
|
+
select{|k,_| k =~ /\A#{type}/}.
|
829
|
+
map{|k, md| _asset_regexp(type, k, md)}
|
830
|
+
return if assets.empty?
|
811
831
|
/#{o[:"compiled_#{type}_prefix"]}(#{Regexp.union(assets)})/
|
812
832
|
else
|
813
|
-
assets =
|
833
|
+
return unless assets = o[type]
|
834
|
+
assets = unnest_assets_hash(assets)
|
814
835
|
ts = o[:timestamp_paths]
|
815
836
|
/#{o[:"#{type}_prefix"]}#{"\\d+#{ts}" if ts}(#{Regexp.union(assets.uniq)})#{o[:"#{type}_suffix"]}/
|
816
837
|
end
|
@@ -18,10 +18,11 @@ class Roda
|
|
18
18
|
# plugin :common_logger
|
19
19
|
# plugin :common_logger, $stdout
|
20
20
|
# plugin :common_logger, Logger.new('filename')
|
21
|
+
# plugin :common_logger, Logger.new('filename'), method: :debug
|
21
22
|
module CommonLogger
|
22
|
-
def self.configure(app, logger=nil)
|
23
|
+
def self.configure(app, logger=nil, opts=OPTS)
|
23
24
|
app.opts[:common_logger] = logger || app.opts[:common_logger] || $stderr
|
24
|
-
app.opts[:common_logger_meth] = app.opts[:common_logger].method(logger.respond_to?(:write) ? :write : :<<)
|
25
|
+
app.opts[:common_logger_meth] = app.opts[:common_logger].method(opts.fetch(:method){logger.respond_to?(:write) ? :write : :<<})
|
25
26
|
end
|
26
27
|
|
27
28
|
if RUBY_VERSION >= '2.1'
|
@@ -23,30 +23,31 @@ class Roda
|
|
23
23
|
module DefaultHeaders
|
24
24
|
# Merge the given headers into the existing default headers, if any.
|
25
25
|
def self.configure(app, headers={})
|
26
|
-
|
26
|
+
app.opts[:default_headers] = (app.default_headers || app::RodaResponse::DEFAULT_HEADERS).merge(headers).freeze
|
27
|
+
end
|
28
|
+
|
29
|
+
module ClassMethods
|
30
|
+
# The default response headers to use for the current class.
|
31
|
+
def default_headers
|
32
|
+
opts[:default_headers]
|
33
|
+
end
|
27
34
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
if
|
32
|
-
|
33
|
-
|
35
|
+
# Optimize the response class set_default_headers method if it hasn't been
|
36
|
+
# overridden and all default headers are strings.
|
37
|
+
def freeze
|
38
|
+
if (headers = opts[:default_headers]).all?{|k, v| k.is_a?(String) && v.is_a?(String)} &&
|
39
|
+
(self::RodaResponse.instance_method(:set_default_headers).owner == Base::ResponseMethods)
|
40
|
+
self::RodaResponse.class_eval(<<-END, __FILE__, __LINE__+1)
|
34
41
|
private
|
35
42
|
|
36
|
-
alias set_default_headers set_default_headers
|
37
43
|
def set_default_headers
|
38
44
|
h = @headers
|
39
45
|
#{headers.map{|k,v| "h[#{k.inspect}] ||= #{v.inspect}"}.join('; ')}
|
40
46
|
end
|
41
47
|
END
|
42
48
|
end
|
43
|
-
end
|
44
|
-
end
|
45
49
|
|
46
|
-
|
47
|
-
# The default response headers to use for the current class.
|
48
|
-
def default_headers
|
49
|
-
opts[:default_headers]
|
50
|
+
super
|
50
51
|
end
|
51
52
|
end
|
52
53
|
|
@@ -0,0 +1,156 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
class Roda
|
5
|
+
module RodaPlugins
|
6
|
+
# The host_authorization plugin allows configuring an authorized host or
|
7
|
+
# an array of authorized hosts. Then in the routing tree, you can check
|
8
|
+
# whether the request uses an authorized host via the +check_host_authorized!+
|
9
|
+
# method.
|
10
|
+
#
|
11
|
+
# If the request doesn't match one of the authorized hosts, the
|
12
|
+
# request processing stops at that point. Using this plugin can prevent
|
13
|
+
# DNS rebinding attacks if the application can receive requests for
|
14
|
+
# arbitrary hosts.
|
15
|
+
#
|
16
|
+
# By default, an empty response using status 403 will be returned for requests
|
17
|
+
# with unauthorized hosts.
|
18
|
+
#
|
19
|
+
# Because +check_host_authorized!+ is an instance method, you can easily choose
|
20
|
+
# to only check for authorization in certain routes, or to check it after
|
21
|
+
# other processing. For example, you could check for authorized hosts after
|
22
|
+
# serving static files, since the serving of static files should not be
|
23
|
+
# vulnerable to DNS rebinding attacks.
|
24
|
+
#
|
25
|
+
# = Usage
|
26
|
+
#
|
27
|
+
# In your routing tree, call the +check_host_authorized!+ method at the point you
|
28
|
+
# want to check for authorized hosts:
|
29
|
+
#
|
30
|
+
# plugin :host_authorization, 'www.example.com'
|
31
|
+
# plugin :public
|
32
|
+
#
|
33
|
+
# route do |r|
|
34
|
+
# r.public
|
35
|
+
# check_host_authorized!
|
36
|
+
#
|
37
|
+
# # ...
|
38
|
+
# end
|
39
|
+
#
|
40
|
+
# = Specifying authorized hosts
|
41
|
+
#
|
42
|
+
# For applications hosted on a single domain name, you can use a single string:
|
43
|
+
#
|
44
|
+
# plugin :host_authorization, 'www.example.com'
|
45
|
+
#
|
46
|
+
# For applications hosted on multiple domain names, you can use an array of strings:
|
47
|
+
#
|
48
|
+
# plugin :host_authorization, %w'www.example.com www.example2.com'
|
49
|
+
#
|
50
|
+
# For applications supporting arbitrary subdomains, you can use a regexp. If using
|
51
|
+
# a regexp, make sure you use <tt>\A<tt> and <tt>\z</tt> in your regexp, and restrict
|
52
|
+
# the allowed characters to the minimum required, otherwise you can potentionally
|
53
|
+
# introduce a security issue:
|
54
|
+
#
|
55
|
+
# plugin :host_authorization, /\A[-0-9a-f]+\.example\.com\z/
|
56
|
+
#
|
57
|
+
# For applications with more complex requirements, you can use a proc. Similarly
|
58
|
+
# to the regexp case, the proc should be aware the host contains user-submitted
|
59
|
+
# values, and not assume it is in any particular format:
|
60
|
+
#
|
61
|
+
# plugin :host_authorization, proc{|host| ExternalService.allowed_host?(host)}
|
62
|
+
#
|
63
|
+
# If an array of values is passed as the host argument, the host is authorized if
|
64
|
+
# it matches any value in the array. All host authorization checks use the
|
65
|
+
# <tt>===</tt> method, which is why it works for strings, regexps, and procs.
|
66
|
+
# It can also work with arbitrary objects that support <tt>===</tt>.
|
67
|
+
#
|
68
|
+
# For security reasons, only the +Host+ header is checked by default. If you are
|
69
|
+
# sure that your application is being run behind a forwarding proxy that sets the
|
70
|
+
# <tt>X-Forwarded-Host</tt> header, you should enable support for checking that
|
71
|
+
# header using the +:check_forwarded+ option:
|
72
|
+
#
|
73
|
+
# plugin :host_authorization, 'www.example.com', check_forwarded: true
|
74
|
+
#
|
75
|
+
# In this case, the trailing host in the <tt>X-Forwarded-Host</tt> header is checked,
|
76
|
+
# which should be the host set by the forwarding proxy closest to the application.
|
77
|
+
# In cases where multiple forwarding proxies are used that append to the
|
78
|
+
# <tt>X-Forwarded-Host</tt> header, you should not use this plugin.
|
79
|
+
#
|
80
|
+
# = Customizing behavior
|
81
|
+
#
|
82
|
+
# By default, an unauthorized host will receive an empty 403 response. You can
|
83
|
+
# customize this by passing a block when loading the plugin. For example, for
|
84
|
+
# sites using the render plugin, you could return a page that uses your default
|
85
|
+
# layout:
|
86
|
+
#
|
87
|
+
# plugin :render
|
88
|
+
# plugin :host_authorization, 'www.example.com' do |r|
|
89
|
+
# response.status = 403
|
90
|
+
# view(:content=>"<h1>Forbidden</h1>")
|
91
|
+
# end
|
92
|
+
#
|
93
|
+
# The block passed to this plugin is treated as a match block.
|
94
|
+
module HostAuthorization
|
95
|
+
def self.configure(app, host, opts=OPTS, &block)
|
96
|
+
app.opts[:host_authorization_host] = host
|
97
|
+
app.opts[:host_authorization_check_forwarded] = opts[:check_forwarded] if opts.key?(:check_forwarded)
|
98
|
+
|
99
|
+
if block
|
100
|
+
app.define_roda_method(:host_authorization_unauthorized, 1, &block)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
module InstanceMethods
|
105
|
+
# Check whether the host is authorized. If not authorized, return a response
|
106
|
+
# immediately based on the plugin block.
|
107
|
+
def check_host_authorization!
|
108
|
+
r = @_request
|
109
|
+
return if host_authorized?(_convert_host_for_authorization(r.env["HTTP_HOST"].to_s.dup))
|
110
|
+
|
111
|
+
if opts[:host_authorization_check_forwarded] && (host = r.env["HTTP_X_FORWARDED_HOST"])
|
112
|
+
if i = host.rindex(',')
|
113
|
+
host = host[i+1, 10000000].to_s
|
114
|
+
end
|
115
|
+
host = _convert_host_for_authorization(host.strip)
|
116
|
+
|
117
|
+
if !host.empty? && host_authorized?(host)
|
118
|
+
return
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
r.on do
|
123
|
+
host_authorization_unauthorized(r)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
private
|
128
|
+
|
129
|
+
# Remove the port information from the passed string (mutates the passed argument).
|
130
|
+
def _convert_host_for_authorization(host)
|
131
|
+
host.sub!(/:\d+\z/, "")
|
132
|
+
host
|
133
|
+
end
|
134
|
+
|
135
|
+
# Whether the host given is one of the authorized hosts for this application.
|
136
|
+
def host_authorized?(host, authorized_host = opts[:host_authorization_host])
|
137
|
+
case authorized_host
|
138
|
+
when Array
|
139
|
+
authorized_host.any?{|auth_host| host_authorized?(host, auth_host)}
|
140
|
+
else
|
141
|
+
authorized_host === host
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
# Action to take for unauthorized hosts. Sets a 403 status by default.
|
146
|
+
def host_authorization_unauthorized(_)
|
147
|
+
@_response.status = 403
|
148
|
+
nil
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
register_plugin(:host_authorization, HostAuthorization)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
class Roda
|
5
|
+
module RodaPlugins
|
6
|
+
# The optimized_segment_matchers plugin adds two optimized matcher methods,
|
7
|
+
# +r.on_segment+ and +r.is_segment+. +r.on_segment+ is an optimized version of
|
8
|
+
# +r.on String+ that accepts no arguments and yields the next segment if there
|
9
|
+
# is a segment. +r.is_segment+ is an optimized version of +r.is String+ that accepts
|
10
|
+
# no arguments and yields the next segment only if it is the last segment.
|
11
|
+
#
|
12
|
+
# plugin :optimized_segment_matchers
|
13
|
+
#
|
14
|
+
# route do |r|
|
15
|
+
# r.on_segment do |x|
|
16
|
+
# # matches any segment (e.g. /a, /b, but not /)
|
17
|
+
# r.is_segment do |y|
|
18
|
+
# # matches only if final segment (e.g. /a/b, /b/c, but not /c, /c/d/, /c/d/e)
|
19
|
+
# end
|
20
|
+
# end
|
21
|
+
# end
|
22
|
+
module OptimizedSegmentMatchers
|
23
|
+
module RequestMethods
|
24
|
+
# Optimized version of +r.on String+ that yields the next segment if there
|
25
|
+
# is a segment.
|
26
|
+
def on_segment
|
27
|
+
rp = @remaining_path
|
28
|
+
if rp.getbyte(0) == 47
|
29
|
+
if last = rp.index('/', 1)
|
30
|
+
@remaining_path = rp[last, rp.length]
|
31
|
+
always{yield rp[1, last-1]}
|
32
|
+
elsif (len = rp.length) > 1
|
33
|
+
@remaining_path = ""
|
34
|
+
always{yield rp[1, len]}
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Optimized version of +r.is String+ that yields the next segment only if it
|
40
|
+
# is the final segment.
|
41
|
+
def is_segment
|
42
|
+
rp = @remaining_path
|
43
|
+
if rp.getbyte(0) == 47 && !rp.index('/', 1) && (len = rp.length) > 1
|
44
|
+
@remaining_path = ""
|
45
|
+
always{yield rp[1, len]}
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
register_plugin(:optimized_segment_matchers, OptimizedSegmentMatchers)
|
52
|
+
end
|
53
|
+
end
|
@@ -19,7 +19,8 @@ class Roda
|
|
19
19
|
# end
|
20
20
|
# end
|
21
21
|
#
|
22
|
-
#
|
22
|
+
# If you are using the placeholder_string_matchers plugin, note
|
23
|
+
# that both of these methods only work with plain strings, not
|
23
24
|
# with strings with embedded colons for capturing. Matching will work
|
24
25
|
# correctly in such cases, but the captures will not be yielded to the
|
25
26
|
# match blocks.
|
@@ -0,0 +1,107 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
class Roda
|
5
|
+
module RodaPlugins
|
6
|
+
# The recheck_precompiled_assets plugin enables checking for the precompiled asset metadata file.
|
7
|
+
# You need to have already loaded the assets plugin with the +:precompiled+ option and the file
|
8
|
+
# specified by the +:precompiled+ option must already exist in order to use the
|
9
|
+
# recheck_precompiled_assets plugin.
|
10
|
+
#
|
11
|
+
# Any time you want to check whether the precompiled asset metadata file has changed and should be
|
12
|
+
# reloaded, you can call the +recheck_precompiled_assets+ class method. This method will check
|
13
|
+
# whether the file has changed, and reload it if it has. If you want to check for modifications on
|
14
|
+
# every request, you can use +self.class.recheck_precompiled_assets+ inside your route block.
|
15
|
+
module RecheckPrecompiledAssets
|
16
|
+
# Thread safe wrapper for the compiled asset metadata hash. Does not wrap all
|
17
|
+
# hash methods, only a few that are used.
|
18
|
+
class CompiledAssetsHash
|
19
|
+
include Enumerable
|
20
|
+
|
21
|
+
def initialize
|
22
|
+
@hash = {}
|
23
|
+
@mutex = Mutex.new
|
24
|
+
end
|
25
|
+
|
26
|
+
def [](key)
|
27
|
+
@mutex.synchronize{@hash[key]}
|
28
|
+
end
|
29
|
+
|
30
|
+
def []=(key, value)
|
31
|
+
@mutex.synchronize{@hash[key] = value}
|
32
|
+
end
|
33
|
+
|
34
|
+
def replace(hash)
|
35
|
+
hash = hash.instance_variable_get(:@hash) if (CompiledAssetsHash === hash)
|
36
|
+
@mutex.synchronize{@hash.replace(hash)}
|
37
|
+
self
|
38
|
+
end
|
39
|
+
|
40
|
+
def each(&block)
|
41
|
+
@mutex.synchronize{@hash.dup}.each(&block)
|
42
|
+
self
|
43
|
+
end
|
44
|
+
|
45
|
+
def to_json(*args)
|
46
|
+
@mutex.synchronize{@hash.dup}.to_json(*args)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.load_dependencies(app)
|
51
|
+
unless app.respond_to?(:assets_opts) && app.assets_opts[:precompiled]
|
52
|
+
raise RodaError, "must load assets plugin with precompiled option before loading recheck_precompiled_assets plugin"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.configure(app)
|
57
|
+
precompiled_file = app.assets_opts[:precompiled]
|
58
|
+
prev_mtime = ::File.mtime(precompiled_file)
|
59
|
+
app.instance_exec do
|
60
|
+
opts[:assets] = opts[:assets].merge(:compiled=>_compiled_assets_initial_hash.replace(assets_opts[:compiled])).freeze
|
61
|
+
|
62
|
+
define_singleton_method(:recheck_precompiled_assets) do
|
63
|
+
new_mtime = ::File.mtime(precompiled_file)
|
64
|
+
if new_mtime != prev_mtime
|
65
|
+
prev_mtime = new_mtime
|
66
|
+
assets_opts[:compiled].replace(_precompiled_asset_metadata(precompiled_file))
|
67
|
+
|
68
|
+
# Unset the cached asset matchers, so new ones will be generated.
|
69
|
+
# This is needed in case the new precompiled metadata uses
|
70
|
+
# different files.
|
71
|
+
app::RodaRequest.instance_variable_set(:@assets_matchers, nil)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
singleton_class.send(:alias_method, :recheck_precompiled_assets, :recheck_precompiled_assets)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
module ClassMethods
|
79
|
+
private
|
80
|
+
|
81
|
+
# Wrap the precompiled asset metadata in a thread-safe hash.
|
82
|
+
def _precompiled_asset_metadata(file)
|
83
|
+
CompiledAssetsHash.new.replace(super)
|
84
|
+
end
|
85
|
+
|
86
|
+
# Use a thread-safe wrapper of a hash for the :compiled assets option, since
|
87
|
+
# the recheck_precompiled_asset_metadata can modify it at runtime.
|
88
|
+
def _compiled_assets_initial_hash
|
89
|
+
CompiledAssetsHash.new
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
module RequestClassMethods
|
94
|
+
private
|
95
|
+
|
96
|
+
# Use a regexp that matches any digest. When the precompiled asset metadata
|
97
|
+
# file is updated, this allows requests for a previous precompiled asset to
|
98
|
+
# still work.
|
99
|
+
def _asset_regexp(type, key, _)
|
100
|
+
/#{Regexp.escape(key.sub(/\A#{type}/, ''))}\.[0-9a-fA-F]+\.#{type}/
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
register_plugin(:recheck_precompiled_assets, RecheckPrecompiledAssets)
|
106
|
+
end
|
107
|
+
end
|
data/lib/roda/plugins/render.rb
CHANGED
@@ -183,6 +183,7 @@ class Roda
|
|
183
183
|
app.const_set(:RodaCompiledTemplates, compiled_templates_module)
|
184
184
|
end
|
185
185
|
opts[:template_method_cache] = orig_method_cache || (opts[:cache_class] || RodaCache).new
|
186
|
+
opts[:template_method_cache][:_roda_layout] = nil if opts[:template_method_cache][:_roda_layout]
|
186
187
|
opts[:cache] = orig_cache || (opts[:cache_class] || RodaCache).new
|
187
188
|
|
188
189
|
opts[:layout_opts] = (opts[:layout_opts] || {}).dup
|
@@ -387,6 +388,8 @@ class Roda
|
|
387
388
|
instance.send(:retrieve_template, :template=>layout_template, :cache_key=>nil, :template_method_cache_key => :_roda_layout)
|
388
389
|
layout_method = opts[:render][:template_method_cache][:_roda_layout]
|
389
390
|
define_method(:_layout_method){layout_method}
|
391
|
+
private :_layout_method
|
392
|
+
alias_method(:_layout_method, :_layout_method)
|
390
393
|
opts[:render] = opts[:render].merge(:optimized_layout_method_created=>true)
|
391
394
|
end
|
392
395
|
end
|
@@ -415,10 +418,8 @@ class Roda
|
|
415
418
|
# Render the given template. If there is a default layout
|
416
419
|
# for the class, take the result of the template rendering
|
417
420
|
# and render it inside the layout. See Render for details.
|
418
|
-
def view(template, opts = (
|
419
|
-
if
|
420
|
-
content = send(optimized_template, OPTS)
|
421
|
-
|
421
|
+
def view(template, opts = (content = _optimized_view_content(template); OPTS))
|
422
|
+
if content
|
422
423
|
# First, check if the optimized layout method has already been created,
|
423
424
|
# and use it if so. This way avoids the extra conditional and local variable
|
424
425
|
# assignments in the next section.
|
@@ -518,6 +519,16 @@ class Roda
|
|
518
519
|
end
|
519
520
|
end
|
520
521
|
end
|
522
|
+
|
523
|
+
# Get the content for #view, or return nil to use the unoptimized approach. Only called if
|
524
|
+
# a single argument is passed to view.
|
525
|
+
def _optimized_view_content(template)
|
526
|
+
if optimized_template = _cached_template_method(template)
|
527
|
+
send(optimized_template, OPTS)
|
528
|
+
elsif template.is_a?(Hash) && template.length == 1
|
529
|
+
template[:content]
|
530
|
+
end
|
531
|
+
end
|
521
532
|
else
|
522
533
|
# :nocov:
|
523
534
|
def _cached_template_method(_)
|
@@ -535,6 +546,10 @@ class Roda
|
|
535
546
|
def _optimized_render_method_for_locals(_, _)
|
536
547
|
nil
|
537
548
|
end
|
549
|
+
|
550
|
+
def _optimized_view_content(template)
|
551
|
+
nil
|
552
|
+
end
|
538
553
|
# :nocov:
|
539
554
|
end
|
540
555
|
|
@@ -211,6 +211,10 @@ class Roda
|
|
211
211
|
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
212
212
|
# OTHER DEALINGS IN THE SOFTWARE.
|
213
213
|
module SinatraHelpers
|
214
|
+
UTF8_ENCODING = Encoding.find('UTF-8')
|
215
|
+
ISO88591_ENCODING = Encoding.find('ISO-8859-1')
|
216
|
+
BINARY_ENCODING = Encoding.find('BINARY')
|
217
|
+
|
214
218
|
# Depend on the status_303 plugin.
|
215
219
|
def self.load_dependencies(app, _opts = nil)
|
216
220
|
app.plugin :status_303
|
@@ -432,7 +436,25 @@ class Roda
|
|
432
436
|
# instructing the user agents to prompt to save.
|
433
437
|
def attachment(filename = nil, disposition='attachment')
|
434
438
|
if filename
|
435
|
-
|
439
|
+
param_filename = File.basename(filename)
|
440
|
+
encoding = param_filename.encoding
|
441
|
+
|
442
|
+
needs_encoding = param_filename.gsub!(/[^ 0-9a-zA-Z!\#$&\+\.\^_`\|~]+/, '-')
|
443
|
+
params = "; filename=#{param_filename.inspect}"
|
444
|
+
|
445
|
+
if needs_encoding && (encoding == UTF8_ENCODING || encoding == ISO88591_ENCODING)
|
446
|
+
# File name contains non attr-char characters from RFC 5987 Section 3.2.1
|
447
|
+
|
448
|
+
encoded_filename = File.basename(filename).force_encoding(BINARY_ENCODING)
|
449
|
+
# Similar regexp as above, but treat each byte separately, and encode
|
450
|
+
# space characters, since those aren't allowed in attr-char
|
451
|
+
encoded_filename.gsub!(/[^0-9a-zA-Z!\#$&\+\.\^_`\|~]/) do |c|
|
452
|
+
"%%%X" % c.ord
|
453
|
+
end
|
454
|
+
|
455
|
+
encoded_params = "; filename*=#{encoding.to_s}''#{encoded_filename}"
|
456
|
+
end
|
457
|
+
|
436
458
|
unless @headers["Content-Type"]
|
437
459
|
ext = File.extname(filename)
|
438
460
|
unless ext.empty?
|
@@ -440,7 +462,7 @@ class Roda
|
|
440
462
|
end
|
441
463
|
end
|
442
464
|
end
|
443
|
-
@headers["Content-Disposition"] = "#{disposition}#{params}"
|
465
|
+
@headers["Content-Disposition"] = "#{disposition}#{params}#{encoded_params}"
|
444
466
|
end
|
445
467
|
|
446
468
|
# Whether or not the status is set to 1xx. Returns nil if status not yet set.
|
data/lib/roda/request.rb
CHANGED
@@ -468,8 +468,8 @@ class Roda
|
|
468
468
|
if last = rp.index('/', 1)
|
469
469
|
@captures << rp[1, last-1]
|
470
470
|
@remaining_path = rp[last, rp.length]
|
471
|
-
elsif rp.length > 1
|
472
|
-
@captures << rp[1,
|
471
|
+
elsif (len = rp.length) > 1
|
472
|
+
@captures << rp[1, len]
|
473
473
|
@remaining_path = ""
|
474
474
|
end
|
475
475
|
end
|
data/lib/roda/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: roda
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.
|
4
|
+
version: 3.44.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jeremy Evans
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-
|
11
|
+
date: 2021-05-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rack
|
@@ -211,6 +211,10 @@ extra_rdoc_files:
|
|
211
211
|
- doc/release_notes/3.39.0.txt
|
212
212
|
- doc/release_notes/3.4.0.txt
|
213
213
|
- doc/release_notes/3.40.0.txt
|
214
|
+
- doc/release_notes/3.41.0.txt
|
215
|
+
- doc/release_notes/3.42.0.txt
|
216
|
+
- doc/release_notes/3.43.0.txt
|
217
|
+
- doc/release_notes/3.44.0.txt
|
214
218
|
- doc/release_notes/3.5.0.txt
|
215
219
|
- doc/release_notes/3.6.0.txt
|
216
220
|
- doc/release_notes/3.7.0.txt
|
@@ -258,6 +262,10 @@ files:
|
|
258
262
|
- doc/release_notes/3.39.0.txt
|
259
263
|
- doc/release_notes/3.4.0.txt
|
260
264
|
- doc/release_notes/3.40.0.txt
|
265
|
+
- doc/release_notes/3.41.0.txt
|
266
|
+
- doc/release_notes/3.42.0.txt
|
267
|
+
- doc/release_notes/3.43.0.txt
|
268
|
+
- doc/release_notes/3.44.0.txt
|
261
269
|
- doc/release_notes/3.5.0.txt
|
262
270
|
- doc/release_notes/3.6.0.txt
|
263
271
|
- doc/release_notes/3.7.0.txt
|
@@ -308,6 +316,7 @@ files:
|
|
308
316
|
- lib/roda/plugins/header_matchers.rb
|
309
317
|
- lib/roda/plugins/heartbeat.rb
|
310
318
|
- lib/roda/plugins/hooks.rb
|
319
|
+
- lib/roda/plugins/host_authorization.rb
|
311
320
|
- lib/roda/plugins/indifferent_params.rb
|
312
321
|
- lib/roda/plugins/json.rb
|
313
322
|
- lib/roda/plugins/json_parser.rb
|
@@ -326,6 +335,7 @@ files:
|
|
326
335
|
- lib/roda/plugins/named_templates.rb
|
327
336
|
- lib/roda/plugins/not_allowed.rb
|
328
337
|
- lib/roda/plugins/not_found.rb
|
338
|
+
- lib/roda/plugins/optimized_segment_matchers.rb
|
329
339
|
- lib/roda/plugins/optimized_string_matchers.rb
|
330
340
|
- lib/roda/plugins/padrino_render.rb
|
331
341
|
- lib/roda/plugins/param_matchers.rb
|
@@ -339,6 +349,7 @@ files:
|
|
339
349
|
- lib/roda/plugins/precompile_templates.rb
|
340
350
|
- lib/roda/plugins/public.rb
|
341
351
|
- lib/roda/plugins/r.rb
|
352
|
+
- lib/roda/plugins/recheck_precompiled_assets.rb
|
342
353
|
- lib/roda/plugins/relative_path.rb
|
343
354
|
- lib/roda/plugins/render.rb
|
344
355
|
- lib/roda/plugins/render_each.rb
|
@@ -396,7 +407,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
396
407
|
- !ruby/object:Gem::Version
|
397
408
|
version: '0'
|
398
409
|
requirements: []
|
399
|
-
rubygems_version: 3.2.
|
410
|
+
rubygems_version: 3.2.15
|
400
411
|
signing_key:
|
401
412
|
specification_version: 4
|
402
413
|
summary: Routing tree web toolkit
|