roda 3.78.0 → 3.79.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: 44eb48fa9dd507c495e32f90e5ef3b9b197d47f1c148f5d91714272afdca74f8
4
- data.tar.gz: 276ceaf7256b816b4a5c59e7c2508cb9066e1d1693d398e7be43ac17be7a9115
3
+ metadata.gz: 8e77e7eba739ca8bf0afe44555c8af8c9188f4ce8668310aad110a2afc638701
4
+ data.tar.gz: 19db55450dfe15e7aa4f678d7556ec427e52bcad421a096791d47a5a3fe128b4
5
5
  SHA512:
6
- metadata.gz: b99672a570f6c119375435546104627d27437cb66372f61df0b7e08a9eb4ae66709b33162d2cf0354b2a22c3ed7c456b60423218cd734a9fe644c9b278c5c44f
7
- data.tar.gz: 6bd6f328f173bfe2dd28b1b4ea9452158f127ac2c93dcf4de6783af5513d38803e9642c3c24b899e16d05dbdb35569464dc9ca2f00111a8814193e48870afe45
6
+ metadata.gz: f504398b08e35ca42b765afca4357d750836ce5d7e3e7ec9ba73b061dde57fd2bbd3fa5d7752b9e6995739fac8a8718b0a8a71d70432ec14b65c95e13dadeb29
7
+ data.tar.gz: ceb20219c23d4e44d006c5fe177fff3ef2cacb77ec75e109b92c03d12f0e9cf434459759d007df96becfa0e426c89f4f8f9517c00c579135cfbb0e613852a0a1
data/CHANGELOG CHANGED
@@ -1,3 +1,9 @@
1
+ = 3.79.0 (2024-04-12)
2
+
3
+ * Do not update template mtime when there is an error reloading templates in the render plugin (jeremyevans)
4
+
5
+ * Add hmac_paths plugin for preventing path enumeration and supporting access control (jeremyevans)
6
+
1
7
  = 3.78.0 (2024-03-13)
2
8
 
3
9
  * Add permissions_policy plugin for setting Permissions-Policy header (jeremyevans)
