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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e2bdd27a69c76000c9c1d2d8f37f9919145fdae55224706c78adec42850c5d21
4
- data.tar.gz: 8b92e1dbc9ec83ebca5d2d5b068c67fa282dfee25fe241451c93228a406a4749
3
+ metadata.gz: 5fd9ea4a9be8072ffc96e5bab91a845e99cc9a9d6641571167badd4146c93318
4
+ data.tar.gz: ed062be1d4b922ba39456f3868e599e3c6134fecfedaa3bfc780667bfec5c5f2
5
5
  SHA512:
6
- metadata.gz: 5c9f2d3f0f021b7b0a16628cefe28c4d4001f2a80804f2bde9b48b641a70482a5d13d699252cb9f7c747de50710b91df30f893f83263e3e73c874621948b4cc6
7
- data.tar.gz: c060f123bfb7bd0f02a83d864169d7d2a021c14cd152b8f3e7641c4c7e0317affb84fce0812afcb0d382e0828e31e2fd97b46482338ae92574fbad97793c8996
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
- For most applications, {rack-unreloader}[https://github.com/jeremyevans/rack-unreloader]
969
- is probably the fastest approach to reloading while still being fairly safe, as it
970
- reloads just files that have been modified, and unloads constants defined in the files
971
- before reloading them. However, it requires modifying your application code to use
972
- rack-unreloader specific APIs.
973
-
974
- A similar solution that reloads files and unloads constants is ActiveSupport::Dependencies.
975
- ActiveSupport::Dependencies doesn't require modifying your application code, but it modifies
976
- some core methods, including +require+ and +const_missing+. It requires less configuration,
977
- but depends that you follow Rails' file and class naming conventions. It also provides
978
- autoloading (on the fly) of files when a missing constant is accessed. If your application
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
@@ -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 to set data. Overwrite is default, use
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
- headers = app.opts[:default_headers] = (app.default_headers || app::RodaResponse::DEFAULT_HEADERS).merge(headers).freeze
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
- if headers.all?{|k, v| k.is_a?(String) && v.is_a?(String)}
29
- response_class = app::RodaResponse
30
- owner = response_class.instance_method(:set_default_headers).owner
31
- if owner == Base::ResponseMethods || (owner == response_class && app.opts[:set_default_headers_overridder] == response_class)
32
- app.opts[:set_default_headers_overridder] = response_class
33
- response_class.class_eval(<<-END, __FILE__, __LINE__+1)
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
- module ClassMethods
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
- # Note that both of these methods only work with plain strings, not
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
- params = "; filename=#{File.basename(filename).inspect}"
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!{|val| send(convert_meth, val)}
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
- def real_remaining_path
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,rp.length]
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
@@ -4,7 +4,7 @@ class Roda
4
4
  RodaMajorVersion = 3
5
5
 
6
6
  # The minor version of Roda, updated for new feature releases of Roda.
7
- RodaMinorVersion = 42
7
+ RodaMinorVersion = 46
8
8
 
9
9
  # The patch version of Roda, updated only for bug fixes from the last
10
10
  # feature release.
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.42.0
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-03-12 00:00:00.000000000 Z
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.3
415
+ rubygems_version: 3.2.22
405
416
  signing_key:
406
417
  specification_version: 4
407
418
  summary: Routing tree web toolkit