roda 3.77.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 +10 -0
- data/doc/release_notes/3.78.0.txt +99 -0
- data/doc/release_notes/3.79.0.txt +148 -0
- data/lib/roda/plugins/content_security_policy.rb +1 -1
- data/lib/roda/plugins/hmac_paths.rb +266 -0
- data/lib/roda/plugins/permissions_policy.rb +326 -0
- data/lib/roda/plugins/render.rb +15 -11
- data/lib/roda/response.rb +2 -1
- data/lib/roda/version.rb +1 -1
- metadata +8 -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,13 @@
|
|
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
|
+
|
7
|
+
= 3.78.0 (2024-03-13)
|
8
|
+
|
9
|
+
* Add permissions_policy plugin for setting Permissions-Policy header (jeremyevans)
|
10
|
+
|
1
11
|
= 3.77.0 (2024-02-12)
|
2
12
|
|
3
13
|
* Support formaction/formmethod attributes in forms in route_csrf plugin (jeremyevans)
|
@@ -0,0 +1,99 @@
|
|
1
|
+
= New Features
|
2
|
+
|
3
|
+
* A permissions_policy plugin has been added that allows you to easily set a
|
4
|
+
Permissions-Policy header for the application, which browsers can use to
|
5
|
+
determine whether to allow specific functionality on the returned page
|
6
|
+
(mainly related to which JavaScript APIs the page is allowed to use).
|
7
|
+
|
8
|
+
You would generally call the plugin with a block to set the default policy:
|
9
|
+
|
10
|
+
plugin :permissions_policy do |pp|
|
11
|
+
pp.camera :none
|
12
|
+
pp.fullscreen :self
|
13
|
+
pp.clipboard_read :self, 'https://example.com'
|
14
|
+
end
|
15
|
+
|
16
|
+
Then, anywhere in the routing tree, you can customize the policy for just that
|
17
|
+
branch or action using the same block syntax:
|
18
|
+
|
19
|
+
r.get 'foo' do
|
20
|
+
permissions_policy do |pp|
|
21
|
+
pp.camera :self
|
22
|
+
end
|
23
|
+
# ...
|
24
|
+
end
|
25
|
+
|
26
|
+
In addition to using a block, you can also call methods on the object returned
|
27
|
+
by the method:
|
28
|
+
|
29
|
+
r.get 'foo' do
|
30
|
+
permissions_policy.camera :self
|
31
|
+
# ...
|
32
|
+
end
|
33
|
+
|
34
|
+
You can use the :default plugin option to set the default for all settings.
|
35
|
+
For example, to disallow all access for each setting by default:
|
36
|
+
|
37
|
+
plugin :permissions_policy, default: :none
|
38
|
+
|
39
|
+
The following methods are available for configuring the permissions policy,
|
40
|
+
which specify the setting (substituting _ with -):
|
41
|
+
|
42
|
+
* accelerometer
|
43
|
+
* ambient_light_sensor
|
44
|
+
* autoplay
|
45
|
+
* bluetooth
|
46
|
+
* camera
|
47
|
+
* clipboard_read
|
48
|
+
* clipboard_write
|
49
|
+
* display_capture
|
50
|
+
* encrypted_media
|
51
|
+
* fullscreen
|
52
|
+
* geolocation
|
53
|
+
* gyroscope
|
54
|
+
* hid
|
55
|
+
* idle_detection
|
56
|
+
* keyboard_map
|
57
|
+
* magnetometer
|
58
|
+
* microphone
|
59
|
+
* midi
|
60
|
+
* payment
|
61
|
+
* picture_in_picture
|
62
|
+
* publickey_credentials_get
|
63
|
+
* screen_wake_lock
|
64
|
+
* serial
|
65
|
+
* sync_xhr
|
66
|
+
* usb
|
67
|
+
* web_share
|
68
|
+
* window_management
|
69
|
+
|
70
|
+
All of these methods support any number of arguments, and each argument should
|
71
|
+
be one of the following values:
|
72
|
+
|
73
|
+
:all :: Grants permission to all domains (must be only argument)
|
74
|
+
:none :: Does not allow permission at all (must be only argument)
|
75
|
+
:self :: Allows feature in current document and any nested browsing contexts
|
76
|
+
that use the same domain as the current document.
|
77
|
+
:src :: Allows feature in current document and any nested browsing contexts
|
78
|
+
that use the same domain as the src of the iframe.
|
79
|
+
String :: Specifies origin domain where access is allowed
|
80
|
+
|
81
|
+
When calling a method with no arguments, the setting is removed from the policy instead
|
82
|
+
of being left empty, since all of these setting require at least one value. Likewise,
|
83
|
+
if the policy does not have any settings, the header will not be added.
|
84
|
+
|
85
|
+
Calling the method overrides any previous setting. Each of the methods has +add_*+ and
|
86
|
+
+get_*+ methods defined. The +add_*+ method appends to any existing setting, and the +get_*+ method
|
87
|
+
returns the current value for the setting (this will be +:all+ if all domains are allowed, or
|
88
|
+
any array of strings/:self/:src).
|
89
|
+
|
90
|
+
permissions_policy.fullscreen :self, 'https://example.com'
|
91
|
+
# fullscreen (self "https://example.com")
|
92
|
+
|
93
|
+
permissions_policy.add_fullscreen 'https://*.example.com'
|
94
|
+
# fullscreen (self "https://example.com" "https://*.example.com")
|
95
|
+
|
96
|
+
permissions_policy.get_fullscreen
|
97
|
+
# => [:self, "https://example.com", "https://*.example.com"]
|
98
|
+
|
99
|
+
The clear method can be used to remove all settings from the policy.
|
@@ -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.
|
@@ -89,7 +89,7 @@ class Roda
|
|
89
89
|
# content_security_policy.add_script_src 'example.com', [:nonce, 'foobarbaz']
|
90
90
|
# # script-src 'self' 'unsafe-eval' example.com 'nonce-foobarbaz';
|
91
91
|
#
|
92
|
-
# content_security_policy.get_script_src
|
92
|
+
# content_security_policy.get_script_src
|
93
93
|
# # => [:self, :unsafe_eval, 'example.com', [:nonce, 'foobarbaz']]
|
94
94
|
#
|
95
95
|
# The clear method can be used to remove all settings from the policy.
|
@@ -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
|
@@ -0,0 +1,326 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
class Roda
|
5
|
+
module RodaPlugins
|
6
|
+
# A permissions_policy plugin has been added that allows you to easily set a
|
7
|
+
# Permissions-Policy header for the application, which browsers can use to
|
8
|
+
# determine whether to allow specific functionality on the returned page
|
9
|
+
# (mainly related to which JavaScript APIs the page is allowed to use).
|
10
|
+
#
|
11
|
+
# You would generally call the plugin with a block to set the default policy:
|
12
|
+
#
|
13
|
+
# plugin :permissions_policy do |pp|
|
14
|
+
# pp.camera :none
|
15
|
+
# pp.fullscreen :self
|
16
|
+
# pp.clipboard_read :self, 'https://example.com'
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# Then, anywhere in the routing tree, you can customize the policy for just that
|
20
|
+
# branch or action using the same block syntax:
|
21
|
+
#
|
22
|
+
# r.get 'foo' do
|
23
|
+
# permissions_policy do |pp|
|
24
|
+
# pp.camera :self
|
25
|
+
# end
|
26
|
+
# # ...
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# In addition to using a block, you can also call methods on the object returned
|
30
|
+
# by the method:
|
31
|
+
#
|
32
|
+
# r.get 'foo' do
|
33
|
+
# permissions_policy.camera :self
|
34
|
+
# # ...
|
35
|
+
# end
|
36
|
+
#
|
37
|
+
# You can use the :default plugin option to set the default for all settings.
|
38
|
+
# For example, to disallow all access for each setting by default:
|
39
|
+
#
|
40
|
+
# plugin :permissions_policy, default: :none
|
41
|
+
#
|
42
|
+
# The following methods are available for configuring the permissions policy,
|
43
|
+
# which specify the setting (substituting _ with -):
|
44
|
+
#
|
45
|
+
# * accelerometer
|
46
|
+
# * ambient_light_sensor
|
47
|
+
# * autoplay
|
48
|
+
# * bluetooth
|
49
|
+
# * camera
|
50
|
+
# * clipboard_read
|
51
|
+
# * clipboard_write
|
52
|
+
# * display_capture
|
53
|
+
# * encrypted_media
|
54
|
+
# * fullscreen
|
55
|
+
# * geolocation
|
56
|
+
# * gyroscope
|
57
|
+
# * hid
|
58
|
+
# * idle_detection
|
59
|
+
# * keyboard_map
|
60
|
+
# * magnetometer
|
61
|
+
# * microphone
|
62
|
+
# * midi
|
63
|
+
# * payment
|
64
|
+
# * picture_in_picture
|
65
|
+
# * publickey_credentials_get
|
66
|
+
# * screen_wake_lock
|
67
|
+
# * serial
|
68
|
+
# * sync_xhr
|
69
|
+
# * usb
|
70
|
+
# * web_share
|
71
|
+
# * window_management
|
72
|
+
#
|
73
|
+
# All of these methods support any number of arguments, and each argument should
|
74
|
+
# be one of the following values:
|
75
|
+
#
|
76
|
+
# :all :: Grants permission to all domains (must be only argument)
|
77
|
+
# :none :: Does not allow permission at all (must be only argument)
|
78
|
+
# :self :: Allows feature in current document and any nested browsing contexts
|
79
|
+
# that use the same domain as the current document.
|
80
|
+
# :src :: Allows feature in current document and any nested browsing contexts
|
81
|
+
# that use the same domain as the src of the iframe.
|
82
|
+
# String :: Specifies origin domain where access is allowed
|
83
|
+
#
|
84
|
+
# When calling a method with no arguments, the setting is removed from the policy instead
|
85
|
+
# of being left empty, since all of these setting require at least one value. Likewise,
|
86
|
+
# if the policy does not have any settings, the header will not be added.
|
87
|
+
#
|
88
|
+
# Calling the method overrides any previous setting. Each of the methods has +add_*+ and
|
89
|
+
# +get_*+ methods defined. The +add_*+ method appends to any existing setting, and the +get_*+ method
|
90
|
+
# returns the current value for the setting (this will be +:all+ if all domains are allowed, or
|
91
|
+
# any array of strings/:self/:src).
|
92
|
+
#
|
93
|
+
# permissions_policy.fullscreen :self, 'https://example.com'
|
94
|
+
# # fullscreen (self "https://example.com")
|
95
|
+
#
|
96
|
+
# permissions_policy.add_fullscreen 'https://*.example.com'
|
97
|
+
# # fullscreen (self "https://example.com" "https://*.example.com")
|
98
|
+
#
|
99
|
+
# permissions_policy.get_fullscreen
|
100
|
+
# # => [:self, "https://example.com", "https://*.example.com"]
|
101
|
+
#
|
102
|
+
# The clear method can be used to remove all settings from the policy.
|
103
|
+
module PermissionsPolicy
|
104
|
+
SUPPORTED_SETTINGS = %w'
|
105
|
+
accelerometer
|
106
|
+
ambient-light-sensor
|
107
|
+
autoplay
|
108
|
+
bluetooth
|
109
|
+
camera
|
110
|
+
clipboard-read
|
111
|
+
clipboard-write
|
112
|
+
display-capture
|
113
|
+
encrypted-media
|
114
|
+
fullscreen
|
115
|
+
geolocation
|
116
|
+
gyroscope
|
117
|
+
hid
|
118
|
+
idle-detection
|
119
|
+
keyboard-map
|
120
|
+
magnetometer
|
121
|
+
microphone
|
122
|
+
midi
|
123
|
+
payment
|
124
|
+
picture-in-picture
|
125
|
+
publickey-credentials-get
|
126
|
+
screen-wake-lock
|
127
|
+
serial
|
128
|
+
sync-xhr
|
129
|
+
usb
|
130
|
+
web-share
|
131
|
+
window-management
|
132
|
+
'.each(&:freeze).freeze
|
133
|
+
private_constant :SUPPORTED_SETTINGS
|
134
|
+
|
135
|
+
# Represents a permissions policy.
|
136
|
+
class Policy
|
137
|
+
SUPPORTED_SETTINGS.each do |setting|
|
138
|
+
meth = setting.gsub('-', '_').freeze
|
139
|
+
|
140
|
+
# Setting method name sets the setting value, or removes it if no args are given.
|
141
|
+
define_method(meth) do |*args|
|
142
|
+
if args.empty?
|
143
|
+
@opts.delete(setting)
|
144
|
+
else
|
145
|
+
@opts[setting] = option_value(args)
|
146
|
+
end
|
147
|
+
nil
|
148
|
+
end
|
149
|
+
|
150
|
+
# add_* method name adds to the setting value, or clears setting if no values
|
151
|
+
# are given.
|
152
|
+
define_method(:"add_#{meth}") do |*args|
|
153
|
+
unless args.empty?
|
154
|
+
case v = @opts[setting]
|
155
|
+
when :all
|
156
|
+
# If all domains are already allowed, there is no reason to add more.
|
157
|
+
return
|
158
|
+
when Array
|
159
|
+
@opts[setting] = option_value(v + args)
|
160
|
+
else
|
161
|
+
@opts[setting] = option_value(args)
|
162
|
+
end
|
163
|
+
end
|
164
|
+
nil
|
165
|
+
end
|
166
|
+
|
167
|
+
# get_* method always returns current setting value.
|
168
|
+
define_method(:"get_#{meth}") do
|
169
|
+
@opts[setting]
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
def initialize
|
174
|
+
clear
|
175
|
+
end
|
176
|
+
|
177
|
+
# Clear all settings, useful to remove any inherited settings.
|
178
|
+
def clear
|
179
|
+
@opts = {}
|
180
|
+
end
|
181
|
+
|
182
|
+
# Do not allow future modifications to any settings.
|
183
|
+
def freeze
|
184
|
+
@opts.freeze
|
185
|
+
header_value.freeze
|
186
|
+
super
|
187
|
+
end
|
188
|
+
|
189
|
+
# The header name to use, depends on whether report only mode has been enabled.
|
190
|
+
def header_key
|
191
|
+
@report_only ? RodaResponseHeaders::PERMISSIONS_POLICY_REPORT_ONLY : RodaResponseHeaders::PERMISSIONS_POLICY
|
192
|
+
end
|
193
|
+
|
194
|
+
# The header value to use.
|
195
|
+
def header_value
|
196
|
+
return @header_value if @header_value
|
197
|
+
|
198
|
+
s = String.new
|
199
|
+
@opts.each do |k, vs|
|
200
|
+
s << k << "="
|
201
|
+
|
202
|
+
if vs == :all
|
203
|
+
s << '*, '
|
204
|
+
else
|
205
|
+
s << '('
|
206
|
+
vs.each{|v| append_formatted_value(s, v)}
|
207
|
+
s.chop! unless vs.empty?
|
208
|
+
s << '), '
|
209
|
+
end
|
210
|
+
end
|
211
|
+
s.chop!
|
212
|
+
s.chop!
|
213
|
+
@header_value = s
|
214
|
+
end
|
215
|
+
|
216
|
+
# Set whether the Permissions-Policy-Report-Only header instead of the
|
217
|
+
# default Permissions-Policy header.
|
218
|
+
def report_only(report=true)
|
219
|
+
@report_only = report
|
220
|
+
end
|
221
|
+
|
222
|
+
# Whether this policy uses report only mode.
|
223
|
+
def report_only?
|
224
|
+
!!@report_only
|
225
|
+
end
|
226
|
+
|
227
|
+
# Set the current policy in the headers hash. If no settings have been made
|
228
|
+
# in the policy, does not set a header.
|
229
|
+
def set_header(headers)
|
230
|
+
return if @opts.empty?
|
231
|
+
headers[header_key] ||= header_value
|
232
|
+
end
|
233
|
+
|
234
|
+
private
|
235
|
+
|
236
|
+
# Formats nested values, quoting strings and using :self and :src verbatim.
|
237
|
+
def append_formatted_value(s, v)
|
238
|
+
case v
|
239
|
+
when String
|
240
|
+
s << v.inspect << ' '
|
241
|
+
when :self
|
242
|
+
s << 'self '
|
243
|
+
when :src
|
244
|
+
s << 'src '
|
245
|
+
else
|
246
|
+
raise RodaError, "unsupported Permissions-Policy item value used: #{v.inspect}"
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
# Make object copy use copy of settings, and remove cached header value.
|
251
|
+
def initialize_copy(_)
|
252
|
+
super
|
253
|
+
@opts = @opts.dup
|
254
|
+
@header_value = nil
|
255
|
+
end
|
256
|
+
|
257
|
+
# The option value to store for the given args.
|
258
|
+
def option_value(args)
|
259
|
+
if args.length == 1
|
260
|
+
case args[0]
|
261
|
+
when :all
|
262
|
+
:all
|
263
|
+
when :none
|
264
|
+
EMPTY_ARRAY
|
265
|
+
else
|
266
|
+
args.freeze
|
267
|
+
end
|
268
|
+
else
|
269
|
+
args.freeze
|
270
|
+
end
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
# Yield the current Permissions Policy to the block.
|
275
|
+
def self.configure(app, opts=OPTS)
|
276
|
+
policy = app.opts[:permissions_policy] = if policy = app.opts[:permissions_policy]
|
277
|
+
policy.dup
|
278
|
+
else
|
279
|
+
Policy.new
|
280
|
+
end
|
281
|
+
|
282
|
+
if default = opts[:default]
|
283
|
+
SUPPORTED_SETTINGS.each do |setting|
|
284
|
+
policy.send(setting.gsub('-', '_'), *default)
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
yield policy if defined?(yield)
|
289
|
+
policy.freeze
|
290
|
+
end
|
291
|
+
|
292
|
+
module InstanceMethods
|
293
|
+
# If a block is given, yield the current permission policy. Returns the
|
294
|
+
# current permissions policy.
|
295
|
+
def permissions_policy
|
296
|
+
policy = @_response.permissions_policy
|
297
|
+
yield policy if defined?(yield)
|
298
|
+
policy
|
299
|
+
end
|
300
|
+
end
|
301
|
+
|
302
|
+
module ResponseMethods
|
303
|
+
# Unset any permissions policy when reinitializing
|
304
|
+
def initialize
|
305
|
+
super
|
306
|
+
@permissions_policy &&= nil
|
307
|
+
end
|
308
|
+
|
309
|
+
# The current permissions policy to be used for this response.
|
310
|
+
def permissions_policy
|
311
|
+
@permissions_policy ||= roda_class.opts[:permissions_policy].dup
|
312
|
+
end
|
313
|
+
|
314
|
+
private
|
315
|
+
|
316
|
+
# Set the appropriate permissions policy header.
|
317
|
+
def set_default_headers
|
318
|
+
super
|
319
|
+
(@permissions_policy || roda_class.opts[:permissions_policy]).set_header(headers)
|
320
|
+
end
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
register_plugin(:permissions_policy, PermissionsPolicy)
|
325
|
+
end
|
326
|
+
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/response.rb
CHANGED
@@ -14,7 +14,8 @@ class Roda
|
|
14
14
|
|
15
15
|
%w'Allow Cache-Control Content-Disposition Content-Encoding Content-Length
|
16
16
|
Content-Security-Policy Content-Security-Policy-Report-Only Content-Type
|
17
|
-
ETag Expires Last-Modified Link Location Set-Cookie Transfer-Encoding Vary
|
17
|
+
ETag Expires Last-Modified Link Location Set-Cookie Transfer-Encoding Vary
|
18
|
+
Permissions-Policy Permissions-Policy-Report-Only'.
|
18
19
|
each do |value|
|
19
20
|
value = value.downcase if downcase
|
20
21
|
const_set(value.gsub('-', '_').upcase!.to_sym, value.freeze)
|
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
|
@@ -251,6 +251,8 @@ extra_rdoc_files:
|
|
251
251
|
- doc/release_notes/3.75.0.txt
|
252
252
|
- doc/release_notes/3.76.0.txt
|
253
253
|
- doc/release_notes/3.77.0.txt
|
254
|
+
- doc/release_notes/3.78.0.txt
|
255
|
+
- doc/release_notes/3.79.0.txt
|
254
256
|
- doc/release_notes/3.8.0.txt
|
255
257
|
- doc/release_notes/3.9.0.txt
|
256
258
|
files:
|
@@ -335,6 +337,8 @@ files:
|
|
335
337
|
- doc/release_notes/3.75.0.txt
|
336
338
|
- doc/release_notes/3.76.0.txt
|
337
339
|
- doc/release_notes/3.77.0.txt
|
340
|
+
- doc/release_notes/3.78.0.txt
|
341
|
+
- doc/release_notes/3.79.0.txt
|
338
342
|
- doc/release_notes/3.8.0.txt
|
339
343
|
- doc/release_notes/3.9.0.txt
|
340
344
|
- lib/roda.rb
|
@@ -397,6 +401,7 @@ files:
|
|
397
401
|
- lib/roda/plugins/head.rb
|
398
402
|
- lib/roda/plugins/header_matchers.rb
|
399
403
|
- lib/roda/plugins/heartbeat.rb
|
404
|
+
- lib/roda/plugins/hmac_paths.rb
|
400
405
|
- lib/roda/plugins/hooks.rb
|
401
406
|
- lib/roda/plugins/host_authorization.rb
|
402
407
|
- lib/roda/plugins/indifferent_params.rb
|
@@ -432,6 +437,7 @@ files:
|
|
432
437
|
- lib/roda/plugins/path.rb
|
433
438
|
- lib/roda/plugins/path_matchers.rb
|
434
439
|
- lib/roda/plugins/path_rewriter.rb
|
440
|
+
- lib/roda/plugins/permissions_policy.rb
|
435
441
|
- lib/roda/plugins/placeholder_string_matchers.rb
|
436
442
|
- lib/roda/plugins/plain_hash_response_headers.rb
|
437
443
|
- lib/roda/plugins/precompile_templates.rb
|