@@ -0,0 +1,148 @@
1
+ = New Features
2
+
3
+ * The hmac_paths plugin allows protection of paths using an HMAC. This can be used
4
+ to prevent users enumerating paths, since only paths with valid HMACs will be
5
+ respected.
6
+
7
+ To use the plugin, you must provide a :secret option. This sets the secret for
8
+ the HMACs. Make sure to keep this value secret, as this plugin does not provide
9
+ protection against users who know the secret value. The secret must be at least
10
+ 32 bytes.
11
+
12
+ plugin :hmac_paths, secret: 'some-secret-value-with-at-least-32-bytes'
13
+
14
+ To generate a valid HMAC path, you call the hmac_path method:
15
+
16
+ hmac_path('/widget/1')
17
+ # => "/0c2feaefdfc80cc73da19b060c713d4193c57022815238c6657ce2d99b5925eb/0/widget/1"
18
+
19
+ The first segment in the returned path is the HMAC. The second segment is flags for
20
+ the type of paths (see below), and the rest of the path is as given.
21
+
22
+ To protect a path or any subsection in the routing tree, you wrap the related code
23
+ in an +r.hmac_path+ block.
24
+
25
+ route do |r|
26
+ r.hmac_path do
27
+ r.get 'widget', Integer do |widget_id|
28
+ # ...
29
+ end
30
+ end
31
+ end
32
+
33
+ If first segment of the remaining path contains a valid HMAC for the rest of the path (considering
34
+ the flags), then r.hmac_path will match and yield to the block, and routing continues inside
35
+ the block with the HMAC and flags segments removed.
36
+
37
+ In the above example, if you provide a user a link for widget with ID 1, there is no way
38
+ for them to guess the valid path for the widget with ID 2, preventing a user from
39
+ enumerating widgets, without relying on custom access control. Users can only access
40
+ paths that have been generated by the application and provided to them, either directly
41
+ or indirectly.
42
+
43
+ In the above example, r.hmac_path is used at the root of the routing tree. If you
44
+ would like to call it below the root of the routing tree, it works correctly, but you
45
+ must pass hmac_path the :root option specifying where r.hmac_paths will be called from.
46
+ Consider this example:
47
+
48
+ route do |r|
49
+ r.on 'widget' do
50
+ r.hmac_path do
51
+ r.get Integer do |widget_id|
52
+ # ...
53
+ end
54
+ end
55
+ end
56
+
57
+ r.on 'foobar' do
58
+ r.hmac_path do
59
+ r.get Integer do |foobar_id|
60
+ # ...
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+ For security reasons, the hmac_path plugin does not allow an HMAC path designed for
67
+ widgets to be a valid match in the r.hmac_path call inside the "r.on 'foobar'"
68
+ block, preventing users who have a valid HMAC for a widget from looking at the page for
69
+ a foobar with the same ID. When generating HMAC paths where the matching r.hmac_path
70
+ call is not at the root of the routing tree, you must pass the :root option:
71
+
72
+ hmac_path('/1', root: '/widget')
73
+ # => "/widget/daccafce3ce0df52e5ce774626779eaa7286085fcbde1e4681c74175ff0bbacd/0/1"
74
+
75
+ hmac_path('/1', root: '/foobar')
76
+ # => "/foobar/c5fdaf482771d4f9f38cc13a1b2832929026a4ceb05e98ed6a0cd5a00bf180b7/0/1"
77
+
78
+ Note how the HMAC changes even though the path is the same.
79
+
80
+ In addition to the +:root+ option, there are additional options that further constrain
81
+ use of the generated paths.
82
+
83
+ The :method option creates a path that can only be called with a certain request
84
+ method:
85
+
86
+ hmac_path('/widget/1', method: :get)
87
+ # => "/d38c1e634ecf9a3c0ab9d0832555b035d91b35069efcbf2670b0dfefd4b62fdd/m/widget/1"
88
+
89
+ Note how this results in a different HMAC than the original hmac_path('/widget/1')
90
+ call. This sets the flags segment to "m", which means r.hmac_path will consider the
91
+ request mehod when checking the HMAC, and will only match if the provided request method
92
+ is GET. This allows you to provide a user the ability to submit a GET request for the
93
+ underlying path, without providing them the ability to submit a POST request for the
94
+ underlying path, with no other access control.
95
+
96
+ The :params option accepts a hash of params, converts it into a query string, and
97
+ includes the query string in the returned path. It sets the flags segment to +p+, which
98
+ means r.hmac_path will check for that exact query string. Requests with an empty query
99
+ string or a different string will not match.
100
+
101
+ hmac_path('/widget/1', params: {foo: 'bar'})
102
+ # => "/fe8d03f9572d5af6c2866295bd3c12c2ea11d290b1cbd016c3b68ee36a678139/p/widget/1?foo=bar"
103
+
104
+ For GET requests, which cannot have request bodies, that is sufficient to ensure that the
105
+ submitted params are exactly as specified. However, POST requests can have request bodies,
106
+ and request body params override query string params in r.params. So if you are using
107
+ this for POST requests (or other HTTP verbs that can have request bodies), use r.GET
108
+ instead of r.params to specifically check query string parameters.
109
+
110
+ You can use +:root+, +:method+, and +:params+ at the same time:
111
+
112
+ hmac_path('/1', root: '/widget', method: :get, params: {foo: 'bar'})
113
+ # => "/widget/9169af1b8f40c62a1c2bb15b1b377c65bda681b8efded0e613a4176387468c15/mp/1?foo=bar"
114
+
115
+ This gives you a path only valid for a GET request with a root of "/widget" and
116
+ a query string of "foo=bar".
117
+
118
+ To handle secret rotation, you can provide an :old_secret option when loading the
119
+ plugin.
120
+
121
+ plugin :hmac_paths, secret: 'some-secret-value-with-at-least-32-bytes',
122
+ old_secret: 'previous-secret-value-with-at-least-32-bytes'
123
+
124
+ This will use :secret for constructing new paths, but will respect paths generated by
125
+ :old_secret.
126
+
127
+ = Other Improvements
128
+
129
+ * When not using cached templates in the render plugin, the render plugin
130
+ now has better handling when a template is modified and results in an
131
+ error. Previously, the error would be raised on the first request after
132
+ the template modification, but subsequent requests would use the
133
+ previous template value. The render plugin will no longer update the
134
+ last modified time in this case, so if a template is modified and
135
+ introduces an error (e.g. SyntaxError in an erb template), all future
136
+ requests that use the template will result in the error being raised,
137
+ until the template is fixed.
138
+
139
+ = Backwards Compatibility
140
+
141
+ * The internal TemplateMtimeWrapper API has been modified. As documented,
142
+ this is an internal class and the API can change in any Roda version.
143
+ However, if any code was relying on the previous implementation of
144
+ TemplateMtimeWrapper#modified?, it will need to be modified, as that
145
+ method has been replaced with TemplateMtimeWrapper#if_modified.
146
+
147
+ Additionally, the TemplateMtimeWrapper#compiled_method_lambda API has
148
+ also changed.
@@ -0,0 +1,266 @@
1
+ # frozen-string-literal: true
2
+
3
+ require 'openssl'
4
+
5
+ #
6
+ class Roda
7
+ module RodaPlugins
8
+ # The hmac_paths plugin allows protection of paths using an HMAC. This can be used
9
+ # to prevent users enumerating paths, since only paths with valid HMACs will be
10
+ # respected.
11
+ #
12
+ # To use the plugin, you must provide a +secret+ option. This sets the secret for
13
+ # the HMACs. Make sure to keep this value secret, as this plugin does not provide
14
+ # protection against users who know the secret value. The secret must be at least
15
+ # 32 bytes.
16
+ #
17
+ # plugin :hmac_paths, secret: 'some-secret-value-with-at-least-32-bytes'
18
+ #
19
+ # To generate a valid HMAC path, you call the +hmac_path+ method:
20
+ #
21
+ # hmac_path('/widget/1')
22
+ # # => "/0c2feaefdfc80cc73da19b060c713d4193c57022815238c6657ce2d99b5925eb/0/widget/1"
23
+ #
24
+ # The first segment in the returned path is the HMAC. The second segment is flags for
25
+ # the type of paths (see below), and the rest of the path is as given.
26
+ #
27
+ # To protect a path or any subsection in the routing tree, you wrap the related code
28
+ # in an +r.hmac_path+ block.
29
+ #
30
+ # route do |r|
31
+ # r.hmac_path do
32
+ # r.get 'widget', Integer do |widget_id|
33
+ # # ...
34
+ # end
35
+ # end
36
+ # end
37
+ #
38
+ # If first segment of the remaining path contains a valid HMAC for the rest of the path (considering
39
+ # the flags), then +r.hmac_path+ will match and yield to the block, and routing continues inside
40
+ # the block with the HMAC and flags segments removed.
41
+ #
42
+ # In the above example, if you provide a user a link for widget with ID 1, there is no way
43
+ # for them to guess the valid path for the widget with ID 2, preventing a user from
44
+ # enumerating widgets, without relying on custom access control. Users can only access
45
+ # paths that have been generated by the application and provided to them, either directly
46
+ # or indirectly.
47
+ #
48
+ # In the above example, +r.hmac_path+ is used at the root of the routing tree. If you
49
+ # would like to call it below the root of the routing tree, it works correctly, but you
50
+ # must pass +hmac_path+ the +:root+ option specifying where +r.hmac_paths+ will be called from.
51
+ # Consider this example:
52
+ #
53
+ # route do |r|
54
+ # r.on 'widget' do
55
+ # r.hmac_path do
56
+ # r.get Integer do |widget_id|
57
+ # # ...
58
+ # end
59
+ # end
60
+ # end
61
+ #
62
+ # r.on 'foobar' do
63
+ # r.hmac_path do
64
+ # r.get Integer do |foobar_id|
65
+ # # ...
66
+ # end
67
+ # end
68
+ # end
69
+ # end
70
+ #
71
+ # For security reasons, the hmac_path plugin does not allow an HMAC path designed for
72
+ # widgets to be a valid match in the +r.hmac_path+ call inside the <tt>r.on 'foobar'</tt>
73
+ # block, preventing users who have a valid HMAC for a widget from looking at the page for
74
+ # a foobar with the same ID. When generating HMAC paths where the matching +r.hmac_path+
75
+ # call is not at the root of the routing tree, you must pass the +:root+ option:
76
+ #
77
+ # hmac_path('/1', root: '/widget')
78
+ # # => "/widget/daccafce3ce0df52e5ce774626779eaa7286085fcbde1e4681c74175ff0bbacd/0/1"
79
+ #
80
+ # hmac_path('/1', root: '/foobar')
81
+ # # => "/foobar/c5fdaf482771d4f9f38cc13a1b2832929026a4ceb05e98ed6a0cd5a00bf180b7/0/1"
82
+ #
83
+ # Note how the HMAC changes even though the path is the same.
84
+ #
85
+ # In addition to the +:root+ option, there are additional options that further constrain
86
+ # use of the generated paths.
87
+ #
88
+ # The +:method+ option creates a path that can only be called with a certain request
89
+ # method:
90
+ #
91
+ # hmac_path('/widget/1', method: :get)
92
+ # # => "/d38c1e634ecf9a3c0ab9d0832555b035d91b35069efcbf2670b0dfefd4b62fdd/m/widget/1"
93
+ #
94
+ # Note how this results in a different HMAC than the original <tt>hmac_path('/widget/1')</tt>
95
+ # call. This sets the flags segment to +m+, which means +r.hmac_path+ will consider the
96
+ # request mehod when checking the HMAC, and will only match if the provided request method
97
+ # is GET. This allows you to provide a user the ability to submit a GET request for the
98
+ # underlying path, without providing them the ability to submit a POST request for the
99
+ # underlying path, with no other access control.
100
+ #
101
+ # The +:params+ option accepts a hash of params, converts it into a query string, and
102
+ # includes the query string in the returned path. It sets the flags segment to +p+, which
103
+ # means +r.hmac_path+ will check for that exact query string. Requests with an empty query
104
+ # string or a different string will not match.
105
+ #
106
+ # hmac_path('/widget/1', params: {foo: 'bar'})
107
+ # # => "/fe8d03f9572d5af6c2866295bd3c12c2ea11d290b1cbd016c3b68ee36a678139/p/widget/1?foo=bar"
108
+ #
109
+ # For GET requests, which cannot have request bodies, that is sufficient to ensure that the
110
+ # submitted params are exactly as specified. However, POST requests can have request bodies,
111
+ # and request body params override query string params in +r.params+. So if you are using
112
+ # this for POST requests (or other HTTP verbs that can have request bodies), use +r.GET+
113
+ # instead of +r.params+ to specifically check query string parameters.
114
+ #
115
+ # You can use +:root+, +:method+, and +:params+ at the same time:
116
+ #
117
+ # hmac_path('/1', root: '/widget', method: :get, params: {foo: 'bar'})
118
+ # # => "/widget/9169af1b8f40c62a1c2bb15b1b377c65bda681b8efded0e613a4176387468c15/mp/1?foo=bar"
119
+ #
120
+ # This gives you a path only valid for a GET request with a root of <tt>/widget</tt> and
121
+ # a query string of <tt>foo=bar</tt>.
122
+ #
123
+ # To handle secret rotation, you can provide an +:old_secret+ option when loading the
124
+ # plugin.
125
+ #
126
+ # plugin :hmac_paths, secret: 'some-secret-value-with-at-least-32-bytes',
127
+ # old_secret: 'previous-secret-value-with-at-least-32-bytes'
128
+ #
129
+ # This will use +:secret+ for constructing new paths, but will respect paths generated by
130
+ # +:old_secret+.
131
+ module HmacPaths
132
+ def self.configure(app, opts=OPTS)
133
+ hmac_secret = opts[:secret]
134
+ unless hmac_secret.is_a?(String) && hmac_secret.bytesize >= 32
135
+ raise RodaError, "hmac_paths plugin :secret option must be a string containing at least 32 bytes"
136
+ end
137
+
138
+ if hmac_old_secret = opts[:old_secret]
139
+ unless hmac_old_secret.is_a?(String) && hmac_old_secret.bytesize >= 32
140
+ raise RodaError, "hmac_paths plugin :old_secret option must be a string containing at least 32 bytes if present"
141
+ end
142
+ end
143
+
144
+ app.opts[:hmac_paths_secret] = hmac_secret
145
+ app.opts[:hmac_paths_old_secret] = hmac_old_secret
146
+ end
147
+
148
+ module InstanceMethods
149
+ # Return a path with an HMAC. Designed to be used with r.hmac_path, to make sure
150
+ # users can only request paths that they have been provided by the application
151
+ # (directly or indirectly). This can prevent users of a site from enumerating
152
+ # valid paths. The given path should be a string starting with +/+. Options:
153
+ #
154
+ # :method :: Limits the returned path to only be valid for the given request method.
155
+ # :params :: Includes parameters in the query string of the returned path, and
156
+ # limits the returned path to only be valid for that exact query string.
157
+ # :root :: Should be an empty string or string starting with +/+. This will be
158
+ # the already matched path of the routing tree using r.hmac_path. Defaults
159
+ # to the empty string, which will returns paths valid for r.hmac_path at
160
+ # the top level of the routing tree.
161
+ def hmac_path(path, opts=OPTS)
162
+ unless path.is_a?(String) && path.getbyte(0) == 47
163
+ raise RodaError, "path must be a string starting with /"
164
+ end
165
+
166
+ root = opts[:root] || ''
167
+ unless root.is_a?(String) && ((root_byte = root.getbyte(0)) == 47 || root_byte == nil)
168
+ raise RodaError, "root must be empty string or string starting with /"
169
+ end
170
+
171
+ flags = String.new
172
+ path = path.dup
173
+
174
+ if method = opts[:method]
175
+ flags << 'm'
176
+ end
177
+
178
+ if params = opts[:params]
179
+ flags << 'p'
180
+ path << '?' << Rack::Utils.build_query(params)
181
+ end
182
+
183
+ flags << '0' if flags.empty?
184
+
185
+ hmac_path = if method
186
+ "#{method.to_s.upcase}:/#{flags}#{path}"
187
+ else
188
+ "/#{flags}#{path}"
189
+ end
190
+
191
+ "#{root}/#{hmac_path_hmac(root, hmac_path)}/#{flags}#{path}"
192
+ end
193
+
194
+ # The HMAC to use in hmac_path, for the given root, path, and options.
195
+ def hmac_path_hmac(root, path, opts=OPTS)
196
+ OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new, hmac_path_hmac_secret(root, opts), path)
197
+ end
198
+
199
+ private
200
+
201
+ # The secret used to calculate the HMAC in hmac_path. This is itself an HMAC, created
202
+ # using the secret given in the plugin, for the given root and options. If the
203
+ def hmac_path_hmac_secret(root, opts=OPTS)
204
+ secret = opts[:secret] || self.opts[:hmac_paths_secret]
205
+ OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new, secret, root)
206
+ end
207
+ end
208
+
209
+ module RequestMethods
210
+ # Looks at the first segment of the remaining path, and if it contains a valid HMAC for the
211
+ # rest of the path considering the flags in the second segment and the given options, the
212
+ # block matches and is yielded to, and the result of the block is returned. Otherwise, the
213
+ # block does not matches and routing continues after the call.
214
+ def hmac_path(opts=OPTS, &block)
215
+ orig_path = remaining_path
216
+ mpath = matched_path
217
+
218
+ on String do |submitted_hmac|
219
+ rpath = remaining_path
220
+
221
+ if submitted_hmac.bytesize == 64
222
+ on String do |flags|
223
+ if flags.bytesize >= 1
224
+ if flags.include?('m')
225
+ rpath = "#{env['REQUEST_METHOD'].to_s.upcase}:#{rpath}"
226
+ end
227
+
228
+ if flags.include?('p')
229
+ rpath = "#{rpath}?#{env["QUERY_STRING"]}"
230
+ end
231
+
232
+ if hmac_path_valid?(mpath, rpath, submitted_hmac)
233
+ always(&block)
234
+ end
235
+ end
236
+
237
+ # Return from method without matching
238
+ @remaining_path = orig_path
239
+ return
240
+ end
241
+ end
242
+
243
+ # Return from method without matching
244
+ @remaining_path = orig_path
245
+ return
246
+ end
247
+ end
248
+
249
+ private
250
+
251
+ # Determine whether the provided hmac matches.
252
+ def hmac_path_valid?(root, path, hmac)
253
+ if Rack::Utils.secure_compare(scope.hmac_path_hmac(root, path), hmac)
254
+ true
255
+ elsif old_secret = roda_class.opts[:hmac_paths_old_secret]
256
+ Rack::Utils.secure_compare(scope.hmac_path_hmac(root, path, secret: old_secret), hmac)
257
+ else
258
+ false
259
+ end
260
+ end
261
+ end
262
+ end
263
+
264
+ register_plugin(:hmac_paths, HmacPaths)
265
+ end
266
+ end
@@ -335,8 +335,13 @@ class Roda
335
335
  # If the template file exists and the modification time has
