roda 3.78.0 → 3.80.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG +10 -0
- data/doc/release_notes/3.79.0.txt +148 -0
- data/doc/release_notes/3.80.0.txt +31 -0
- data/lib/roda/plugins/hmac_paths.rb +373 -0
- data/lib/roda/plugins/render.rb +15 -11
- data/lib/roda/version.rb +1 -1
- metadata +8 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0cebe935d536e1f903b212075108ba9cbcade4aa2e8d2abf1c5e0d6e6f539ce8
|
4
|
+
data.tar.gz: 9b0c576aaa36c5a05596c1bc1bec23d3ab0f37c56dc72342bfa962b10442928f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0e83d0fe8e1f70bb196ea5f285f7a00590ab1669e4b1d686f859d1e5c65a8e760d1320b345306740d00c7d063f0f6bada1af88433b3b04c6b429f439bb7e2521
|
7
|
+
data.tar.gz: e9b0ffd5b5fb7e976a971f11fbed4f0beb35868dcb7aa70c41c005046a9053cb6c62caf29f669c02cd53e67cd5db51a28076f058824c90a1c1b917a9b7f0f4fb
|
data/CHANGELOG
CHANGED
@@ -1,3 +1,13 @@
|
|
1
|
+
= 3.80.0 (2024-05-10)
|
2
|
+
|
3
|
+
* Support :namespace option in hmac_paths plugin, allowing for easy per-user/per-group HMAC paths (jeremyevans)
|
4
|
+
|
5
|
+
= 3.79.0 (2024-04-12)
|
6
|
+
|
7
|
+
* Do not update template mtime when there is an error reloading templates in the render plugin (jeremyevans)
|
8
|
+
|
9
|
+
* Add hmac_paths plugin for preventing path enumeration and supporting access control (jeremyevans)
|
10
|
+
|
1
11
|
= 3.78.0 (2024-03-13)
|
2
12
|
|
3
13
|
* 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,31 @@
|
|
1
|
+
= New Features
|
2
|
+
|
3
|
+
* The hmac_paths plugin now supports a :namespace option for both hmac_path and
|
4
|
+
r.hmac_path. The :namespace option makes the generated HMAC values unique
|
5
|
+
per namespace, allowing easy use of per user/group HMAC paths. This can
|
6
|
+
be useful if the same path will show different information to different
|
7
|
+
users/groups, and you want to prevent path enumeration for each user/group
|
8
|
+
(not allow paths enumerated by one user/group to be valid for a different
|
9
|
+
user/group). Example:
|
10
|
+
|
11
|
+
hmac_path('/widget/1', namespace: '1')
|
12
|
+
# => "/3793ac2a72ea399c40cbd63f154d19f0fe34cdf8d347772134c506a0b756d590/n/widget/1"
|
13
|
+
|
14
|
+
hmac_path('/widget/1', namespace: '2')
|
15
|
+
# => "/0e1e748860d4fd17fe9b7c8259b1e26996502c38e465f802c2c9a0a13000087c/n/widget/1"
|
16
|
+
|
17
|
+
The HMAC path created with namespace: '1' will only be valid when calling
|
18
|
+
r.hmac_path with namespace: '1' (similar for namespace: '2').
|
19
|
+
|
20
|
+
It is expected that the most common use of the :namespace option is to
|
21
|
+
reference session values, so the value of each path depends on the logged in
|
22
|
+
user. You can use the :namespace_session_key plugin option to set the
|
23
|
+
default namespace for both hmac_path and r.hmac_path:
|
24
|
+
|
25
|
+
plugin :hmac_paths, secret: 'some-secret-value-with-at-least-32-bytes',
|
26
|
+
namespace_session_key: 'account_id'
|
27
|
+
|
28
|
+
This will use <tt>session['account_id']</tt> (converted to a string) as the namespace
|
29
|
+
for both hmac_path and r.hmac_path, unless a specific :namespace option is
|
30
|
+
given, making it simple to implement per user/group HMAC paths across an
|
31
|
+
application.
|
@@ -0,0 +1,373 @@
|
|
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
|
+
# The :namespace option, if provided, should be a string, and it modifies the generated HMACs
|
116
|
+
# to only match those in the same namespace. This can be used to provide different paths to
|
117
|
+
# different users or groups of users.
|
118
|
+
#
|
119
|
+
# hmac_path('/widget/1', namespace: '1')
|
120
|
+
# # => "/3793ac2a72ea399c40cbd63f154d19f0fe34cdf8d347772134c506a0b756d590/n/widget/1"
|
121
|
+
#
|
122
|
+
# hmac_path('/widget/1', namespace: '2')
|
123
|
+
# # => "/0e1e748860d4fd17fe9b7c8259b1e26996502c38e465f802c2c9a0a13000087c/n/widget/1"
|
124
|
+
#
|
125
|
+
# The +r.hmac_path+ method accepts a :namespace option, and if a :namespace option is
|
126
|
+
# provided, it will only match an hmac path if the namespace given matches the one used
|
127
|
+
# when the hmac path was created.
|
128
|
+
#
|
129
|
+
# r.hmac_path(namespace: '1'){}
|
130
|
+
# # will match "/3793ac2a72ea399c40cbd63f154d19f0fe34cdf8d347772134c506a0b756d590/n/widget/1"
|
131
|
+
# # will not match "/0e1e748860d4fd17fe9b7c8259b1e26996502c38e465f802c2c9a0a13000087c/n/widget/1"
|
132
|
+
#
|
133
|
+
# The most common use of the :namespace option is to reference session values, so the value of
|
134
|
+
# each path depends on the logged in user. You can use the +:namespace_session_key+ plugin
|
135
|
+
# option to set the default namespace for both +hmac_path+ and +r.hmac_path+:
|
136
|
+
#
|
137
|
+
# plugin :hmac_paths, secret: 'some-secret-value-with-at-least-32-bytes',
|
138
|
+
# namespace_session_key: 'account_id'
|
139
|
+
#
|
140
|
+
# This will use <tt>session['account_id']</tt> as the default namespace for both +hmac_path+
|
141
|
+
# and +r.hmac_path+ (if the session value is not nil, it is converted to a string using +to_s+).
|
142
|
+
# You can override the default namespace by passing a +:namespace+ option when calling +hmac_path+
|
143
|
+
# and +r.hmac_path+.
|
144
|
+
#
|
145
|
+
# You can use +:root+, +:method+, +:params+, and +:namespace+ at the same time:
|
146
|
+
#
|
147
|
+
# hmac_path('/1', root: '/widget', method: :get, params: {foo: 'bar'}, namespace: '1')
|
148
|
+
# # => "/widget/c14c78a81d34d766cf334a3ddbb7a6b231bc2092ef50a77ded0028586027b14e/mpn/1?foo=bar"
|
149
|
+
#
|
150
|
+
# This gives you a path only valid for a GET request with a root of <tt>/widget</tt> and
|
151
|
+
# a query string of <tt>foo=bar</tt>, using namespace +1+.
|
152
|
+
#
|
153
|
+
# To handle secret rotation, you can provide an +:old_secret+ option when loading the
|
154
|
+
# plugin.
|
155
|
+
#
|
156
|
+
# plugin :hmac_paths, secret: 'some-secret-value-with-at-least-32-bytes',
|
157
|
+
# old_secret: 'previous-secret-value-with-at-least-32-bytes'
|
158
|
+
#
|
159
|
+
# This will use +:secret+ for constructing new paths, but will respect paths generated by
|
160
|
+
# +:old_secret+.
|
161
|
+
#
|
162
|
+
# = HMAC Construction
|
163
|
+
#
|
164
|
+
# This describes the internals for how HMACs are constructed based on the options provided
|
165
|
+
# to +hmac_path+. In the examples below:
|
166
|
+
#
|
167
|
+
# * +HMAC+ is the raw HMAC-SHA256 output (first argument is secret, second is data)
|
168
|
+
# * +HMAC_hex+ is the hexidecimal version of +HMAC+
|
169
|
+
# * +secret+ is the plugin :secret option
|
170
|
+
#
|
171
|
+
# The +:secret+ plugin option is never used directly as the HMAC secret. All HMACs are
|
172
|
+
# generated with a root-specific secret. The root will be the empty if no +:root+ option
|
173
|
+
# is given. The hmac path flags are always included in the hmac calculation, prepended to the
|
174
|
+
# path:
|
175
|
+
#
|
176
|
+
# r.hmac_path('/1')
|
177
|
+
# HMAC_hex(HMAC_hex(secret, ''), '/0/1')
|
178
|
+
#
|
179
|
+
# r.hmac_path('/1', root: '/2')
|
180
|
+
# HMAC_hex(HMAC_hex(secret, '/2'), '/0/1')
|
181
|
+
#
|
182
|
+
# The +:method+ option uses an uppercase version of the method prepended to the path. This
|
183
|
+
# cannot conflict with the path itself, since paths must start with a slash.
|
184
|
+
#
|
185
|
+
# r.hmac_path('/1', method: :get)
|
186
|
+
# HMAC_hex(HMAC_hex(secret, ''), 'GET:/m/1')
|
187
|
+
#
|
188
|
+
# The +:params+ option includes the query string for the params in the HMAC:
|
189
|
+
#
|
190
|
+
# r.hmac_path('/1', params: {k: 2})
|
191
|
+
# HMAC_hex(HMAC_hex(secret, ''), '/p/1?k=2')
|
192
|
+
#
|
193
|
+
# If a +:namespace+ option is provided, the original secret used before the +:root+ option is
|
194
|
+
# an HMAC of the +:secret+ plugin option and the given namespace.
|
195
|
+
#
|
196
|
+
# r.hmac_path('/1', namespace: '2')
|
197
|
+
# HMAC_hex(HMAC_hex(HMAC(secret, '2'), ''), '/n/1')
|
198
|
+
module HmacPaths
|
199
|
+
def self.configure(app, opts=OPTS)
|
200
|
+
hmac_secret = opts[:secret]
|
201
|
+
unless hmac_secret.is_a?(String) && hmac_secret.bytesize >= 32
|
202
|
+
raise RodaError, "hmac_paths plugin :secret option must be a string containing at least 32 bytes"
|
203
|
+
end
|
204
|
+
|
205
|
+
if hmac_old_secret = opts[:old_secret]
|
206
|
+
unless hmac_old_secret.is_a?(String) && hmac_old_secret.bytesize >= 32
|
207
|
+
raise RodaError, "hmac_paths plugin :old_secret option must be a string containing at least 32 bytes if present"
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
app.opts[:hmac_paths_secret] = hmac_secret
|
212
|
+
app.opts[:hmac_paths_old_secret] = hmac_old_secret
|
213
|
+
|
214
|
+
if opts[:namespace_session_key]
|
215
|
+
app.opts[:hmac_paths_namespace_session_key] = opts[:namespace_session_key]
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
module InstanceMethods
|
220
|
+
# Return a path with an HMAC. Designed to be used with r.hmac_path, to make sure
|
221
|
+
# users can only request paths that they have been provided by the application
|
222
|
+
# (directly or indirectly). This can prevent users of a site from enumerating
|
223
|
+
# valid paths. The given path should be a string starting with +/+. Options:
|
224
|
+
#
|
225
|
+
# :method :: Limits the returned path to only be valid for the given request method.
|
226
|
+
# :namespace :: Make the HMAC value depend on the given namespace. If this is not
|
227
|
+
# provided, the default namespace is used. To explicitly not use a
|
228
|
+
# namespace when there is a default namespace, pass a nil value.
|
229
|
+
# :params :: Includes parameters in the query string of the returned path, and
|
230
|
+
# limits the returned path to only be valid for that exact query string.
|
231
|
+
# :root :: Should be an empty string or string starting with +/+. This will be
|
232
|
+
# the already matched path of the routing tree using r.hmac_path. Defaults
|
233
|
+
# to the empty string, which will returns paths valid for r.hmac_path at
|
234
|
+
# the top level of the routing tree.
|
235
|
+
def hmac_path(path, opts=OPTS)
|
236
|
+
unless path.is_a?(String) && path.getbyte(0) == 47
|
237
|
+
raise RodaError, "path must be a string starting with /"
|
238
|
+
end
|
239
|
+
|
240
|
+
root = opts[:root] || ''
|
241
|
+
unless root.is_a?(String) && ((root_byte = root.getbyte(0)) == 47 || root_byte == nil)
|
242
|
+
raise RodaError, "root must be empty string or string starting with /"
|
243
|
+
end
|
244
|
+
|
245
|
+
flags = String.new
|
246
|
+
path = path.dup
|
247
|
+
|
248
|
+
if method = opts[:method]
|
249
|
+
flags << 'm'
|
250
|
+
end
|
251
|
+
|
252
|
+
if params = opts[:params]
|
253
|
+
flags << 'p'
|
254
|
+
path << '?' << Rack::Utils.build_query(params)
|
255
|
+
end
|
256
|
+
|
257
|
+
if hmac_path_namespace(opts)
|
258
|
+
flags << 'n'
|
259
|
+
end
|
260
|
+
|
261
|
+
flags << '0' if flags.empty?
|
262
|
+
|
263
|
+
hmac_path = if method
|
264
|
+
"#{method.to_s.upcase}:/#{flags}#{path}"
|
265
|
+
else
|
266
|
+
"/#{flags}#{path}"
|
267
|
+
end
|
268
|
+
|
269
|
+
"#{root}/#{hmac_path_hmac(root, hmac_path, opts)}/#{flags}#{path}"
|
270
|
+
end
|
271
|
+
|
272
|
+
# The HMAC to use in hmac_path, for the given root, path, and options.
|
273
|
+
def hmac_path_hmac(root, path, opts=OPTS)
|
274
|
+
OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new, hmac_path_hmac_secret(root, opts), path)
|
275
|
+
end
|
276
|
+
|
277
|
+
# The namespace to use for the hmac path. If a :namespace option is not
|
278
|
+
# provided, and a :namespace_session_key option was provided, this will
|
279
|
+
# use the value of the related session key, if present.
|
280
|
+
def hmac_path_namespace(opts=OPTS)
|
281
|
+
opts.fetch(:namespace){hmac_path_default_namespace}
|
282
|
+
end
|
283
|
+
|
284
|
+
private
|
285
|
+
|
286
|
+
# The secret used to calculate the HMAC in hmac_path. This is itself an HMAC, created
|
287
|
+
# using the secret given in the plugin, for the given root and options.
|
288
|
+
# This always returns a hexidecimal string.
|
289
|
+
def hmac_path_hmac_secret(root, opts=OPTS)
|
290
|
+
secret = opts[:secret] || self.opts[:hmac_paths_secret]
|
291
|
+
|
292
|
+
if namespace = hmac_path_namespace(opts)
|
293
|
+
secret = OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new, secret, namespace)
|
294
|
+
end
|
295
|
+
|
296
|
+
OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new, secret, root)
|
297
|
+
end
|
298
|
+
|
299
|
+
# The default namespace to use for hmac_path, if a :namespace option is not provided.
|
300
|
+
def hmac_path_default_namespace
|
301
|
+
if (key = opts[:hmac_paths_namespace_session_key]) && (value = session[key])
|
302
|
+
value.to_s
|
303
|
+
end
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
module RequestMethods
|
308
|
+
# Looks at the first segment of the remaining path, and if it contains a valid HMAC for the
|
309
|
+
# rest of the path considering the flags in the second segment and the given options, the
|
310
|
+
# block matches and is yielded to, and the result of the block is returned. Otherwise, the
|
311
|
+
# block does not matches and routing continues after the call.
|
312
|
+
def hmac_path(opts=OPTS, &block)
|
313
|
+
orig_path = remaining_path
|
314
|
+
mpath = matched_path
|
315
|
+
|
316
|
+
on String do |submitted_hmac|
|
317
|
+
rpath = remaining_path
|
318
|
+
|
319
|
+
if submitted_hmac.bytesize == 64
|
320
|
+
on String do |flags|
|
321
|
+
if flags.bytesize >= 1
|
322
|
+
if flags.include?('n') ^ !scope.hmac_path_namespace(opts).nil?
|
323
|
+
# Namespace required and not provided, or provided and not required.
|
324
|
+
# Bail early to avoid unnecessary HMAC calculation.
|
325
|
+
@remaining_path = orig_path
|
326
|
+
return
|
327
|
+
end
|
328
|
+
|
329
|
+
if flags.include?('m')
|
330
|
+
rpath = "#{env['REQUEST_METHOD'].to_s.upcase}:#{rpath}"
|
331
|
+
end
|
332
|
+
|
333
|
+
if flags.include?('p')
|
334
|
+
rpath = "#{rpath}?#{env["QUERY_STRING"]}"
|
335
|
+
end
|
336
|
+
|
337
|
+
if hmac_path_valid?(mpath, rpath, submitted_hmac, opts)
|
338
|
+
always(&block)
|
339
|
+
end
|
340
|
+
end
|
341
|
+
|
342
|
+
# Return from method without matching
|
343
|
+
@remaining_path = orig_path
|
344
|
+
return
|
345
|
+
end
|
346
|
+
end
|
347
|
+
|
348
|
+
# Return from method without matching
|
349
|
+
@remaining_path = orig_path
|
350
|
+
return
|
351
|
+
end
|
352
|
+
end
|
353
|
+
|
354
|
+
private
|
355
|
+
|
356
|
+
# Determine whether the provided hmac matches.
|
357
|
+
def hmac_path_valid?(root, path, hmac, opts=OPTS)
|
358
|
+
if Rack::Utils.secure_compare(scope.hmac_path_hmac(root, path, opts), hmac)
|
359
|
+
true
|
360
|
+
elsif old_secret = roda_class.opts[:hmac_paths_old_secret]
|
361
|
+
opts = opts.dup
|
362
|
+
opts[:secret] = old_secret
|
363
|
+
Rack::Utils.secure_compare(scope.hmac_path_hmac(root, path, opts), hmac)
|
364
|
+
else
|
365
|
+
false
|
366
|
+
end
|
367
|
+
end
|
368
|
+
end
|
369
|
+
end
|
370
|
+
|
371
|
+
register_plugin(:hmac_paths, HmacPaths)
|
372
|
+
end
|
373
|
+
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.80.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-05-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rack
|
@@ -252,7 +252,9 @@ 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
|
257
|
+
- doc/release_notes/3.80.0.txt
|
256
258
|
- doc/release_notes/3.9.0.txt
|
257
259
|
files:
|
258
260
|
- CHANGELOG
|
@@ -337,7 +339,9 @@ files:
|
|
337
339
|
- doc/release_notes/3.76.0.txt
|
338
340
|
- doc/release_notes/3.77.0.txt
|
339
341
|
- doc/release_notes/3.78.0.txt
|
342
|
+
- doc/release_notes/3.79.0.txt
|
340
343
|
- doc/release_notes/3.8.0.txt
|
344
|
+
- doc/release_notes/3.80.0.txt
|
341
345
|
- doc/release_notes/3.9.0.txt
|
342
346
|
- lib/roda.rb
|
343
347
|
- lib/roda/cache.rb
|
@@ -399,6 +403,7 @@ files:
|
|
399
403
|
- lib/roda/plugins/head.rb
|
400
404
|
- lib/roda/plugins/header_matchers.rb
|
401
405
|
- lib/roda/plugins/heartbeat.rb
|
406
|
+
- lib/roda/plugins/hmac_paths.rb
|
402
407
|
- lib/roda/plugins/hooks.rb
|
403
408
|
- lib/roda/plugins/host_authorization.rb
|
404
409
|
- lib/roda/plugins/indifferent_params.rb
|
@@ -502,7 +507,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
502
507
|
- !ruby/object:Gem::Version
|
503
508
|
version: '0'
|
504
509
|
requirements: []
|
505
|
-
rubygems_version: 3.5.
|
510
|
+
rubygems_version: 3.5.9
|
506
511
|
signing_key:
|
507
512
|
specification_version: 4
|
508
513
|
summary: Routing tree web toolkit
|