roda 3.42.0 → 3.46.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG +22 -0
- data/README.rdoc +11 -14
- 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/doc/release_notes/3.46.0.txt +19 -0
- data/lib/roda.rb +4 -0
- data/lib/roda/plugins/_optimized_matching.rb +137 -0
- data/lib/roda/plugins/assets.rb +16 -0
- data/lib/roda/plugins/content_for.rb +1 -2
- 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/sinatra_helpers.rb +24 -2
- data/lib/roda/plugins/typecast_params.rb +28 -1
- data/lib/roda/request.rb +7 -9
- 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: 5fd9ea4a9be8072ffc96e5bab91a845e99cc9a9d6641571167badd4146c93318
|
4
|
+
data.tar.gz: ed062be1d4b922ba39456f3868e599e3c6134fecfedaa3bfc780667bfec5c5f2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 59e5677db771776107744f2438505b467363b410d6b2c8d19b1b11e802bfd15b22974997b20185e9ba3a1c9a7b8721bb5e3dbc1481a575f0812300c9c3241cac
|
7
|
+
data.tar.gz: 6233ec5251cfbeeaabdb44a7cba75d4831910215ae8239ec19873f58a62f14722bb6b57059f0814ae07e7ccc50977e3a2fbf74bb3b423a253b7dace5773c6887
|
data/CHANGELOG
CHANGED
@@ -1,3 +1,25 @@
|
|
1
|
+
= 3.46.0 (2021-07-12)
|
2
|
+
|
3
|
+
* Automatically optimize r.on/r.is/r.get/r.post methods with a single string, String, Integer, or regexp argument (jeremyevans)
|
4
|
+
|
5
|
+
= 3.45.0 (2021-06-14)
|
6
|
+
|
7
|
+
* Make typecast_params plugin check for null bytes in strings by default, with :allow_null_bytes option for previous behavior (jeremyevans)
|
8
|
+
|
9
|
+
= 3.44.0 (2021-05-12)
|
10
|
+
|
11
|
+
* Add optimized_segment_matchers plugin for optimized matchers for a single String class argument (jeremyevans)
|
12
|
+
|
13
|
+
* Use RFC 5987 UTF-8 and ISO-8859-1 encoded filenames when using send_file and attachment in the sinatra_helpers plugin (jeremyevans)
|
14
|
+
|
15
|
+
= 3.43.1 (2021-04-13)
|
16
|
+
|
17
|
+
* [SECURITY] Fix issue where loading content_security_policy plugin after default_headers plugin had no effect (jeremyevans)
|
18
|
+
|
19
|
+
= 3.43.0 (2021-04-12)
|
20
|
+
|
21
|
+
* Add host_authorization plugin, for checking that requests are submitted using an approved host (jeremyevans)
|
22
|
+
|
1
23
|
= 3.42.0 (2021-03-12)
|
2
24
|
|
3
25
|
* Make Roda.plugin support plugins using keyword arguments in Ruby 3 (jeremyevans)
|
data/README.rdoc
CHANGED
@@ -13,7 +13,6 @@ Website :: http://roda.jeremyevans.net
|
|
13
13
|
Source :: http://github.com/jeremyevans/roda
|
14
14
|
Bugs :: http://github.com/jeremyevans/roda/issues
|
15
15
|
Google Group :: http://groups.google.com/group/ruby-roda
|
16
|
-
IRC :: irc://chat.freenode.net/#roda
|
17
16
|
|
18
17
|
== Goals
|
19
18
|
|
@@ -965,19 +964,17 @@ option. If you really want to turn path checking off, you can do so via the
|
|
965
964
|
Roda does not ship with integrated support for code reloading, but there are rack-based
|
966
965
|
reloaders that will work with Roda apps.
|
967
966
|
|
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.
|
967
|
+
{Zeitwerk}[https://github.com/fxn/zeitwerk] (which Rails now uses for reloading) can be used
|
968
|
+
with Roda. It requires minimal setup and handles most cases. It overrides +require+ when
|
969
|
+
activated. If it can meet the needs of your application, it's probably the best approach.
|
970
|
+
|
971
|
+
{rack-unreloader}[https://github.com/jeremyevans/rack-unreloader] uses a fast
|
972
|
+
approach to reloading while still being fairly safe, as it only reloads files that have
|
973
|
+
been modified, and unloads constants defined in the files before reloading them. It can handle
|
974
|
+
advanced cases that Zeitwerk does not support, such as classes defined in multiple files
|
975
|
+
(common when using separate route files for different routing branches in the same application).
|
976
|
+
However, rack-unreloader does not modify core classes and using it requires modifying your
|
977
|
+
application code to use rack-unreloader specific APIs, which may not be simple.
|
981
978
|
|
982
979
|
{AutoReloader}[https://github.com/rosenfeld/auto_reloader] provides transparent reloading for
|
983
980
|
all files reached from one of the +reloadable_paths+ option entries, by detecting new top-level
|
@@ -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.
|
@@ -0,0 +1,19 @@
|
|
1
|
+
= Improvements
|
2
|
+
|
3
|
+
* The r.on, r.is, r.get and r.post methods (and other verb methods
|
4
|
+
if using the all_verbs plugin) have now been optimized when using
|
5
|
+
a single string or regexp matcher, or the String or Integer class
|
6
|
+
matcher. Since those four matchers are the most common types of
|
7
|
+
matchers passed to the methods, this can significantly improve
|
8
|
+
routing performance (about 50% in the r10k benchmark).
|
9
|
+
|
10
|
+
This optimization is automatically applied when freezing
|
11
|
+
applications, if the related methods have not been modified by
|
12
|
+
plugins.
|
13
|
+
|
14
|
+
This optimization does come at the expense of a small decrease
|
15
|
+
in routing performance (3-4%) for unoptimized cases, but the
|
16
|
+
majority of applications will see a overall performance benefit
|
17
|
+
from this change.
|
18
|
+
|
19
|
+
* Other minor performance improvements have been made.
|
data/lib/roda.rb
CHANGED
@@ -212,6 +212,10 @@ class Roda
|
|
212
212
|
if @middleware.empty? && use_new_dispatch_api?
|
213
213
|
plugin :direct_call
|
214
214
|
end
|
215
|
+
|
216
|
+
if ([:on, :is, :_verb, :_match_class_String, :_match_class_Integer, :_match_string, :_match_regexp, :empty_path?]).all?{|m| self::RodaRequest.instance_method(m).owner == RequestMethods}
|
217
|
+
plugin :_optimized_matching
|
218
|
+
end
|
215
219
|
end
|
216
220
|
|
217
221
|
build_rack_app
|
@@ -0,0 +1,137 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
class Roda
|
5
|
+
module RodaPlugins
|
6
|
+
# The _optimized_matching plugin is automatically used internally to speed
|
7
|
+
# up matching when a single argument String instance, String class, Integer
|
8
|
+
# class, or Regexp matcher is passed to +r.on+, +r.is_+, or a verb method
|
9
|
+
# such as +r.get+ or +r.post+.
|
10
|
+
#
|
11
|
+
# The optimization works by avoiding the +if_match+ method if possible.
|
12
|
+
# Instead of clearing the captures array on every call, and having the
|
13
|
+
# matching append to the captures, it checks directly for the match,
|
14
|
+
# and on succesful match, it yields directly to the block without using
|
15
|
+
# the captures array.
|
16
|
+
module OptimizedMatching
|
17
|
+
TERM = Base::RequestMethods::TERM
|
18
|
+
|
19
|
+
module RequestMethods
|
20
|
+
# Optimize the r.is method handling of a single string, String, Integer,
|
21
|
+
# regexp, or true, argument.
|
22
|
+
def is(*args, &block)
|
23
|
+
case args.length
|
24
|
+
when 1
|
25
|
+
_is1(args, &block)
|
26
|
+
when 0
|
27
|
+
always(&block) if @remaining_path.empty?
|
28
|
+
else
|
29
|
+
if_match(args << TERM, &block)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Optimize the r.on method handling of a single string, String, Integer,
|
34
|
+
# or regexp argument. Inline the related matching code to avoid the
|
35
|
+
# need to modify @captures.
|
36
|
+
def on(*args, &block)
|
37
|
+
case args.length
|
38
|
+
when 1
|
39
|
+
case matcher = args[0]
|
40
|
+
when String
|
41
|
+
always{yield} if _match_string(matcher)
|
42
|
+
when Class
|
43
|
+
if matcher == String
|
44
|
+
rp = @remaining_path
|
45
|
+
if rp.getbyte(0) == 47
|
46
|
+
if last = rp.index('/', 1)
|
47
|
+
@remaining_path = rp[last, rp.length]
|
48
|
+
always{yield rp[1, last-1]}
|
49
|
+
elsif (len = rp.length) > 1
|
50
|
+
@remaining_path = ""
|
51
|
+
always{yield rp[1, len]}
|
52
|
+
end
|
53
|
+
end
|
54
|
+
elsif matcher == Integer
|
55
|
+
if matchdata = @remaining_path.match(/\A\/(\d+)(?=\/|\z)/)
|
56
|
+
@remaining_path = matchdata.post_match
|
57
|
+
always{yield(matchdata[1].to_i)}
|
58
|
+
end
|
59
|
+
else
|
60
|
+
if_match(args, &block)
|
61
|
+
end
|
62
|
+
when Regexp
|
63
|
+
if matchdata = @remaining_path.match(self.class.cached_matcher(matcher){matcher})
|
64
|
+
@remaining_path = matchdata.post_match
|
65
|
+
always{yield(*matchdata.captures)}
|
66
|
+
end
|
67
|
+
else
|
68
|
+
if_match(args, &block)
|
69
|
+
end
|
70
|
+
when 0
|
71
|
+
always(&block)
|
72
|
+
else
|
73
|
+
if_match(args, &block)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
# Optimize the r.get/r.post method handling of a single string, String, Integer,
|
80
|
+
# regexp, or true, argument.
|
81
|
+
def _verb(args, &block)
|
82
|
+
case args.length
|
83
|
+
when 0
|
84
|
+
always(&block)
|
85
|
+
when 1
|
86
|
+
_is1(args, &block)
|
87
|
+
else
|
88
|
+
if_match(args << TERM, &block)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# Internals of r.is/r.get/r.post optimization. Inline the related matching
|
93
|
+
# code to avoid the need to modify @captures.
|
94
|
+
def _is1(args, &block)
|
95
|
+
case matcher = args[0]
|
96
|
+
when String
|
97
|
+
rp = @remaining_path
|
98
|
+
if _match_string(matcher)
|
99
|
+
if @remaining_path.empty?
|
100
|
+
always{yield}
|
101
|
+
else
|
102
|
+
@remaining_path = rp
|
103
|
+
nil
|
104
|
+
end
|
105
|
+
end
|
106
|
+
when Class
|
107
|
+
if matcher == String
|
108
|
+
rp = @remaining_path
|
109
|
+
if rp.getbyte(0) == 47 && !rp.index('/', 1) && (len = rp.length) > 1
|
110
|
+
@remaining_path = ''
|
111
|
+
always{yield rp[1, len]}
|
112
|
+
end
|
113
|
+
elsif matcher == Integer
|
114
|
+
if matchdata = @remaining_path.match(/\A\/(\d+)\z/)
|
115
|
+
@remaining_path = ''
|
116
|
+
always{yield(matchdata[1].to_i)}
|
117
|
+
end
|
118
|
+
else
|
119
|
+
if_match(args << TERM, &block)
|
120
|
+
end
|
121
|
+
when Regexp
|
122
|
+
if (matchdata = @remaining_path.match(self.class.cached_matcher(matcher){matcher})) && (post_match = matchdata.post_match).empty?
|
123
|
+
@remaining_path = ''
|
124
|
+
always{yield(*matchdata.captures)}
|
125
|
+
end
|
126
|
+
when true
|
127
|
+
always(&block) if @remaining_path.empty?
|
128
|
+
else
|
129
|
+
if_match(args << TERM, &block)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
register_plugin(:_optimized_matching, OptimizedMatching)
|
136
|
+
end
|
137
|
+
end
|
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
|
@@ -62,8 +62,7 @@ class Roda
|
|
62
62
|
end
|
63
63
|
|
64
64
|
# Configure whether to append or overwrite if content_for
|
65
|
-
# is called multiple times
|
66
|
-
# the :append option to append.
|
65
|
+
# is called multiple times with the same key.
|
67
66
|
def self.configure(app, opts = OPTS)
|
68
67
|
app.opts[:append_content_for] = opts.fetch(:append, true)
|
69
68
|
end
|
@@ -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.
|
@@ -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
@@ -24,7 +24,7 @@ class Roda
|
|
24
24
|
|
25
25
|
# Return the cached pattern for the given object. If the object is
|
26
26
|
# not already cached, yield to get the basic pattern, and convert the
|
27
|
-
# basic pattern to a pattern that does not partial segments.
|
27
|
+
# basic pattern to a pattern that does not match partial segments.
|
28
28
|
def cached_matcher(obj)
|
29
29
|
cache = @match_pattern_cache
|
30
30
|
|
@@ -234,9 +234,7 @@ class Roda
|
|
234
234
|
|
235
235
|
# An alias of remaining_path. If a plugin changes remaining_path then
|
236
236
|
# it should override this method to return the untouched original.
|
237
|
-
|
238
|
-
remaining_path
|
239
|
-
end
|
237
|
+
alias real_remaining_path remaining_path
|
240
238
|
|
241
239
|
# Match POST requests. If no arguments are provided, matches all POST
|
242
240
|
# requests, otherwise, matches only POST requests where the arguments
|
@@ -336,7 +334,7 @@ class Roda
|
|
336
334
|
# Use <tt>r.get true</tt> to handle +GET+ requests where the current
|
337
335
|
# path is empty.
|
338
336
|
def root(&block)
|
339
|
-
if remaining_path == "/" && is_get?
|
337
|
+
if @remaining_path == "/" && is_get?
|
340
338
|
always(&block)
|
341
339
|
end
|
342
340
|
end
|
@@ -468,8 +466,8 @@ class Roda
|
|
468
466
|
if last = rp.index('/', 1)
|
469
467
|
@captures << rp[1, last-1]
|
470
468
|
@remaining_path = rp[last, rp.length]
|
471
|
-
elsif rp.length > 1
|
472
|
-
@captures << rp[1,
|
469
|
+
elsif (len = rp.length) > 1
|
470
|
+
@captures << rp[1, len]
|
473
471
|
@remaining_path = ""
|
474
472
|
end
|
475
473
|
end
|
@@ -519,7 +517,7 @@ class Roda
|
|
519
517
|
# SCRIPT_NAME to include the matched path, removes the matched
|
520
518
|
# path from PATH_INFO, and updates captures with any regex captures.
|
521
519
|
def consume(pattern)
|
522
|
-
if matchdata = remaining_path.match(pattern)
|
520
|
+
if matchdata = @remaining_path.match(pattern)
|
523
521
|
@remaining_path = matchdata.post_match
|
524
522
|
captures = matchdata.captures
|
525
523
|
captures = yield(*captures) if block_given?
|
@@ -548,7 +546,7 @@ class Roda
|
|
548
546
|
|
549
547
|
# Whether the current path is considered empty.
|
550
548
|
def empty_path?
|
551
|
-
remaining_path.empty?
|
549
|
+
@remaining_path.empty?
|
552
550
|
end
|
553
551
|
|
554
552
|
# If all of the arguments match, yields to the match block and
|
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.46.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-07-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rack
|
@@ -213,6 +213,10 @@ extra_rdoc_files:
|
|
213
213
|
- doc/release_notes/3.40.0.txt
|
214
214
|
- doc/release_notes/3.41.0.txt
|
215
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
|
219
|
+
- doc/release_notes/3.46.0.txt
|
216
220
|
- doc/release_notes/3.5.0.txt
|
217
221
|
- doc/release_notes/3.6.0.txt
|
218
222
|
- doc/release_notes/3.7.0.txt
|
@@ -262,6 +266,10 @@ files:
|
|
262
266
|
- doc/release_notes/3.40.0.txt
|
263
267
|
- doc/release_notes/3.41.0.txt
|
264
268
|
- doc/release_notes/3.42.0.txt
|
269
|
+
- doc/release_notes/3.43.0.txt
|
270
|
+
- doc/release_notes/3.44.0.txt
|
271
|
+
- doc/release_notes/3.45.0.txt
|
272
|
+
- doc/release_notes/3.46.0.txt
|
265
273
|
- doc/release_notes/3.5.0.txt
|
266
274
|
- doc/release_notes/3.6.0.txt
|
267
275
|
- doc/release_notes/3.7.0.txt
|
@@ -272,6 +280,7 @@ files:
|
|
272
280
|
- lib/roda/plugins.rb
|
273
281
|
- lib/roda/plugins/_after_hook.rb
|
274
282
|
- lib/roda/plugins/_before_hook.rb
|
283
|
+
- lib/roda/plugins/_optimized_matching.rb
|
275
284
|
- lib/roda/plugins/_symbol_regexp_matchers.rb
|
276
285
|
- lib/roda/plugins/all_verbs.rb
|
277
286
|
- lib/roda/plugins/assets.rb
|
@@ -312,6 +321,7 @@ files:
|
|
312
321
|
- lib/roda/plugins/header_matchers.rb
|
313
322
|
- lib/roda/plugins/heartbeat.rb
|
314
323
|
- lib/roda/plugins/hooks.rb
|
324
|
+
- lib/roda/plugins/host_authorization.rb
|
315
325
|
- lib/roda/plugins/indifferent_params.rb
|
316
326
|
- lib/roda/plugins/json.rb
|
317
327
|
- lib/roda/plugins/json_parser.rb
|
@@ -330,6 +340,7 @@ files:
|
|
330
340
|
- lib/roda/plugins/named_templates.rb
|
331
341
|
- lib/roda/plugins/not_allowed.rb
|
332
342
|
- lib/roda/plugins/not_found.rb
|
343
|
+
- lib/roda/plugins/optimized_segment_matchers.rb
|
333
344
|
- lib/roda/plugins/optimized_string_matchers.rb
|
334
345
|
- lib/roda/plugins/padrino_render.rb
|
335
346
|
- lib/roda/plugins/param_matchers.rb
|
@@ -401,7 +412,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
401
412
|
- !ruby/object:Gem::Version
|
402
413
|
version: '0'
|
403
414
|
requirements: []
|
404
|
-
rubygems_version: 3.2.
|
415
|
+
rubygems_version: 3.2.22
|
405
416
|
signing_key:
|
406
417
|
specification_version: 4
|
407
418
|
summary: Routing tree web toolkit
|