roda 3.77.0 → 3.79.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.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
|