roda 3.41.0 → 3.45.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/CHANGELOG +30 -0
- data/README.rdoc +11 -13
- 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/doc/release_notes/3.45.0.txt +22 -0
- data/lib/roda.rb +6 -0
- data/lib/roda/plugins/assets.rb +47 -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 +2 -0
- data/lib/roda/plugins/sinatra_helpers.rb +24 -2
- data/lib/roda/plugins/typecast_params.rb +28 -1
- 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: 5dbdf55425a681a0cb32fa0b4a44cd50bf5be6f85c78c1ea1adbdd19e28c2d61
|
4
|
+
data.tar.gz: e91ee695e5f8648f4269a2c229b794ef2ee921f4291102519d4ca2cadd9bf13a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: aa46429e09091c3cf0112c251a9863845c69d4245674dcaddaec030244213d8b9ac1aae96cfa6093680c33f87c732ee4464e4cbe2572d192eb0a9f7e09acd43b
|
7
|
+
data.tar.gz: 57cf84f7db6e6334918f0c01f7308c4c48db7e0ba9f2a14fe87420c3b2bd17471a229ec02a2ad9242ec6fd4aa67848f6dff04b319fea361a985dd264b52252ab
|
data/CHANGELOG
CHANGED
@@ -1,3 +1,33 @@
|
|
1
|
+
= 3.45.0 (2021-06-14)
|
2
|
+
|
3
|
+
* Make typecast_params plugin check for null bytes in strings by default, with :allow_null_bytes option for previous behavior (jeremyevans)
|
4
|
+
|
5
|
+
= 3.44.0 (2021-05-12)
|
6
|
+
|
7
|
+
* Add optimized_segment_matchers plugin for optimized matchers for a single String class argument (jeremyevans)
|
8
|
+
|
9
|
+
* Use RFC 5987 UTF-8 and ISO-8859-1 encoded filenames when using send_file and attachment in the sinatra_helpers plugin (jeremyevans)
|
10
|
+
|
11
|
+
= 3.43.1 (2021-04-13)
|
12
|
+
|
13
|
+
* [SECURITY] Fix issue where loading content_security_policy plugin after default_headers plugin had no effect (jeremyevans)
|
14
|
+
|
15
|
+
= 3.43.0 (2021-04-12)
|
16
|
+
|
17
|
+
* Add host_authorization plugin, for checking that requests are submitted using an approved host (jeremyevans)
|
18
|
+
|
19
|
+
= 3.42.0 (2021-03-12)
|
20
|
+
|
21
|
+
* Make Roda.plugin support plugins using keyword arguments in Ruby 3 (jeremyevans)
|
22
|
+
|
23
|
+
* Make Roda.use support middleware using keyword arguments in Ruby 3 (pat) (#207)
|
24
|
+
|
25
|
+
* Support common_logger plugin :method option for specifying the method to call on the logger (fnordfish, jeremyevans) (#206)
|
26
|
+
|
27
|
+
* Add recheck_precompiled_assets plugin for checking for updates to the precompiled asset metadata file (jeremyevans)
|
28
|
+
|
29
|
+
* Make compile_assets class method in assets plugin use an atomic approach to writing precompiled metadata file (jeremyevans)
|
30
|
+
|
1
31
|
= 3.41.0 (2021-02-17)
|
2
32
|
|
3
33
|
* Improve view performance with :content option up to 3x by calling compiled template methods directly (jeremyevans)
|
data/README.rdoc
CHANGED
@@ -965,19 +965,17 @@ option. If you really want to turn path checking off, you can do so via the
|
|
965
965
|
Roda does not ship with integrated support for code reloading, but there are rack-based
|
966
966
|
reloaders that will work with Roda apps.
|
967
967
|
|
968
|
-
|
969
|
-
|
970
|
-
|
971
|
-
|
972
|
-
rack-unreloader
|
973
|
-
|
974
|
-
|
975
|
-
|
976
|
-
|
977
|
-
|
978
|
-
|
979
|
-
does not rely on autoloading then +require_dependency+ must be used to require the dependencies
|
980
|
-
or they won't be reloaded.
|
968
|
+
{Zeitwerk}[https://github.com/fxn/zeitwerk] (which Rails now uses for reloading) can be used
|
969
|
+
with Roda. It requires minimal setup and handles most cases. It overrides +require+ when
|
970
|
+
activated. If it can meet the needs of your application, it's probably the best approach.
|
971
|
+
|
972
|
+
{rack-unreloader}[https://github.com/jeremyevans/rack-unreloader] uses a fast
|
973
|
+
approach to reloading while still being fairly safe, as it only reloads files that have
|
974
|
+
been modified, and unloads constants defined in the files before reloading them. It can handle
|
975
|
+
advanced cases that Zeitwerk does not support, such as classes defined in multiple files
|
976
|
+
(common when using separate route files for different routing branches in the same application).
|
977
|
+
However, rack-unreloader does not modify core classes and using it requires modifying your
|
978
|
+
application code to use rack-unreloader specific APIs, which may not be simple.
|
981
979
|
|
982
980
|
{AutoReloader}[https://github.com/rosenfeld/auto_reloader] provides transparent reloading for
|
983
981
|
all files reached from one of the +reloadable_paths+ option entries, by detecting new top-level
|
@@ -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.
|
@@ -0,0 +1,22 @@
|
|
1
|
+
= Improvements
|
2
|
+
|
3
|
+
* The typecast_params plugin checks now checks for null bytes by
|
4
|
+
default before typecasting. If null bytes are present, it raises
|
5
|
+
an error. Most applications do not require null bytes in
|
6
|
+
parameters, and in some cases allowing them can lead to security
|
7
|
+
issues, especially when parameters are passed to C extensions.
|
8
|
+
In general, the benefit of forbidding null bytes in parameters is
|
9
|
+
greater than the cost.
|
10
|
+
|
11
|
+
If you would like to continue allowing null bytes, use the
|
12
|
+
:allow_null_bytes option when loading the plugin.
|
13
|
+
|
14
|
+
Note that this change does not affect uploaded files, since those
|
15
|
+
are expected to contain null bytes.
|
16
|
+
|
17
|
+
= Backwards Compatibility
|
18
|
+
|
19
|
+
* The change to the typecast_params plugin to raise an error for
|
20
|
+
null bytes can break applications that are expecting null bytes
|
21
|
+
to be passed in parameters. Such applications should use the
|
22
|
+
:allow_null_bytes option when loading the plugin.
|
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
@@ -152,6 +152,22 @@ class Roda
|
|
152
152
|
# together and not compressed during compilation. You can use the
|
153
153
|
# :css_compressor and :js_compressor options to specify the compressor to use.
|
154
154
|
#
|
155
|
+
# It is also possible to use the built-in compression options in the CSS or JS
|
156
|
+
# compiler, assuming the compiler supports such options. For example, with
|
157
|
+
# sass/sassc, you can use:
|
158
|
+
#
|
159
|
+
# plugin :assets,
|
160
|
+
# css_opts: {style: :compressed}
|
161
|
+
#
|
162
|
+
# === Source Maps (CSS)
|
163
|
+
#
|
164
|
+
# The assets plugin does not have direct support for source maps, so it is
|
165
|
+
# recommended you use embedded source maps if supported by the CSS compiler.
|
166
|
+
# For sass/sassc, you can use:
|
167
|
+
#
|
168
|
+
# plugin :assets,
|
169
|
+
# css_opts: {:source_map_embed=>true, source_map_contents: true, source_map_file: "."}
|
170
|
+
#
|
155
171
|
# === With Asset Groups
|
156
172
|
#
|
157
173
|
# When using asset groups, a separate compiled file will be produced per
|
@@ -376,7 +392,7 @@ class Roda
|
|
376
392
|
|
377
393
|
if opts[:precompiled] && !opts[:compiled] && ::File.exist?(opts[:precompiled])
|
378
394
|
require 'json'
|
379
|
-
opts[:compiled] =
|
395
|
+
opts[:compiled] = app.send(:_precompiled_asset_metadata, opts[:precompiled])
|
380
396
|
end
|
381
397
|
|
382
398
|
if opts[:early_hints]
|
@@ -455,7 +471,7 @@ class Roda
|
|
455
471
|
require 'fileutils'
|
456
472
|
|
457
473
|
unless assets_opts[:compiled]
|
458
|
-
opts[:assets] = assets_opts.merge(:compiled =>
|
474
|
+
opts[:assets] = assets_opts.merge(:compiled => _compiled_assets_initial_hash).freeze
|
459
475
|
end
|
460
476
|
|
461
477
|
if type == nil
|
@@ -465,10 +481,12 @@ class Roda
|
|
465
481
|
_compile_assets(type)
|
466
482
|
end
|
467
483
|
|
468
|
-
if assets_opts[:precompiled]
|
484
|
+
if precompile_file = assets_opts[:precompiled]
|
469
485
|
require 'json'
|
470
|
-
::FileUtils.mkdir_p(File.dirname(
|
471
|
-
|
486
|
+
::FileUtils.mkdir_p(File.dirname(precompile_file))
|
487
|
+
tmp_file = "#{precompile_file}.tmp"
|
488
|
+
::File.open(tmp_file, 'wb'){|f| f.write((opts[:json_serializer] || :to_json.to_proc).call(assets_opts[:compiled]))}
|
489
|
+
::File.rename(tmp_file, precompile_file)
|
472
490
|
end
|
473
491
|
|
474
492
|
assets_opts[:compiled]
|
@@ -476,6 +494,11 @@ class Roda
|
|
476
494
|
|
477
495
|
private
|
478
496
|
|
497
|
+
# The initial hash to use to store compiled asset metadata.
|
498
|
+
def _compiled_assets_initial_hash
|
499
|
+
{}
|
500
|
+
end
|
501
|
+
|
479
502
|
# Internals of compile_assets, handling recursive calls for loading
|
480
503
|
# all asset groups under the given type.
|
481
504
|
def _compile_assets(type)
|
@@ -493,6 +516,11 @@ class Roda
|
|
493
516
|
end
|
494
517
|
end
|
495
518
|
|
519
|
+
# The precompiled asset metadata stored in the given file
|
520
|
+
def _precompiled_asset_metadata(file)
|
521
|
+
(opts[:json_parser] || ::JSON.method(:parse)).call(::File.read(file))
|
522
|
+
end
|
523
|
+
|
496
524
|
# Compile each array of files for the given type into a single
|
497
525
|
# file. Dirs should be an array of asset group names, if these
|
498
526
|
# are files in an asset group.
|
@@ -794,23 +822,32 @@ class Roda
|
|
794
822
|
# handled.
|
795
823
|
def assets_matchers
|
796
824
|
@assets_matchers ||= [:css, :js].map do |t|
|
797
|
-
|
825
|
+
if regexp = assets_regexp(t)
|
826
|
+
[t, regexp].freeze
|
827
|
+
end
|
798
828
|
end.compact.freeze
|
799
829
|
end
|
800
830
|
|
801
831
|
private
|
802
832
|
|
833
|
+
# A string for the asset filename for the asset type, key, and digest.
|
834
|
+
def _asset_regexp(type, key, digest)
|
835
|
+
"#{key.sub(/\A#{type}/, '')}.#{digest}.#{type}"
|
836
|
+
end
|
837
|
+
|
803
838
|
# The regexp matcher to use for the given type. This handles any asset groups
|
804
839
|
# for the asset types.
|
805
840
|
def assets_regexp(type)
|
806
841
|
o = roda_class.assets_opts
|
807
842
|
if compiled = o[:compiled]
|
808
|
-
assets = compiled.
|
809
|
-
|
810
|
-
|
843
|
+
assets = compiled.
|
844
|
+
select{|k,_| k =~ /\A#{type}/}.
|
845
|
+
map{|k, md| _asset_regexp(type, k, md)}
|
846
|
+
return if assets.empty?
|
811
847
|
/#{o[:"compiled_#{type}_prefix"]}(#{Regexp.union(assets)})/
|
812
848
|
else
|
813
|
-
assets =
|
849
|
+
return unless assets = o[type]
|
850
|
+
assets = unnest_assets_hash(assets)
|
814
851
|
ts = o[:timestamp_paths]
|
815
852
|
/#{o[:"#{type}_prefix"]}#{"\\d+#{ts}" if ts}(#{Regexp.union(assets.uniq)})#{o[:"#{type}_suffix"]}/
|
816
853
|
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
@@ -388,6 +388,8 @@ class Roda
|
|
388
388
|
instance.send(:retrieve_template, :template=>layout_template, :cache_key=>nil, :template_method_cache_key => :_roda_layout)
|
389
389
|
layout_method = opts[:render][:template_method_cache][:_roda_layout]
|
390
390
|
define_method(:_layout_method){layout_method}
|
391
|
+
private :_layout_method
|
392
|
+
alias_method(:_layout_method, :_layout_method)
|
391
393
|
opts[:render] = opts[:render].merge(:optimized_layout_method_created=>true)
|
392
394
|
end
|
393
395
|
end
|
@@ -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.
|
@@ -260,6 +260,11 @@ class Roda
|
|
260
260
|
# strip leading and trailing whitespace from parameter string values before processing, which
|
261
261
|
# you can do by passing the <tt>strip: :all</tt> option when loading the plugin.
|
262
262
|
#
|
263
|
+
# By default, the typecast_params conversion procs check that null bytes are not allowed
|
264
|
+
# in param string values. This check for null bytes occurs prior to any type conversion.
|
265
|
+
# If you would like to skip this check and allow null bytes in param string values,
|
266
|
+
# you can do by passing the <tt>:allow_null_bytes</tt> option when loading the plugin.
|
267
|
+
#
|
263
268
|
# By design, typecast_params only deals with string keys, it is not possible to use
|
264
269
|
# symbol keys as arguments to the conversion methods and have them converted.
|
265
270
|
module TypecastParams
|
@@ -356,6 +361,14 @@ class Roda
|
|
356
361
|
end
|
357
362
|
end
|
358
363
|
|
364
|
+
module AllowNullByte
|
365
|
+
private
|
366
|
+
|
367
|
+
# Allow ASCII NUL bytes ("\0") in parameter string values.
|
368
|
+
def check_null_byte(v)
|
369
|
+
end
|
370
|
+
end
|
371
|
+
|
359
372
|
module StringStripper
|
360
373
|
private
|
361
374
|
|
@@ -391,7 +404,10 @@ class Roda
|
|
391
404
|
convert_array_meth = :"_convert_array_#{type}"
|
392
405
|
define_method(convert_array_meth) do |v|
|
393
406
|
raise Error, "expected array but received #{v.inspect}" unless v.is_a?(Array)
|
394
|
-
v.map!
|
407
|
+
v.map! do |val|
|
408
|
+
check_null_byte(val)
|
409
|
+
send(convert_meth, val)
|
410
|
+
end
|
395
411
|
end
|
396
412
|
|
397
413
|
private convert_meth, convert_array_meth
|
@@ -927,12 +943,20 @@ class Roda
|
|
927
943
|
end
|
928
944
|
end
|
929
945
|
|
946
|
+
# Raise an Error if the value is a string containing a null byte.
|
947
|
+
def check_null_byte(v)
|
948
|
+
if v.is_a?(String) && v.index("\0")
|
949
|
+
handle_error(nil, :null_byte, "string parameter contains null byte", true)
|
950
|
+
end
|
951
|
+
end
|
952
|
+
|
930
953
|
# Get the value of +key+ for the object, and convert it to the expected type using +meth+.
|
931
954
|
# If the value either before or after conversion is nil, return the +default+ value.
|
932
955
|
def process(meth, key, default)
|
933
956
|
v = param_value(key)
|
934
957
|
|
935
958
|
unless v.nil?
|
959
|
+
check_null_byte(v)
|
936
960
|
v = send(meth, v)
|
937
961
|
end
|
938
962
|
|
@@ -992,6 +1016,9 @@ class Roda
|
|
992
1016
|
if opts[:strip] == :all
|
993
1017
|
app::TypecastParams.send(:include, StringStripper)
|
994
1018
|
end
|
1019
|
+
if opts[:allow_null_bytes]
|
1020
|
+
app::TypecastParams.send(:include, AllowNullByte)
|
1021
|
+
end
|
995
1022
|
end
|
996
1023
|
|
997
1024
|
module ClassMethods
|
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.45.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-06-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rack
|
@@ -212,6 +212,10 @@ extra_rdoc_files:
|
|
212
212
|
- doc/release_notes/3.4.0.txt
|
213
213
|
- doc/release_notes/3.40.0.txt
|
214
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
|
218
|
+
- doc/release_notes/3.45.0.txt
|
215
219
|
- doc/release_notes/3.5.0.txt
|
216
220
|
- doc/release_notes/3.6.0.txt
|
217
221
|
- doc/release_notes/3.7.0.txt
|
@@ -260,6 +264,10 @@ files:
|
|
260
264
|
- doc/release_notes/3.4.0.txt
|
261
265
|
- doc/release_notes/3.40.0.txt
|
262
266
|
- doc/release_notes/3.41.0.txt
|
267
|
+
- doc/release_notes/3.42.0.txt
|
268
|
+
- doc/release_notes/3.43.0.txt
|
269
|
+
- doc/release_notes/3.44.0.txt
|
270
|
+
- doc/release_notes/3.45.0.txt
|
263
271
|
- doc/release_notes/3.5.0.txt
|
264
272
|
- doc/release_notes/3.6.0.txt
|
265
273
|
- doc/release_notes/3.7.0.txt
|
@@ -310,6 +318,7 @@ files:
|
|
310
318
|
- lib/roda/plugins/header_matchers.rb
|
311
319
|
- lib/roda/plugins/heartbeat.rb
|
312
320
|
- lib/roda/plugins/hooks.rb
|
321
|
+
- lib/roda/plugins/host_authorization.rb
|
313
322
|
- lib/roda/plugins/indifferent_params.rb
|
314
323
|
- lib/roda/plugins/json.rb
|
315
324
|
- lib/roda/plugins/json_parser.rb
|
@@ -328,6 +337,7 @@ files:
|
|
328
337
|
- lib/roda/plugins/named_templates.rb
|
329
338
|
- lib/roda/plugins/not_allowed.rb
|
330
339
|
- lib/roda/plugins/not_found.rb
|
340
|
+
- lib/roda/plugins/optimized_segment_matchers.rb
|
331
341
|
- lib/roda/plugins/optimized_string_matchers.rb
|
332
342
|
- lib/roda/plugins/padrino_render.rb
|
333
343
|
- lib/roda/plugins/param_matchers.rb
|
@@ -341,6 +351,7 @@ files:
|
|
341
351
|
- lib/roda/plugins/precompile_templates.rb
|
342
352
|
- lib/roda/plugins/public.rb
|
343
353
|
- lib/roda/plugins/r.rb
|
354
|
+
- lib/roda/plugins/recheck_precompiled_assets.rb
|
344
355
|
- lib/roda/plugins/relative_path.rb
|
345
356
|
- lib/roda/plugins/render.rb
|
346
357
|
- lib/roda/plugins/render_each.rb
|
@@ -398,7 +409,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
398
409
|
- !ruby/object:Gem::Version
|
399
410
|
version: '0'
|
400
411
|
requirements: []
|
401
|
-
rubygems_version: 3.2.
|
412
|
+
rubygems_version: 3.2.15
|
402
413
|
signing_key:
|
403
414
|
specification_version: 4
|
404
415
|
summary: Routing tree web toolkit
|