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 +4 -4
- data/CHANGELOG +6 -0
- data/doc/release_notes/3.79.0.txt +148 -0
- data/lib/roda/plugins/hmac_paths.rb +266 -0
- data/lib/roda/plugins/render.rb +15 -11
- data/lib/roda/version.rb +1 -1
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8e77e7eba739ca8bf0afe44555c8af8c9188f4ce8668310aad110a2afc638701
|
4
|
+
data.tar.gz: 19db55450dfe15e7aa4f678d7556ec427e52bcad421a096791d47a5a3fe128b4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
data/lib/roda/plugins/render.rb
CHANGED
@@ -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
|
-
|
339
|
-
|
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
|
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
|
-
|
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,
|
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(
|
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(
|
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
|
-
|
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
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.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-
|
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
|