actionpack 7.1.5.1 → 8.1.2
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.md +308 -523
- data/README.rdoc +1 -1
- data/lib/abstract_controller/asset_paths.rb +6 -2
- data/lib/abstract_controller/base.rb +104 -105
- data/lib/abstract_controller/caching/fragments.rb +50 -53
- data/lib/abstract_controller/caching.rb +8 -3
- data/lib/abstract_controller/callbacks.rb +70 -62
- data/lib/abstract_controller/collector.rb +7 -7
- data/lib/abstract_controller/deprecator.rb +2 -0
- data/lib/abstract_controller/error.rb +2 -0
- data/lib/abstract_controller/helpers.rb +71 -84
- data/lib/abstract_controller/logger.rb +4 -1
- data/lib/abstract_controller/railties/routes_helpers.rb +2 -0
- data/lib/abstract_controller/rendering.rb +13 -13
- data/lib/abstract_controller/translation.rb +12 -13
- data/lib/abstract_controller/url_for.rb +8 -6
- data/lib/abstract_controller.rb +2 -0
- data/lib/action_controller/api/api_rendering.rb +2 -0
- data/lib/action_controller/api.rb +76 -72
- data/lib/action_controller/base.rb +199 -126
- data/lib/action_controller/caching.rb +16 -14
- data/lib/action_controller/deprecator.rb +2 -0
- data/lib/action_controller/form_builder.rb +21 -18
- data/lib/action_controller/log_subscriber.rb +23 -2
- data/lib/action_controller/metal/allow_browser.rb +133 -0
- data/lib/action_controller/metal/basic_implicit_render.rb +2 -0
- data/lib/action_controller/metal/conditional_get.rb +217 -175
- data/lib/action_controller/metal/content_security_policy.rb +25 -24
- data/lib/action_controller/metal/cookies.rb +4 -2
- data/lib/action_controller/metal/data_streaming.rb +72 -63
- data/lib/action_controller/metal/default_headers.rb +5 -3
- data/lib/action_controller/metal/etag_with_flash.rb +3 -1
- data/lib/action_controller/metal/etag_with_template_digest.rb +17 -15
- data/lib/action_controller/metal/exceptions.rb +16 -9
- data/lib/action_controller/metal/flash.rb +13 -14
- data/lib/action_controller/metal/head.rb +15 -11
- data/lib/action_controller/metal/helpers.rb +63 -55
- data/lib/action_controller/metal/http_authentication.rb +209 -201
- data/lib/action_controller/metal/implicit_render.rb +17 -15
- data/lib/action_controller/metal/instrumentation.rb +16 -14
- data/lib/action_controller/metal/live.rb +177 -128
- data/lib/action_controller/metal/logging.rb +6 -4
- data/lib/action_controller/metal/mime_responds.rb +151 -142
- data/lib/action_controller/metal/parameter_encoding.rb +34 -32
- data/lib/action_controller/metal/params_wrapper.rb +57 -59
- data/lib/action_controller/metal/permissions_policy.rb +22 -12
- data/lib/action_controller/metal/rate_limiting.rb +92 -0
- data/lib/action_controller/metal/redirecting.rb +213 -94
- data/lib/action_controller/metal/renderers.rb +78 -57
- data/lib/action_controller/metal/rendering.rb +111 -77
- data/lib/action_controller/metal/request_forgery_protection.rb +182 -143
- data/lib/action_controller/metal/rescue.rb +20 -9
- data/lib/action_controller/metal/streaming.rb +118 -195
- data/lib/action_controller/metal/strong_parameters.rb +720 -530
- data/lib/action_controller/metal/testing.rb +2 -0
- data/lib/action_controller/metal/url_for.rb +17 -15
- data/lib/action_controller/metal.rb +86 -60
- data/lib/action_controller/railtie.rb +36 -15
- data/lib/action_controller/railties/helpers.rb +2 -0
- data/lib/action_controller/renderer.rb +41 -36
- data/lib/action_controller/structured_event_subscriber.rb +116 -0
- data/lib/action_controller/template_assertions.rb +4 -2
- data/lib/action_controller/test_case.rb +160 -131
- data/lib/action_controller.rb +5 -1
- data/lib/action_dispatch/constants.rb +8 -0
- data/lib/action_dispatch/deprecator.rb +2 -0
- data/lib/action_dispatch/http/cache.rb +163 -35
- data/lib/action_dispatch/http/content_disposition.rb +2 -0
- data/lib/action_dispatch/http/content_security_policy.rb +54 -39
- data/lib/action_dispatch/http/filter_parameters.rb +14 -8
- data/lib/action_dispatch/http/filter_redirect.rb +22 -1
- data/lib/action_dispatch/http/headers.rb +22 -22
- data/lib/action_dispatch/http/mime_negotiation.rb +89 -41
- data/lib/action_dispatch/http/mime_type.rb +25 -21
- data/lib/action_dispatch/http/mime_types.rb +3 -0
- data/lib/action_dispatch/http/param_builder.rb +187 -0
- data/lib/action_dispatch/http/param_error.rb +26 -0
- data/lib/action_dispatch/http/parameters.rb +14 -12
- data/lib/action_dispatch/http/permissions_policy.rb +25 -36
- data/lib/action_dispatch/http/query_parser.rb +55 -0
- data/lib/action_dispatch/http/rack_cache.rb +2 -0
- data/lib/action_dispatch/http/request.rb +141 -92
- data/lib/action_dispatch/http/response.rb +137 -77
- data/lib/action_dispatch/http/upload.rb +18 -16
- data/lib/action_dispatch/http/url.rb +187 -89
- data/lib/action_dispatch/journey/formatter.rb +21 -9
- data/lib/action_dispatch/journey/gtg/builder.rb +4 -3
- data/lib/action_dispatch/journey/gtg/simulator.rb +34 -11
- data/lib/action_dispatch/journey/gtg/transition_table.rb +47 -53
- data/lib/action_dispatch/journey/nfa/dot.rb +2 -0
- data/lib/action_dispatch/journey/nodes/node.rb +8 -6
- data/lib/action_dispatch/journey/parser.rb +99 -195
- data/lib/action_dispatch/journey/path/pattern.rb +4 -1
- data/lib/action_dispatch/journey/route.rb +54 -38
- data/lib/action_dispatch/journey/router/utils.rb +22 -27
- data/lib/action_dispatch/journey/router.rb +63 -83
- data/lib/action_dispatch/journey/routes.rb +11 -2
- data/lib/action_dispatch/journey/scanner.rb +46 -42
- data/lib/action_dispatch/journey/visitors.rb +57 -23
- data/lib/action_dispatch/journey/visualizer/fsm.js +4 -6
- data/lib/action_dispatch/journey.rb +2 -0
- data/lib/action_dispatch/log_subscriber.rb +7 -1
- data/lib/action_dispatch/middleware/actionable_exceptions.rb +2 -0
- data/lib/action_dispatch/middleware/assume_ssl.rb +8 -5
- data/lib/action_dispatch/middleware/callbacks.rb +3 -1
- data/lib/action_dispatch/middleware/cookies.rb +125 -106
- data/lib/action_dispatch/middleware/debug_exceptions.rb +37 -8
- data/lib/action_dispatch/middleware/debug_locks.rb +15 -13
- data/lib/action_dispatch/middleware/debug_view.rb +13 -5
- data/lib/action_dispatch/middleware/exception_wrapper.rb +18 -23
- data/lib/action_dispatch/middleware/executor.rb +19 -4
- data/lib/action_dispatch/middleware/flash.rb +63 -51
- data/lib/action_dispatch/middleware/host_authorization.rb +17 -15
- data/lib/action_dispatch/middleware/public_exceptions.rb +14 -12
- data/lib/action_dispatch/middleware/reloader.rb +5 -3
- data/lib/action_dispatch/middleware/remote_ip.rb +87 -77
- data/lib/action_dispatch/middleware/request_id.rb +16 -10
- data/lib/action_dispatch/middleware/server_timing.rb +4 -2
- data/lib/action_dispatch/middleware/session/abstract_store.rb +2 -0
- data/lib/action_dispatch/middleware/session/cache_store.rb +30 -8
- data/lib/action_dispatch/middleware/session/cookie_store.rb +27 -26
- data/lib/action_dispatch/middleware/session/mem_cache_store.rb +7 -3
- data/lib/action_dispatch/middleware/show_exceptions.rb +16 -16
- data/lib/action_dispatch/middleware/ssl.rb +53 -40
- data/lib/action_dispatch/middleware/stack.rb +11 -10
- data/lib/action_dispatch/middleware/static.rb +33 -31
- data/lib/action_dispatch/middleware/templates/rescues/_copy_button.html.erb +1 -0
- data/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +3 -5
- data/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +9 -5
- data/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb +1 -0
- data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +1 -0
- data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +4 -0
- data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +3 -0
- data/lib/action_dispatch/middleware/templates/rescues/layout.erb +50 -0
- data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb +1 -0
- data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +1 -0
- data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +1 -0
- data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +1 -0
- data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +1 -0
- data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +1 -1
- data/lib/action_dispatch/railtie.rb +23 -3
- data/lib/action_dispatch/request/session.rb +24 -21
- data/lib/action_dispatch/request/utils.rb +11 -3
- data/lib/action_dispatch/routing/endpoint.rb +2 -0
- data/lib/action_dispatch/routing/inspector.rb +85 -60
- data/lib/action_dispatch/routing/mapper.rb +1031 -851
- data/lib/action_dispatch/routing/polymorphic_routes.rb +69 -62
- data/lib/action_dispatch/routing/redirection.rb +47 -39
- data/lib/action_dispatch/routing/route_set.rb +79 -56
- data/lib/action_dispatch/routing/routes_proxy.rb +7 -4
- data/lib/action_dispatch/routing/url_for.rb +130 -125
- data/lib/action_dispatch/routing.rb +150 -148
- data/lib/action_dispatch/structured_event_subscriber.rb +20 -0
- data/lib/action_dispatch/system_test_case.rb +91 -81
- data/lib/action_dispatch/system_testing/browser.rb +16 -23
- data/lib/action_dispatch/system_testing/driver.rb +2 -0
- data/lib/action_dispatch/system_testing/server.rb +2 -0
- data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +34 -23
- data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +2 -0
- data/lib/action_dispatch/testing/assertion_response.rb +9 -7
- data/lib/action_dispatch/testing/assertions/response.rb +52 -25
- data/lib/action_dispatch/testing/assertions/routing.rb +168 -87
- data/lib/action_dispatch/testing/assertions.rb +2 -0
- data/lib/action_dispatch/testing/integration.rb +233 -223
- data/lib/action_dispatch/testing/request_encoder.rb +11 -9
- data/lib/action_dispatch/testing/test_helpers/page_dump_helper.rb +35 -0
- data/lib/action_dispatch/testing/test_process.rb +11 -8
- data/lib/action_dispatch/testing/test_request.rb +3 -1
- data/lib/action_dispatch/testing/test_response.rb +27 -26
- data/lib/action_dispatch.rb +36 -32
- data/lib/action_pack/gem_version.rb +6 -4
- data/lib/action_pack/version.rb +3 -1
- data/lib/action_pack.rb +17 -16
- metadata +36 -32
- data/lib/action_dispatch/journey/parser.y +0 -50
- data/lib/action_dispatch/journey/parser_extras.rb +0 -31
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# :markup: markdown
|
|
4
|
+
|
|
3
5
|
module ActionDispatch
|
|
4
6
|
module Http
|
|
5
7
|
module Cache
|
|
@@ -7,6 +9,8 @@ module ActionDispatch
|
|
|
7
9
|
HTTP_IF_MODIFIED_SINCE = "HTTP_IF_MODIFIED_SINCE"
|
|
8
10
|
HTTP_IF_NONE_MATCH = "HTTP_IF_NONE_MATCH"
|
|
9
11
|
|
|
12
|
+
mattr_accessor :strict_freshness, default: false
|
|
13
|
+
|
|
10
14
|
def if_modified_since
|
|
11
15
|
if since = get_header(HTTP_IF_MODIFIED_SINCE)
|
|
12
16
|
Time.rfc2822(since) rescue nil
|
|
@@ -32,19 +36,140 @@ module ActionDispatch
|
|
|
32
36
|
end
|
|
33
37
|
end
|
|
34
38
|
|
|
35
|
-
# Check response freshness (
|
|
36
|
-
#
|
|
37
|
-
# supplied,
|
|
39
|
+
# Check response freshness (`Last-Modified` and `ETag`) against request
|
|
40
|
+
# `If-Modified-Since` and `If-None-Match` conditions.
|
|
41
|
+
# If both headers are supplied, based on configuration, either `ETag` is preferred over `Last-Modified`
|
|
42
|
+
# or both are considered equally. You can adjust the preference with
|
|
43
|
+
# `config.action_dispatch.strict_freshness`.
|
|
44
|
+
# Reference: http://tools.ietf.org/html/rfc7232#section-6
|
|
38
45
|
def fresh?(response)
|
|
39
|
-
|
|
40
|
-
|
|
46
|
+
if Request.strict_freshness
|
|
47
|
+
if if_none_match
|
|
48
|
+
etag_matches?(response.etag)
|
|
49
|
+
elsif if_modified_since
|
|
50
|
+
not_modified?(response.last_modified)
|
|
51
|
+
else
|
|
52
|
+
false
|
|
53
|
+
end
|
|
54
|
+
else
|
|
55
|
+
last_modified = if_modified_since
|
|
56
|
+
etag = if_none_match
|
|
57
|
+
|
|
58
|
+
return false unless last_modified || etag
|
|
59
|
+
|
|
60
|
+
success = true
|
|
61
|
+
success &&= not_modified?(response.last_modified) if last_modified
|
|
62
|
+
success &&= etag_matches?(response.etag) if etag
|
|
63
|
+
success
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def cache_control_directives
|
|
68
|
+
@cache_control_directives ||= CacheControlDirectives.new(get_header("HTTP_CACHE_CONTROL"))
|
|
69
|
+
end
|
|
41
70
|
|
|
42
|
-
|
|
71
|
+
# Represents the HTTP Cache-Control header for requests,
|
|
72
|
+
# providing methods to access various cache control directives
|
|
73
|
+
# Reference: https://www.rfc-editor.org/rfc/rfc9111.html#name-request-directives
|
|
74
|
+
class CacheControlDirectives
|
|
75
|
+
def initialize(cache_control_header)
|
|
76
|
+
@only_if_cached = false
|
|
77
|
+
@no_cache = false
|
|
78
|
+
@no_store = false
|
|
79
|
+
@no_transform = false
|
|
80
|
+
@max_age = nil
|
|
81
|
+
@max_stale = nil
|
|
82
|
+
@min_fresh = nil
|
|
83
|
+
@stale_if_error = false
|
|
84
|
+
parse_directives(cache_control_header)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Returns true if the only-if-cached directive is present.
|
|
88
|
+
# This directive indicates that the client only wishes to obtain a
|
|
89
|
+
# stored response. If a valid stored response is not available,
|
|
90
|
+
# the server should respond with a 504 (Gateway Timeout) status.
|
|
91
|
+
def only_if_cached?
|
|
92
|
+
@only_if_cached
|
|
93
|
+
end
|
|
43
94
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
95
|
+
# Returns true if the no-cache directive is present.
|
|
96
|
+
# This directive indicates that a cache must not use the response
|
|
97
|
+
# to satisfy subsequent requests without successful validation on the origin server.
|
|
98
|
+
def no_cache?
|
|
99
|
+
@no_cache
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Returns true if the no-store directive is present.
|
|
103
|
+
# This directive indicates that a cache must not store any part of the
|
|
104
|
+
# request or response.
|
|
105
|
+
def no_store?
|
|
106
|
+
@no_store
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Returns true if the no-transform directive is present.
|
|
110
|
+
# This directive indicates that a cache or proxy must not transform the payload.
|
|
111
|
+
def no_transform?
|
|
112
|
+
@no_transform
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Returns the value of the max-age directive.
|
|
116
|
+
# This directive indicates that the client is willing to accept a response
|
|
117
|
+
# whose age is no greater than the specified number of seconds.
|
|
118
|
+
attr_reader :max_age
|
|
119
|
+
|
|
120
|
+
# Returns the value of the max-stale directive.
|
|
121
|
+
# When max-stale is present with a value, returns that integer value.
|
|
122
|
+
# When max-stale is present without a value, returns true (unlimited staleness).
|
|
123
|
+
# When max-stale is not present, returns nil.
|
|
124
|
+
attr_reader :max_stale
|
|
125
|
+
|
|
126
|
+
# Returns true if max-stale directive is present (with or without a value)
|
|
127
|
+
def max_stale?
|
|
128
|
+
!@max_stale.nil?
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Returns true if max-stale directive is present without a value (unlimited staleness)
|
|
132
|
+
def max_stale_unlimited?
|
|
133
|
+
@max_stale == true
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Returns the value of the min-fresh directive.
|
|
137
|
+
# This directive indicates that the client is willing to accept a response
|
|
138
|
+
# whose freshness lifetime is no less than its current age plus the specified time in seconds.
|
|
139
|
+
attr_reader :min_fresh
|
|
140
|
+
|
|
141
|
+
# Returns the value of the stale-if-error directive.
|
|
142
|
+
# This directive indicates that the client is willing to accept a stale response
|
|
143
|
+
# if the check for a fresh one fails with an error for the specified number of seconds.
|
|
144
|
+
attr_reader :stale_if_error
|
|
145
|
+
|
|
146
|
+
private
|
|
147
|
+
def parse_directives(header_value)
|
|
148
|
+
return unless header_value
|
|
149
|
+
|
|
150
|
+
header_value.delete(" ").downcase.split(",").each do |directive|
|
|
151
|
+
name, value = directive.split("=", 2)
|
|
152
|
+
|
|
153
|
+
case name
|
|
154
|
+
when "max-age"
|
|
155
|
+
@max_age = value.to_i
|
|
156
|
+
when "min-fresh"
|
|
157
|
+
@min_fresh = value.to_i
|
|
158
|
+
when "stale-if-error"
|
|
159
|
+
@stale_if_error = value.to_i
|
|
160
|
+
when "no-cache"
|
|
161
|
+
@no_cache = true
|
|
162
|
+
when "no-store"
|
|
163
|
+
@no_store = true
|
|
164
|
+
when "no-transform"
|
|
165
|
+
@no_transform = true
|
|
166
|
+
when "only-if-cached"
|
|
167
|
+
@only_if_cached = true
|
|
168
|
+
when "max-stale"
|
|
169
|
+
@max_stale = value ? value.to_i : true
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
48
173
|
end
|
|
49
174
|
end
|
|
50
175
|
|
|
@@ -79,25 +204,24 @@ module ActionDispatch
|
|
|
79
204
|
set_header DATE, utc_time.httpdate
|
|
80
205
|
end
|
|
81
206
|
|
|
82
|
-
# This method sets a weak ETag validator on the response so browsers
|
|
83
|
-
#
|
|
84
|
-
#
|
|
85
|
-
#
|
|
86
|
-
#
|
|
87
|
-
#
|
|
207
|
+
# This method sets a weak ETag validator on the response so browsers and proxies
|
|
208
|
+
# may cache the response, keyed on the ETag. On subsequent requests, the
|
|
209
|
+
# `If-None-Match` header is set to the cached ETag. If it matches the current
|
|
210
|
+
# ETag, we can return a `304 Not Modified` response with no body, letting the
|
|
211
|
+
# browser or proxy know that their cache is current. Big savings in request time
|
|
212
|
+
# and network bandwidth.
|
|
88
213
|
#
|
|
89
|
-
# Weak ETags are considered to be semantically equivalent but not
|
|
90
|
-
#
|
|
91
|
-
#
|
|
92
|
-
# is viewing.
|
|
214
|
+
# Weak ETags are considered to be semantically equivalent but not byte-for-byte
|
|
215
|
+
# identical. This is perfect for browser caching of HTML pages where we don't
|
|
216
|
+
# care about exact equality, just what the user is viewing.
|
|
93
217
|
#
|
|
94
|
-
# Strong ETags are considered byte-for-byte identical. They allow a
|
|
95
|
-
#
|
|
96
|
-
#
|
|
97
|
-
#
|
|
218
|
+
# Strong ETags are considered byte-for-byte identical. They allow a browser or
|
|
219
|
+
# proxy cache to support `Range` requests, useful for paging through a PDF file
|
|
220
|
+
# or scrubbing through a video. Some CDNs only support strong ETags and will
|
|
221
|
+
# ignore weak ETags entirely.
|
|
98
222
|
#
|
|
99
|
-
# Weak ETags are what we almost always need, so they're the default.
|
|
100
|
-
#
|
|
223
|
+
# Weak ETags are what we almost always need, so they're the default. Check out
|
|
224
|
+
# #strong_etag= to provide a strong ETag validator.
|
|
101
225
|
def etag=(weak_validators)
|
|
102
226
|
self.weak_etag = weak_validators
|
|
103
227
|
end
|
|
@@ -112,12 +236,13 @@ module ActionDispatch
|
|
|
112
236
|
|
|
113
237
|
def etag?; etag; end
|
|
114
238
|
|
|
115
|
-
# True if an ETag is set, and it's a weak validator (preceded with
|
|
239
|
+
# True if an ETag is set, and it's a weak validator (preceded with `W/`).
|
|
116
240
|
def weak_etag?
|
|
117
241
|
etag? && etag.start_with?('W/"')
|
|
118
242
|
end
|
|
119
243
|
|
|
120
|
-
# True if an ETag is set, and it isn't a weak validator (not preceded with
|
|
244
|
+
# True if an ETag is set, and it isn't a weak validator (not preceded with
|
|
245
|
+
# `W/`).
|
|
121
246
|
def strong_etag?
|
|
122
247
|
etag? && !weak_etag?
|
|
123
248
|
end
|
|
@@ -125,7 +250,7 @@ module ActionDispatch
|
|
|
125
250
|
private
|
|
126
251
|
DATE = "Date"
|
|
127
252
|
LAST_MODIFIED = "Last-Modified"
|
|
128
|
-
SPECIAL_KEYS = Set.new(%w[extras no-store no-cache max-age public private must-revalidate])
|
|
253
|
+
SPECIAL_KEYS = Set.new(%w[extras no-store no-cache max-age public private must-revalidate must-understand])
|
|
129
254
|
|
|
130
255
|
def generate_weak_etag(validators)
|
|
131
256
|
"W/#{generate_strong_etag(validators)}"
|
|
@@ -169,12 +294,13 @@ module ActionDispatch
|
|
|
169
294
|
PUBLIC = "public"
|
|
170
295
|
PRIVATE = "private"
|
|
171
296
|
MUST_REVALIDATE = "must-revalidate"
|
|
297
|
+
IMMUTABLE = "immutable"
|
|
298
|
+
MUST_UNDERSTAND = "must-understand"
|
|
172
299
|
|
|
173
300
|
def handle_conditional_get!
|
|
174
|
-
# Normally default cache control setting is handled by ETag
|
|
175
|
-
#
|
|
176
|
-
#
|
|
177
|
-
# previously set. So, set a default one here.
|
|
301
|
+
# Normally default cache control setting is handled by ETag middleware. But, if
|
|
302
|
+
# an etag is already set, the middleware defaults to `no-cache` unless a default
|
|
303
|
+
# `Cache-Control` value is previously set. So, set a default one here.
|
|
178
304
|
if (etag? || last_modified?) && !self._cache_control
|
|
179
305
|
self._cache_control = DEFAULT_CACHE_CONTROL
|
|
180
306
|
end
|
|
@@ -186,8 +312,8 @@ module ActionDispatch
|
|
|
186
312
|
return if control.empty? && cache_control.empty? # Let middleware handle default behavior
|
|
187
313
|
|
|
188
314
|
if cache_control.any?
|
|
189
|
-
# Any caching directive coming from a controller overrides
|
|
190
|
-
#
|
|
315
|
+
# Any caching directive coming from a controller overrides no-cache/no-store in
|
|
316
|
+
# the default Cache-Control header.
|
|
191
317
|
control.delete(:no_cache)
|
|
192
318
|
control.delete(:no_store)
|
|
193
319
|
|
|
@@ -204,6 +330,7 @@ module ActionDispatch
|
|
|
204
330
|
|
|
205
331
|
if control[:no_store]
|
|
206
332
|
options << PRIVATE if control[:private]
|
|
333
|
+
options << MUST_UNDERSTAND if control[:must_understand]
|
|
207
334
|
options << NO_STORE
|
|
208
335
|
elsif control[:no_cache]
|
|
209
336
|
options << PUBLIC if control[:public]
|
|
@@ -220,6 +347,7 @@ module ActionDispatch
|
|
|
220
347
|
options << MUST_REVALIDATE if control[:must_revalidate]
|
|
221
348
|
options << "stale-while-revalidate=#{stale_while_revalidate.to_i}" if stale_while_revalidate
|
|
222
349
|
options << "stale-if-error=#{stale_if_error.to_i}" if stale_if_error
|
|
350
|
+
options << IMMUTABLE if control[:immutable]
|
|
223
351
|
options.concat(extras) if extras
|
|
224
352
|
end
|
|
225
353
|
|
|
@@ -1,28 +1,30 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# :markup: markdown
|
|
4
|
+
|
|
3
5
|
require "active_support/core_ext/object/deep_dup"
|
|
4
6
|
require "active_support/core_ext/array/wrap"
|
|
5
7
|
|
|
6
8
|
module ActionDispatch # :nodoc:
|
|
7
|
-
#
|
|
9
|
+
# # Action Dispatch Content Security Policy
|
|
8
10
|
#
|
|
9
|
-
# Configures the HTTP
|
|
10
|
-
#
|
|
11
|
-
#
|
|
11
|
+
# Configures the HTTP [Content-Security-Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy)
|
|
12
|
+
# response header to help protect against XSS and
|
|
13
|
+
# injection attacks.
|
|
12
14
|
#
|
|
13
15
|
# Example global policy:
|
|
14
16
|
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
17
|
+
# Rails.application.config.content_security_policy do |policy|
|
|
18
|
+
# policy.default_src :self, :https
|
|
19
|
+
# policy.font_src :self, :https, :data
|
|
20
|
+
# policy.img_src :self, :https, :data
|
|
21
|
+
# policy.object_src :none
|
|
22
|
+
# policy.script_src :self, :https
|
|
23
|
+
# policy.style_src :self, :https
|
|
22
24
|
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
25
|
+
# # Specify URI for violation reports
|
|
26
|
+
# policy.report_uri "/csp-violation-report-endpoint"
|
|
27
|
+
# end
|
|
26
28
|
class ContentSecurityPolicy
|
|
27
29
|
class InvalidDirectiveError < StandardError
|
|
28
30
|
end
|
|
@@ -35,8 +37,8 @@ module ActionDispatch # :nodoc:
|
|
|
35
37
|
def call(env)
|
|
36
38
|
status, headers, _ = response = @app.call(env)
|
|
37
39
|
|
|
38
|
-
# Returning CSP headers with a 304 Not Modified is harmful, since nonces in the
|
|
39
|
-
# CSP headers might not match nonces in the cached HTML.
|
|
40
|
+
# Returning CSP headers with a 304 Not Modified is harmful, since nonces in the
|
|
41
|
+
# new CSP headers might not match nonces in the cached HTML.
|
|
40
42
|
return response if status == 304
|
|
41
43
|
|
|
42
44
|
return response if policy_present?(headers)
|
|
@@ -126,6 +128,7 @@ module ActionDispatch # :nodoc:
|
|
|
126
128
|
MAPPINGS = {
|
|
127
129
|
self: "'self'",
|
|
128
130
|
unsafe_eval: "'unsafe-eval'",
|
|
131
|
+
wasm_unsafe_eval: "'wasm-unsafe-eval'",
|
|
129
132
|
unsafe_hashes: "'unsafe-hashes'",
|
|
130
133
|
unsafe_inline: "'unsafe-inline'",
|
|
131
134
|
none: "'none'",
|
|
@@ -168,6 +171,8 @@ module ActionDispatch # :nodoc:
|
|
|
168
171
|
worker_src: "worker-src"
|
|
169
172
|
}.freeze
|
|
170
173
|
|
|
174
|
+
HASH_SOURCE_ALGORITHM_PREFIXES = ["sha256-", "sha384-", "sha512-"].freeze
|
|
175
|
+
|
|
171
176
|
DEFAULT_NONCE_DIRECTIVES = %w[script-src style-src].freeze
|
|
172
177
|
|
|
173
178
|
private_constant :MAPPINGS, :DIRECTIVES, :DEFAULT_NONCE_DIRECTIVES
|
|
@@ -193,14 +198,14 @@ module ActionDispatch # :nodoc:
|
|
|
193
198
|
end
|
|
194
199
|
end
|
|
195
200
|
|
|
196
|
-
# Specify whether to prevent the user agent from loading any assets over
|
|
197
|
-
#
|
|
201
|
+
# Specify whether to prevent the user agent from loading any assets over HTTP
|
|
202
|
+
# when the page uses HTTPS:
|
|
198
203
|
#
|
|
199
|
-
#
|
|
204
|
+
# policy.block_all_mixed_content
|
|
200
205
|
#
|
|
201
|
-
# Pass
|
|
206
|
+
# Pass `false` to allow it again:
|
|
202
207
|
#
|
|
203
|
-
#
|
|
208
|
+
# policy.block_all_mixed_content false
|
|
204
209
|
#
|
|
205
210
|
def block_all_mixed_content(enabled = true)
|
|
206
211
|
if enabled
|
|
@@ -212,11 +217,11 @@ module ActionDispatch # :nodoc:
|
|
|
212
217
|
|
|
213
218
|
# Restricts the set of plugins that can be embedded:
|
|
214
219
|
#
|
|
215
|
-
#
|
|
220
|
+
# policy.plugin_types "application/x-shockwave-flash"
|
|
216
221
|
#
|
|
217
222
|
# Leave empty to allow all plugins:
|
|
218
223
|
#
|
|
219
|
-
#
|
|
224
|
+
# policy.plugin_types
|
|
220
225
|
#
|
|
221
226
|
def plugin_types(*types)
|
|
222
227
|
if types.first
|
|
@@ -226,23 +231,23 @@ module ActionDispatch # :nodoc:
|
|
|
226
231
|
end
|
|
227
232
|
end
|
|
228
233
|
|
|
229
|
-
# Enable the
|
|
230
|
-
# directive. Violation reports will be sent to the
|
|
234
|
+
# Enable the [report-uri](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/report-uri)
|
|
235
|
+
# directive. Violation reports will be sent to the
|
|
236
|
+
# specified URI:
|
|
231
237
|
#
|
|
232
|
-
#
|
|
238
|
+
# policy.report_uri "/csp-violation-report-endpoint"
|
|
233
239
|
#
|
|
234
240
|
def report_uri(uri)
|
|
235
241
|
@directives["report-uri"] = [uri]
|
|
236
242
|
end
|
|
237
243
|
|
|
238
|
-
# Specify asset types for which
|
|
239
|
-
# is required:
|
|
244
|
+
# Specify asset types for which [Subresource Integrity](https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity) is required:
|
|
240
245
|
#
|
|
241
|
-
#
|
|
246
|
+
# policy.require_sri_for :script, :style
|
|
242
247
|
#
|
|
243
248
|
# Leave empty to not require Subresource Integrity:
|
|
244
249
|
#
|
|
245
|
-
#
|
|
250
|
+
# policy.require_sri_for
|
|
246
251
|
#
|
|
247
252
|
def require_sri_for(*types)
|
|
248
253
|
if types.first
|
|
@@ -252,18 +257,18 @@ module ActionDispatch # :nodoc:
|
|
|
252
257
|
end
|
|
253
258
|
end
|
|
254
259
|
|
|
255
|
-
# Specify whether a
|
|
260
|
+
# Specify whether a [sandbox](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/sandbox)
|
|
256
261
|
# should be enabled for the requested resource:
|
|
257
262
|
#
|
|
258
|
-
#
|
|
263
|
+
# policy.sandbox
|
|
259
264
|
#
|
|
260
265
|
# Values can be passed as arguments:
|
|
261
266
|
#
|
|
262
|
-
#
|
|
267
|
+
# policy.sandbox "allow-scripts", "allow-modals"
|
|
263
268
|
#
|
|
264
|
-
# Pass
|
|
269
|
+
# Pass `false` to disable the sandbox:
|
|
265
270
|
#
|
|
266
|
-
#
|
|
271
|
+
# policy.sandbox false
|
|
267
272
|
#
|
|
268
273
|
def sandbox(*values)
|
|
269
274
|
if values.empty?
|
|
@@ -277,11 +282,11 @@ module ActionDispatch # :nodoc:
|
|
|
277
282
|
|
|
278
283
|
# Specify whether user agents should treat any assets over HTTP as HTTPS:
|
|
279
284
|
#
|
|
280
|
-
#
|
|
285
|
+
# policy.upgrade_insecure_requests
|
|
281
286
|
#
|
|
282
|
-
# Pass
|
|
287
|
+
# Pass `false` to disable it:
|
|
283
288
|
#
|
|
284
|
-
#
|
|
289
|
+
# policy.upgrade_insecure_requests false
|
|
285
290
|
#
|
|
286
291
|
def upgrade_insecure_requests(enabled = true)
|
|
287
292
|
if enabled
|
|
@@ -302,7 +307,13 @@ module ActionDispatch # :nodoc:
|
|
|
302
307
|
case source
|
|
303
308
|
when Symbol
|
|
304
309
|
apply_mapping(source)
|
|
305
|
-
when String
|
|
310
|
+
when String
|
|
311
|
+
if hash_source?(source)
|
|
312
|
+
"'#{source}'"
|
|
313
|
+
else
|
|
314
|
+
source
|
|
315
|
+
end
|
|
316
|
+
when Proc
|
|
306
317
|
source
|
|
307
318
|
else
|
|
308
319
|
raise ArgumentError, "Invalid content security policy source: #{source.inspect}"
|
|
@@ -371,5 +382,9 @@ module ActionDispatch # :nodoc:
|
|
|
371
382
|
def nonce_directive?(directive, nonce_directives)
|
|
372
383
|
nonce_directives.include?(directive)
|
|
373
384
|
end
|
|
385
|
+
|
|
386
|
+
def hash_source?(source)
|
|
387
|
+
source.start_with?(*HASH_SOURCE_ALGORITHM_PREFIXES)
|
|
388
|
+
end
|
|
374
389
|
end
|
|
375
390
|
end
|
|
@@ -1,22 +1,27 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# :markup: markdown
|
|
4
|
+
|
|
3
5
|
require "active_support/parameter_filter"
|
|
4
6
|
|
|
5
7
|
module ActionDispatch
|
|
6
8
|
module Http
|
|
7
|
-
#
|
|
9
|
+
# # Action Dispatch HTTP Filter Parameters
|
|
8
10
|
#
|
|
9
11
|
# Allows you to specify sensitive query string and POST parameters to filter
|
|
10
12
|
# from the request log.
|
|
11
13
|
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
+
# # Replaces values with "[FILTERED]" for keys that match /foo|bar/i.
|
|
15
|
+
# env["action_dispatch.parameter_filter"] = [:foo, "bar"]
|
|
14
16
|
#
|
|
15
|
-
# For more information about filter behavior, see
|
|
17
|
+
# For more information about filter behavior, see
|
|
18
|
+
# ActiveSupport::ParameterFilter.
|
|
16
19
|
module FilterParameters
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
+
# :stopdoc:
|
|
21
|
+
ENV_MATCH = [/RAW_POST_DATA/, "rack.request.form_vars"]
|
|
22
|
+
NULL_PARAM_FILTER = ActiveSupport::ParameterFilter.new
|
|
23
|
+
NULL_ENV_FILTER = ActiveSupport::ParameterFilter.new ENV_MATCH
|
|
24
|
+
# :startdoc:
|
|
20
25
|
|
|
21
26
|
def initialize
|
|
22
27
|
super
|
|
@@ -43,7 +48,8 @@ module ActionDispatch
|
|
|
43
48
|
@filtered_path ||= query_string.empty? ? path : "#{path}?#{filtered_query_string}"
|
|
44
49
|
end
|
|
45
50
|
|
|
46
|
-
# Returns the
|
|
51
|
+
# Returns the `ActiveSupport::ParameterFilter` object used to filter in this
|
|
52
|
+
# request.
|
|
47
53
|
def parameter_filter
|
|
48
54
|
@parameter_filter ||= if has_header?("action_dispatch.parameter_filter")
|
|
49
55
|
parameter_filter_for get_header("action_dispatch.parameter_filter")
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# :markup: markdown
|
|
4
|
+
|
|
3
5
|
module ActionDispatch
|
|
4
6
|
module Http
|
|
5
7
|
module FilterRedirect
|
|
@@ -9,7 +11,7 @@ module ActionDispatch
|
|
|
9
11
|
if location_filter_match?
|
|
10
12
|
FILTERED
|
|
11
13
|
else
|
|
12
|
-
|
|
14
|
+
parameter_filtered_location
|
|
13
15
|
end
|
|
14
16
|
end
|
|
15
17
|
|
|
@@ -31,6 +33,25 @@ module ActionDispatch
|
|
|
31
33
|
end
|
|
32
34
|
end
|
|
33
35
|
end
|
|
36
|
+
|
|
37
|
+
def parameter_filtered_location
|
|
38
|
+
uri = URI.parse(location)
|
|
39
|
+
unless uri.query.nil? || uri.query.empty?
|
|
40
|
+
parts = uri.query.split(/([&;])/)
|
|
41
|
+
filtered_parts = parts.map do |part|
|
|
42
|
+
if part.include?("=")
|
|
43
|
+
key, value = part.split("=", 2)
|
|
44
|
+
request.parameter_filter.filter(key => value).first.join("=")
|
|
45
|
+
else
|
|
46
|
+
part
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
uri.query = filtered_parts.join("")
|
|
50
|
+
end
|
|
51
|
+
uri.to_s
|
|
52
|
+
rescue URI::Error
|
|
53
|
+
FILTERED
|
|
54
|
+
end
|
|
34
55
|
end
|
|
35
56
|
end
|
|
36
57
|
end
|
|
@@ -1,28 +1,30 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# :markup: markdown
|
|
4
|
+
|
|
3
5
|
module ActionDispatch
|
|
4
6
|
module Http
|
|
5
|
-
#
|
|
7
|
+
# # Action Dispatch HTTP Headers
|
|
6
8
|
#
|
|
7
9
|
# Provides access to the request's HTTP headers from the environment.
|
|
8
10
|
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
11
|
+
# env = { "CONTENT_TYPE" => "text/plain", "HTTP_USER_AGENT" => "curl/7.43.0" }
|
|
12
|
+
# headers = ActionDispatch::Http::Headers.from_hash(env)
|
|
13
|
+
# headers["Content-Type"] # => "text/plain"
|
|
14
|
+
# headers["User-Agent"] # => "curl/7.43.0"
|
|
13
15
|
#
|
|
14
16
|
# Also note that when headers are mapped to CGI-like variables by the Rack
|
|
15
17
|
# server, both dashes and underscores are converted to underscores. This
|
|
16
18
|
# ambiguity cannot be resolved at this stage anymore. Both underscores and
|
|
17
19
|
# dashes have to be interpreted as if they were originally sent as dashes.
|
|
18
20
|
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
21
|
+
# # GET / HTTP/1.1
|
|
22
|
+
# # ...
|
|
23
|
+
# # User-Agent: curl/7.43.0
|
|
24
|
+
# # X_Custom_Header: token
|
|
23
25
|
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
+
# headers["X_Custom_Header"] # => nil
|
|
27
|
+
# headers["X-Custom-Header"] # => "token"
|
|
26
28
|
class Headers
|
|
27
29
|
CGI_VARIABLES = Set.new(%W[
|
|
28
30
|
AUTH_TYPE
|
|
@@ -67,7 +69,7 @@ module ActionDispatch
|
|
|
67
69
|
@req.set_header env_name(key), value
|
|
68
70
|
end
|
|
69
71
|
|
|
70
|
-
# Add a value to a multivalued header like
|
|
72
|
+
# Add a value to a multivalued header like `Vary` or `Accept-Encoding`.
|
|
71
73
|
def add(key, value)
|
|
72
74
|
@req.add_header env_name(key), value
|
|
73
75
|
end
|
|
@@ -81,11 +83,10 @@ module ActionDispatch
|
|
|
81
83
|
|
|
82
84
|
# Returns the value for the given key mapped to @env.
|
|
83
85
|
#
|
|
84
|
-
# If the key is not found and an optional code block is not provided,
|
|
85
|
-
#
|
|
86
|
+
# If the key is not found and an optional code block is not provided, raises a
|
|
87
|
+
# `KeyError` exception.
|
|
86
88
|
#
|
|
87
|
-
# If the code block is provided, then it will be run and
|
|
88
|
-
# its result returned.
|
|
89
|
+
# If the code block is provided, then it will be run and its result returned.
|
|
89
90
|
def fetch(key, default = DEFAULT)
|
|
90
91
|
@req.fetch_header(env_name(key)) do
|
|
91
92
|
return default unless default == DEFAULT
|
|
@@ -99,16 +100,15 @@ module ActionDispatch
|
|
|
99
100
|
end
|
|
100
101
|
|
|
101
102
|
# Returns a new Http::Headers instance containing the contents of
|
|
102
|
-
#
|
|
103
|
+
# `headers_or_env` and the original instance.
|
|
103
104
|
def merge(headers_or_env)
|
|
104
105
|
headers = @req.dup.headers
|
|
105
106
|
headers.merge!(headers_or_env)
|
|
106
107
|
headers
|
|
107
108
|
end
|
|
108
109
|
|
|
109
|
-
# Adds the contents of
|
|
110
|
-
#
|
|
111
|
-
# <tt>headers_or_env</tt>.
|
|
110
|
+
# Adds the contents of `headers_or_env` to original instance entries; duplicate
|
|
111
|
+
# keys are overwritten with the values from `headers_or_env`.
|
|
112
112
|
def merge!(headers_or_env)
|
|
113
113
|
headers_or_env.each do |key, value|
|
|
114
114
|
@req.set_header env_name(key), value
|
|
@@ -118,8 +118,8 @@ module ActionDispatch
|
|
|
118
118
|
def env; @req.env.dup; end
|
|
119
119
|
|
|
120
120
|
private
|
|
121
|
-
# Converts an HTTP header name to an environment variable name if it is
|
|
122
|
-
#
|
|
121
|
+
# Converts an HTTP header name to an environment variable name if it is not
|
|
122
|
+
# contained within the headers hash.
|
|
123
123
|
def env_name(key)
|
|
124
124
|
key = key.to_s
|
|
125
125
|
if HTTP_HEADER.match?(key)
|