336
336
  # changed, rebuild the template file, then call render on it.
337
337
  def render(*args, &block)
338
- modified?
339
- @template.render(*args, &block)
338
+ res = nil
339
+ modified = false
340
+ if_modified do
341
+ res = @template.render(*args, &block)
342
+ modified = true
343
+ end
344
+ modified ? res : @template.render(*args, &block)
340
345
  end
341
346
 
342
347
  # Return when the template was last modified. If the template depends on any
@@ -352,20 +357,18 @@ class Roda
352
357
 
353
358
  # If the template file has been updated, return true and update
354
359
  # the template object and the modification time. Other return false.
355
- def modified?
360
+ def if_modified
356
361
  begin
357
362
  mtime = template_last_modified
358
363
  rescue
359
364
  # ignore errors
360
365
  else
361
366
  if mtime != @mtime
362
- @mtime = mtime
363
367
  reset_template
364
- return true
368
+ yield
369
+ @mtime = mtime
365
370
  end
366
371
  end
367
-
368
- false
369
372
  end
370
373
 
371
374
  if COMPILED_METHOD_SUPPORT
@@ -375,13 +378,13 @@ class Roda
375
378
  mod = roda_class::RodaCompiledTemplates
376
379
  internal_method_name = :"_#{method_name}"
377
380
  begin
378
- mod.send(:define_method, internal_method_name, send(:compiled_method, locals_keys, roda_class))
381
+ mod.send(:define_method, internal_method_name, compiled_method(locals_keys, roda_class))
379
382
  rescue ::NotImplementedError
380
383
  return false
381
384
  end
382
385
 
383
386
  mod.send(:private, internal_method_name)
384
- mod.send(:define_method, method_name, &compiled_method_lambda(self, roda_class, internal_method_name, locals_keys))
387
+ mod.send(:define_method, method_name, &compiled_method_lambda(roda_class, internal_method_name, locals_keys))
385
388
  mod.send(:private, method_name)
386
389
 
387
390
  method_name
@@ -397,10 +400,11 @@ class Roda
397
400
  # Return the lambda used to define the compiled template method. This
398
401
  # is separated into its own method so the lambda does not capture any
399
402
  # unnecessary local variables
400
- def compiled_method_lambda(template, roda_class, method_name, locals_keys=EMPTY_ARRAY)
403
+ def compiled_method_lambda(roda_class, method_name, locals_keys=EMPTY_ARRAY)
401
404
  mod = roda_class::RodaCompiledTemplates
405
+ template = self
402
406
  lambda do |locals, &block|
403
- if template.modified?
407
+ template.if_modified do
404
408
  mod.send(:define_method, method_name, Render.tilt_template_compiled_method(template, locals_keys, roda_class))
405
409
  mod.send(:private, method_name)
406
410
  end
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 = 78
7
+ RodaMinorVersion = 79
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.78.0
4
+ version: 3.79.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: 2024-03-13 00:00:00.000000000 Z
11
+ date: 2024-04-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack
@@ -252,6 +252,7 @@ extra_rdoc_files:
252
252
  - doc/release_notes/3.76.0.txt
253
253
  - doc/release_notes/3.77.0.txt
254
254
  - doc/release_notes/3.78.0.txt
255
+ - doc/release_notes/3.79.0.txt
255
256
  - doc/release_notes/3.8.0.txt
256
257
  - doc/release_notes/3.9.0.txt
257
258
  files:
@@ -337,6 +338,7 @@ files:
337
338
  - doc/release_notes/3.76.0.txt
338
339
  - doc/release_notes/3.77.0.txt
339
340
  - doc/release_notes/3.78.0.txt
341
+ - doc/release_notes/3.79.0.txt
340
342
  - doc/release_notes/3.8.0.txt
341
343
  - doc/release_notes/3.9.0.txt
342
344
  - lib/roda.rb
@@ -399,6 +401,7 @@ files:
399
401
  - lib/roda/plugins/head.rb
400
402
  - lib/roda/plugins/header_matchers.rb
401
403
  - lib/roda/plugins/heartbeat.rb
404
+ - lib/roda/plugins/hmac_paths.rb
402
405
  - lib/roda/plugins/hooks.rb
403
406
  - lib/roda/plugins/host_authorization.rb
404
407
  - lib/roda/plugins/indifferent_params.rb