actionpack 4.2.11.1 → 5.2.4.3

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of actionpack might be problematic. Click here for more details.

Files changed (166) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +287 -488
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +6 -7
  5. data/lib/abstract_controller/asset_paths.rb +2 -0
  6. data/lib/abstract_controller/base.rb +45 -49
  7. data/lib/{action_controller → abstract_controller}/caching/fragments.rb +78 -15
  8. data/lib/abstract_controller/caching.rb +66 -0
  9. data/lib/abstract_controller/callbacks.rb +47 -31
  10. data/lib/abstract_controller/collector.rb +8 -11
  11. data/lib/abstract_controller/error.rb +6 -0
  12. data/lib/abstract_controller/helpers.rb +25 -25
  13. data/lib/abstract_controller/logger.rb +2 -0
  14. data/lib/abstract_controller/railties/routes_helpers.rb +4 -2
  15. data/lib/abstract_controller/rendering.rb +42 -41
  16. data/lib/abstract_controller/translation.rb +10 -7
  17. data/lib/abstract_controller/url_for.rb +2 -0
  18. data/lib/abstract_controller.rb +12 -5
  19. data/lib/action_controller/api/api_rendering.rb +16 -0
  20. data/lib/action_controller/api.rb +149 -0
  21. data/lib/action_controller/base.rb +27 -19
  22. data/lib/action_controller/caching.rb +14 -57
  23. data/lib/action_controller/form_builder.rb +50 -0
  24. data/lib/action_controller/log_subscriber.rb +10 -15
  25. data/lib/action_controller/metal/basic_implicit_render.rb +13 -0
  26. data/lib/action_controller/metal/conditional_get.rb +118 -44
  27. data/lib/action_controller/metal/content_security_policy.rb +52 -0
  28. data/lib/action_controller/metal/cookies.rb +3 -3
  29. data/lib/action_controller/metal/data_streaming.rb +27 -46
  30. data/lib/action_controller/metal/etag_with_flash.rb +18 -0
  31. data/lib/action_controller/metal/etag_with_template_digest.rb +20 -13
  32. data/lib/action_controller/metal/exceptions.rb +8 -14
  33. data/lib/action_controller/metal/flash.rb +4 -3
  34. data/lib/action_controller/metal/force_ssl.rb +23 -21
  35. data/lib/action_controller/metal/head.rb +21 -19
  36. data/lib/action_controller/metal/helpers.rb +24 -14
  37. data/lib/action_controller/metal/http_authentication.rb +64 -57
  38. data/lib/action_controller/metal/implicit_render.rb +62 -8
  39. data/lib/action_controller/metal/instrumentation.rb +19 -21
  40. data/lib/action_controller/metal/live.rb +90 -106
  41. data/lib/action_controller/metal/mime_responds.rb +33 -46
  42. data/lib/action_controller/metal/parameter_encoding.rb +51 -0
  43. data/lib/action_controller/metal/params_wrapper.rb +61 -53
  44. data/lib/action_controller/metal/redirecting.rb +49 -28
  45. data/lib/action_controller/metal/renderers.rb +87 -44
  46. data/lib/action_controller/metal/rendering.rb +72 -50
  47. data/lib/action_controller/metal/request_forgery_protection.rb +229 -93
  48. data/lib/action_controller/metal/rescue.rb +9 -16
  49. data/lib/action_controller/metal/streaming.rb +12 -10
  50. data/lib/action_controller/metal/strong_parameters.rb +583 -164
  51. data/lib/action_controller/metal/testing.rb +2 -17
  52. data/lib/action_controller/metal/url_for.rb +19 -10
  53. data/lib/action_controller/metal.rb +98 -83
  54. data/lib/action_controller/railtie.rb +28 -10
  55. data/lib/action_controller/railties/helpers.rb +2 -0
  56. data/lib/action_controller/renderer.rb +117 -0
  57. data/lib/action_controller/template_assertions.rb +11 -0
  58. data/lib/action_controller/test_case.rb +280 -411
  59. data/lib/action_controller.rb +29 -21
  60. data/lib/action_dispatch/http/cache.rb +93 -47
  61. data/lib/action_dispatch/http/content_security_policy.rb +272 -0
  62. data/lib/action_dispatch/http/filter_parameters.rb +26 -20
  63. data/lib/action_dispatch/http/filter_redirect.rb +10 -11
  64. data/lib/action_dispatch/http/headers.rb +55 -22
  65. data/lib/action_dispatch/http/mime_negotiation.rb +56 -41
  66. data/lib/action_dispatch/http/mime_type.rb +134 -121
  67. data/lib/action_dispatch/http/mime_types.rb +20 -6
  68. data/lib/action_dispatch/http/parameter_filter.rb +25 -11
  69. data/lib/action_dispatch/http/parameters.rb +98 -39
  70. data/lib/action_dispatch/http/rack_cache.rb +2 -0
  71. data/lib/action_dispatch/http/request.rb +200 -118
  72. data/lib/action_dispatch/http/response.rb +225 -110
  73. data/lib/action_dispatch/http/upload.rb +12 -6
  74. data/lib/action_dispatch/http/url.rb +110 -28
  75. data/lib/action_dispatch/journey/formatter.rb +55 -32
  76. data/lib/action_dispatch/journey/gtg/builder.rb +7 -5
  77. data/lib/action_dispatch/journey/gtg/simulator.rb +3 -9
  78. data/lib/action_dispatch/journey/gtg/transition_table.rb +17 -16
  79. data/lib/action_dispatch/journey/nfa/builder.rb +5 -3
  80. data/lib/action_dispatch/journey/nfa/dot.rb +13 -13
  81. data/lib/action_dispatch/journey/nfa/simulator.rb +3 -1
  82. data/lib/action_dispatch/journey/nfa/transition_table.rb +5 -48
  83. data/lib/action_dispatch/journey/nodes/node.rb +18 -6
  84. data/lib/action_dispatch/journey/parser.rb +23 -22
  85. data/lib/action_dispatch/journey/parser.y +3 -2
  86. data/lib/action_dispatch/journey/parser_extras.rb +12 -4
  87. data/lib/action_dispatch/journey/path/pattern.rb +50 -44
  88. data/lib/action_dispatch/journey/route.rb +106 -28
  89. data/lib/action_dispatch/journey/router/utils.rb +20 -11
  90. data/lib/action_dispatch/journey/router.rb +35 -23
  91. data/lib/action_dispatch/journey/routes.rb +18 -16
  92. data/lib/action_dispatch/journey/scanner.rb +18 -15
  93. data/lib/action_dispatch/journey/visitors.rb +99 -52
  94. data/lib/action_dispatch/journey.rb +7 -5
  95. data/lib/action_dispatch/middleware/callbacks.rb +1 -2
  96. data/lib/action_dispatch/middleware/cookies.rb +304 -193
  97. data/lib/action_dispatch/middleware/debug_exceptions.rb +152 -57
  98. data/lib/action_dispatch/middleware/debug_locks.rb +124 -0
  99. data/lib/action_dispatch/middleware/exception_wrapper.rb +68 -69
  100. data/lib/action_dispatch/middleware/executor.rb +21 -0
  101. data/lib/action_dispatch/middleware/flash.rb +78 -54
  102. data/lib/action_dispatch/middleware/public_exceptions.rb +27 -25
  103. data/lib/action_dispatch/middleware/reloader.rb +5 -91
  104. data/lib/action_dispatch/middleware/remote_ip.rb +41 -31
  105. data/lib/action_dispatch/middleware/request_id.rb +17 -9
  106. data/lib/action_dispatch/middleware/session/abstract_store.rb +41 -25
  107. data/lib/action_dispatch/middleware/session/cache_store.rb +24 -14
  108. data/lib/action_dispatch/middleware/session/cookie_store.rb +72 -67
  109. data/lib/action_dispatch/middleware/session/mem_cache_store.rb +8 -2
  110. data/lib/action_dispatch/middleware/show_exceptions.rb +26 -22
  111. data/lib/action_dispatch/middleware/ssl.rb +114 -36
  112. data/lib/action_dispatch/middleware/stack.rb +31 -44
  113. data/lib/action_dispatch/middleware/static.rb +57 -50
  114. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb +2 -14
  115. data/lib/action_dispatch/middleware/templates/rescues/{_source.erb → _source.html.erb} +0 -0
  116. data/lib/action_dispatch/middleware/templates/rescues/_source.text.erb +8 -0
  117. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +21 -0
  118. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +13 -0
  119. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +1 -0
  120. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +1 -1
  121. data/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb +1 -1
  122. data/lib/action_dispatch/middleware/templates/routes/_route.html.erb +4 -4
  123. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +64 -64
  124. data/lib/action_dispatch/railtie.rb +19 -11
  125. data/lib/action_dispatch/request/session.rb +106 -59
  126. data/lib/action_dispatch/request/utils.rb +67 -24
  127. data/lib/action_dispatch/routing/endpoint.rb +9 -2
  128. data/lib/action_dispatch/routing/inspector.rb +58 -67
  129. data/lib/action_dispatch/routing/mapper.rb +733 -447
  130. data/lib/action_dispatch/routing/polymorphic_routes.rb +161 -139
  131. data/lib/action_dispatch/routing/redirection.rb +36 -26
  132. data/lib/action_dispatch/routing/route_set.rb +321 -291
  133. data/lib/action_dispatch/routing/routes_proxy.rb +32 -5
  134. data/lib/action_dispatch/routing/url_for.rb +65 -25
  135. data/lib/action_dispatch/routing.rb +17 -18
  136. data/lib/action_dispatch/system_test_case.rb +147 -0
  137. data/lib/action_dispatch/system_testing/browser.rb +49 -0
  138. data/lib/action_dispatch/system_testing/driver.rb +59 -0
  139. data/lib/action_dispatch/system_testing/server.rb +31 -0
  140. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +96 -0
  141. data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +31 -0
  142. data/lib/action_dispatch/system_testing/test_helpers/undef_methods.rb +26 -0
  143. data/lib/action_dispatch/testing/assertion_response.rb +47 -0
  144. data/lib/action_dispatch/testing/assertions/response.rb +45 -20
  145. data/lib/action_dispatch/testing/assertions/routing.rb +30 -26
  146. data/lib/action_dispatch/testing/assertions.rb +6 -4
  147. data/lib/action_dispatch/testing/integration.rb +347 -209
  148. data/lib/action_dispatch/testing/request_encoder.rb +55 -0
  149. data/lib/action_dispatch/testing/test_process.rb +28 -22
  150. data/lib/action_dispatch/testing/test_request.rb +27 -34
  151. data/lib/action_dispatch/testing/test_response.rb +35 -7
  152. data/lib/action_dispatch.rb +27 -19
  153. data/lib/action_pack/gem_version.rb +5 -3
  154. data/lib/action_pack/version.rb +3 -1
  155. data/lib/action_pack.rb +4 -2
  156. metadata +56 -38
  157. data/lib/action_controller/metal/hide_actions.rb +0 -40
  158. data/lib/action_controller/metal/rack_delegation.rb +0 -32
  159. data/lib/action_controller/middleware.rb +0 -39
  160. data/lib/action_controller/model_naming.rb +0 -12
  161. data/lib/action_dispatch/journey/backwards.rb +0 -5
  162. data/lib/action_dispatch/journey/router/strexp.rb +0 -27
  163. data/lib/action_dispatch/middleware/params_parser.rb +0 -60
  164. data/lib/action_dispatch/testing/assertions/dom.rb +0 -3
  165. data/lib/action_dispatch/testing/assertions/selector.rb +0 -3
  166. data/lib/action_dispatch/testing/assertions/tag.rb +0 -3
@@ -1,34 +1,42 @@
1
- require 'active_support/rails'
2
- require 'abstract_controller'
3
- require 'action_dispatch'
4
- require 'action_controller/metal/live'
5
- require 'action_controller/metal/strong_parameters'
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/rails"
4
+ require "abstract_controller"
5
+ require "action_dispatch"
6
+ require "action_controller/metal/live"
7
+ require "action_controller/metal/strong_parameters"
6
8
 
7
9
  module ActionController
8
10
  extend ActiveSupport::Autoload
9
11
 
12
+ autoload :API
10
13
  autoload :Base
11
- autoload :Caching
12
14
  autoload :Metal
13
15
  autoload :Middleware
16
+ autoload :Renderer
17
+ autoload :FormBuilder
18
+
19
+ eager_autoload do
20
+ autoload :Caching
21
+ end
14
22
 
15
23
  autoload_under "metal" do
16
- autoload :Compatibility
17
24
  autoload :ConditionalGet
25
+ autoload :ContentSecurityPolicy
18
26
  autoload :Cookies
19
27
  autoload :DataStreaming
20
28
  autoload :EtagWithTemplateDigest
29
+ autoload :EtagWithFlash
21
30
  autoload :Flash
22
31
  autoload :ForceSSL
23
32
  autoload :Head
24
33
  autoload :Helpers
25
- autoload :HideActions
26
34
  autoload :HttpAuthentication
35
+ autoload :BasicImplicitRender
27
36
  autoload :ImplicitRender
28
37
  autoload :Instrumentation
29
38
  autoload :MimeResponds
30
39
  autoload :ParamsWrapper
31
- autoload :RackDelegation
32
40
  autoload :Redirecting
33
41
  autoload :Renderers
34
42
  autoload :Rendering
@@ -36,23 +44,23 @@ module ActionController
36
44
  autoload :Rescue
37
45
  autoload :Streaming
38
46
  autoload :StrongParameters
47
+ autoload :ParameterEncoding
39
48
  autoload :Testing
40
49
  autoload :UrlFor
41
50
  end
42
51
 
43
- autoload :TestCase, 'action_controller/test_case'
44
- autoload :TemplateAssertions, 'action_controller/test_case'
45
-
46
- def self.eager_load!
47
- super
48
- ActionController::Caching.eager_load!
52
+ autoload_under "api" do
53
+ autoload :ApiRendering
49
54
  end
55
+
56
+ autoload :TestCase, "action_controller/test_case"
57
+ autoload :TemplateAssertions, "action_controller/test_case"
50
58
  end
51
59
 
52
60
  # Common Active Support usage in Action Controller
53
- require 'active_support/core_ext/module/attribute_accessors'
54
- require 'active_support/core_ext/load_error'
55
- require 'active_support/core_ext/module/attr_internal'
56
- require 'active_support/core_ext/name_error'
57
- require 'active_support/core_ext/uri'
58
- require 'active_support/inflector'
61
+ require "active_support/core_ext/module/attribute_accessors"
62
+ require "active_support/core_ext/load_error"
63
+ require "active_support/core_ext/module/attr_internal"
64
+ require "active_support/core_ext/name_error"
65
+ require "active_support/core_ext/uri"
66
+ require "active_support/inflector"
@@ -1,26 +1,24 @@
1
+ # frozen_string_literal: true
1
2
 
2
3
  module ActionDispatch
3
4
  module Http
4
5
  module Cache
5
6
  module Request
6
-
7
- HTTP_IF_MODIFIED_SINCE = 'HTTP_IF_MODIFIED_SINCE'.freeze
8
- HTTP_IF_NONE_MATCH = 'HTTP_IF_NONE_MATCH'.freeze
7
+ HTTP_IF_MODIFIED_SINCE = "HTTP_IF_MODIFIED_SINCE".freeze
8
+ HTTP_IF_NONE_MATCH = "HTTP_IF_NONE_MATCH".freeze
9
9
 
10
10
  def if_modified_since
11
- if since = env[HTTP_IF_MODIFIED_SINCE]
11
+ if since = get_header(HTTP_IF_MODIFIED_SINCE)
12
12
  Time.rfc2822(since) rescue nil
13
13
  end
14
14
  end
15
15
 
16
16
  def if_none_match
17
- env[HTTP_IF_NONE_MATCH]
17
+ get_header HTTP_IF_NONE_MATCH
18
18
  end
19
19
 
20
20
  def if_none_match_etags
21
- (if_none_match ? if_none_match.split(/\s*,\s*/) : []).collect do |etag|
22
- etag.gsub(/^\"|\"$/, "")
23
- end
21
+ if_none_match ? if_none_match.split(/\s*,\s*/) : []
24
22
  end
25
23
 
26
24
  def not_modified?(modified_at)
@@ -29,8 +27,8 @@ module ActionDispatch
29
27
 
30
28
  def etag_matches?(etag)
31
29
  if etag
32
- etag = etag.gsub(/^\"|\"$/, "")
33
- if_none_match_etags.include?(etag)
30
+ validators = if_none_match_etags
31
+ validators.include?(etag) || validators.include?("*")
34
32
  end
35
33
  end
36
34
 
@@ -51,53 +49,96 @@ module ActionDispatch
51
49
  end
52
50
 
53
51
  module Response
54
- attr_reader :cache_control, :etag
55
- alias :etag? :etag
52
+ attr_reader :cache_control
56
53
 
57
54
  def last_modified
58
- if last = headers[LAST_MODIFIED]
55
+ if last = get_header(LAST_MODIFIED)
59
56
  Time.httpdate(last)
60
57
  end
61
58
  end
62
59
 
63
60
  def last_modified?
64
- headers.include?(LAST_MODIFIED)
61
+ has_header? LAST_MODIFIED
65
62
  end
66
63
 
67
64
  def last_modified=(utc_time)
68
- headers[LAST_MODIFIED] = utc_time.httpdate
65
+ set_header LAST_MODIFIED, utc_time.httpdate
69
66
  end
70
67
 
71
68
  def date
72
- if date_header = headers[DATE]
69
+ if date_header = get_header(DATE)
73
70
  Time.httpdate(date_header)
74
71
  end
75
72
  end
76
73
 
77
74
  def date?
78
- headers.include?(DATE)
75
+ has_header? DATE
79
76
  end
80
77
 
81
78
  def date=(utc_time)
82
- headers[DATE] = utc_time.httpdate
79
+ set_header DATE, utc_time.httpdate
80
+ end
81
+
82
+ # This method sets a weak ETag validator on the response so browsers
83
+ # and proxies may cache the response, keyed on the ETag. On subsequent
84
+ # requests, the If-None-Match header is set to the cached ETag. If it
85
+ # matches the current ETag, we can return a 304 Not Modified response
86
+ # with no body, letting the browser or proxy know that their cache is
87
+ # current. Big savings in request time and network bandwidth.
88
+ #
89
+ # Weak ETags are considered to be semantically equivalent but not
90
+ # byte-for-byte identical. This is perfect for browser caching of HTML
91
+ # pages where we don't care about exact equality, just what the user
92
+ # is viewing.
93
+ #
94
+ # Strong ETags are considered byte-for-byte identical. They allow a
95
+ # browser or proxy cache to support Range requests, useful for paging
96
+ # through a PDF file or scrubbing through a video. Some CDNs only
97
+ # support strong ETags and will ignore weak ETags entirely.
98
+ #
99
+ # Weak ETags are what we almost always need, so they're the default.
100
+ # Check out #strong_etag= to provide a strong ETag validator.
101
+ def etag=(weak_validators)
102
+ self.weak_etag = weak_validators
83
103
  end
84
104
 
85
- def etag=(etag)
86
- key = ActiveSupport::Cache.expand_cache_key(etag)
87
- @etag = self[ETAG] = %("#{Digest::MD5.hexdigest(key)}")
105
+ def weak_etag=(weak_validators)
106
+ set_header "ETag", generate_weak_etag(weak_validators)
107
+ end
108
+
109
+ def strong_etag=(strong_validators)
110
+ set_header "ETag", generate_strong_etag(strong_validators)
111
+ end
112
+
113
+ def etag?; etag; end
114
+
115
+ # True if an ETag is set and it's a weak validator (preceded with W/)
116
+ def weak_etag?
117
+ etag? && etag.starts_with?('W/"')
118
+ end
119
+
120
+ # True if an ETag is set and it isn't a weak validator (not preceded with W/)
121
+ def strong_etag?
122
+ etag? && !weak_etag?
88
123
  end
89
124
 
90
125
  private
91
126
 
92
- DATE = 'Date'.freeze
127
+ DATE = "Date".freeze
93
128
  LAST_MODIFIED = "Last-Modified".freeze
94
- ETAG = "ETag".freeze
95
- CACHE_CONTROL = "Cache-Control".freeze
96
- SPECIAL_KEYS = Set.new(%w[extras no-cache max-age public must-revalidate])
129
+ SPECIAL_KEYS = Set.new(%w[extras no-cache max-age public private must-revalidate])
130
+
131
+ def generate_weak_etag(validators)
132
+ "W/#{generate_strong_etag(validators)}"
133
+ end
134
+
135
+ def generate_strong_etag(validators)
136
+ %("#{ActiveSupport::Digest.hexdigest(ActiveSupport::Cache.expand_cache_key(validators))}")
137
+ end
97
138
 
98
139
  def cache_control_segments
99
- if cache_control = self[CACHE_CONTROL]
100
- cache_control.delete(' ').split(',')
140
+ if cache_control = _cache_control
141
+ cache_control.delete(" ").split(",")
101
142
  else
102
143
  []
103
144
  end
@@ -107,10 +148,10 @@ module ActionDispatch
107
148
  cache_control = {}
108
149
 
109
150
  cache_control_segments.each do |segment|
110
- directive, argument = segment.split('=', 2)
151
+ directive, argument = segment.split("=", 2)
111
152
 
112
153
  if SPECIAL_KEYS.include? directive
113
- key = directive.tr('-', '_')
154
+ key = directive.tr("-", "_")
114
155
  cache_control[key.to_sym] = argument || true
115
156
  else
116
157
  cache_control[:extras] ||= []
@@ -123,13 +164,6 @@ module ActionDispatch
123
164
 
124
165
  def prepare_cache_control!
125
166
  @cache_control = cache_control_headers
126
- @etag = self[ETAG]
127
- end
128
-
129
- def handle_conditional_get!
130
- if etag? || last_modified? || !@cache_control.empty?
131
- set_conditional_cache_control!
132
- end
133
167
  end
134
168
 
135
169
  DEFAULT_CACHE_CONTROL = "max-age=0, private, must-revalidate".freeze
@@ -138,25 +172,37 @@ module ActionDispatch
138
172
  PRIVATE = "private".freeze
139
173
  MUST_REVALIDATE = "must-revalidate".freeze
140
174
 
141
- def set_conditional_cache_control!
175
+ def handle_conditional_get!
176
+ # Normally default cache control setting is handled by ETag
177
+ # middleware. But, if an etag is already set, the middleware
178
+ # defaults to `no-cache` unless a default `Cache-Control` value is
179
+ # previously set. So, set a default one here.
180
+ if (etag? || last_modified?) && !self._cache_control
181
+ self._cache_control = DEFAULT_CACHE_CONTROL
182
+ end
183
+ end
184
+
185
+ def merge_and_normalize_cache_control!(cache_control)
142
186
  control = {}
143
187
  cc_headers = cache_control_headers
144
188
  if extras = cc_headers.delete(:extras)
145
- @cache_control[:extras] ||= []
146
- @cache_control[:extras] += extras
147
- @cache_control[:extras].uniq!
189
+ cache_control[:extras] ||= []
190
+ cache_control[:extras] += extras
191
+ cache_control[:extras].uniq!
148
192
  end
149
193
 
150
194
  control.merge! cc_headers
151
- control.merge! @cache_control
195
+ control.merge! cache_control
152
196
 
153
197
  if control.empty?
154
- headers[CACHE_CONTROL] = DEFAULT_CACHE_CONTROL
198
+ # Let middleware handle default behavior
155
199
  elsif control[:no_cache]
156
- headers[CACHE_CONTROL] = NO_CACHE
157
- if control[:extras]
158
- headers[CACHE_CONTROL] += ", #{control[:extras].join(', ')}"
159
- end
200
+ options = []
201
+ options << PUBLIC if control[:public]
202
+ options << NO_CACHE
203
+ options.concat(control[:extras]) if control[:extras]
204
+
205
+ self._cache_control = options.join(", ")
160
206
  else
161
207
  extras = control[:extras]
162
208
  max_age = control[:max_age]
@@ -167,7 +213,7 @@ module ActionDispatch
167
213
  options << MUST_REVALIDATE if control[:must_revalidate]
168
214
  options.concat(extras) if extras
169
215
 
170
- headers[CACHE_CONTROL] = options.join(", ")
216
+ self._cache_control = options.join(", ")
171
217
  end
172
218
  end
173
219
  end
@@ -0,0 +1,272 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/object/deep_dup"
4
+
5
+ module ActionDispatch #:nodoc:
6
+ class ContentSecurityPolicy
7
+ class Middleware
8
+ CONTENT_TYPE = "Content-Type".freeze
9
+ POLICY = "Content-Security-Policy".freeze
10
+ POLICY_REPORT_ONLY = "Content-Security-Policy-Report-Only".freeze
11
+
12
+ def initialize(app)
13
+ @app = app
14
+ end
15
+
16
+ def call(env)
17
+ request = ActionDispatch::Request.new env
18
+ _, headers, _ = response = @app.call(env)
19
+
20
+ return response unless html_response?(headers)
21
+ return response if policy_present?(headers)
22
+
23
+ if policy = request.content_security_policy
24
+ nonce = request.content_security_policy_nonce
25
+ context = request.controller_instance || request
26
+ headers[header_name(request)] = policy.build(context, nonce)
27
+ end
28
+
29
+ response
30
+ end
31
+
32
+ private
33
+
34
+ def html_response?(headers)
35
+ if content_type = headers[CONTENT_TYPE]
36
+ content_type =~ /html/
37
+ end
38
+ end
39
+
40
+ def header_name(request)
41
+ if request.content_security_policy_report_only
42
+ POLICY_REPORT_ONLY
43
+ else
44
+ POLICY
45
+ end
46
+ end
47
+
48
+ def policy_present?(headers)
49
+ headers[POLICY] || headers[POLICY_REPORT_ONLY]
50
+ end
51
+ end
52
+
53
+ module Request
54
+ POLICY = "action_dispatch.content_security_policy".freeze
55
+ POLICY_REPORT_ONLY = "action_dispatch.content_security_policy_report_only".freeze
56
+ NONCE_GENERATOR = "action_dispatch.content_security_policy_nonce_generator".freeze
57
+ NONCE = "action_dispatch.content_security_policy_nonce".freeze
58
+
59
+ def content_security_policy
60
+ get_header(POLICY)
61
+ end
62
+
63
+ def content_security_policy=(policy)
64
+ set_header(POLICY, policy)
65
+ end
66
+
67
+ def content_security_policy_report_only
68
+ get_header(POLICY_REPORT_ONLY)
69
+ end
70
+
71
+ def content_security_policy_report_only=(value)
72
+ set_header(POLICY_REPORT_ONLY, value)
73
+ end
74
+
75
+ def content_security_policy_nonce_generator
76
+ get_header(NONCE_GENERATOR)
77
+ end
78
+
79
+ def content_security_policy_nonce_generator=(generator)
80
+ set_header(NONCE_GENERATOR, generator)
81
+ end
82
+
83
+ def content_security_policy_nonce
84
+ if content_security_policy_nonce_generator
85
+ if nonce = get_header(NONCE)
86
+ nonce
87
+ else
88
+ set_header(NONCE, generate_content_security_policy_nonce)
89
+ end
90
+ end
91
+ end
92
+
93
+ private
94
+
95
+ def generate_content_security_policy_nonce
96
+ content_security_policy_nonce_generator.call(self)
97
+ end
98
+ end
99
+
100
+ MAPPINGS = {
101
+ self: "'self'",
102
+ unsafe_eval: "'unsafe-eval'",
103
+ unsafe_inline: "'unsafe-inline'",
104
+ none: "'none'",
105
+ http: "http:",
106
+ https: "https:",
107
+ data: "data:",
108
+ mediastream: "mediastream:",
109
+ blob: "blob:",
110
+ filesystem: "filesystem:",
111
+ report_sample: "'report-sample'",
112
+ strict_dynamic: "'strict-dynamic'",
113
+ ws: "ws:",
114
+ wss: "wss:"
115
+ }.freeze
116
+
117
+ DIRECTIVES = {
118
+ base_uri: "base-uri",
119
+ child_src: "child-src",
120
+ connect_src: "connect-src",
121
+ default_src: "default-src",
122
+ font_src: "font-src",
123
+ form_action: "form-action",
124
+ frame_ancestors: "frame-ancestors",
125
+ frame_src: "frame-src",
126
+ img_src: "img-src",
127
+ manifest_src: "manifest-src",
128
+ media_src: "media-src",
129
+ object_src: "object-src",
130
+ script_src: "script-src",
131
+ style_src: "style-src",
132
+ worker_src: "worker-src"
133
+ }.freeze
134
+
135
+ NONCE_DIRECTIVES = %w[script-src].freeze
136
+
137
+ private_constant :MAPPINGS, :DIRECTIVES, :NONCE_DIRECTIVES
138
+
139
+ attr_reader :directives
140
+
141
+ def initialize
142
+ @directives = {}
143
+ yield self if block_given?
144
+ end
145
+
146
+ def initialize_copy(other)
147
+ @directives = other.directives.deep_dup
148
+ end
149
+
150
+ DIRECTIVES.each do |name, directive|
151
+ define_method(name) do |*sources|
152
+ if sources.first
153
+ @directives[directive] = apply_mappings(sources)
154
+ else
155
+ @directives.delete(directive)
156
+ end
157
+ end
158
+ end
159
+
160
+ def block_all_mixed_content(enabled = true)
161
+ if enabled
162
+ @directives["block-all-mixed-content"] = true
163
+ else
164
+ @directives.delete("block-all-mixed-content")
165
+ end
166
+ end
167
+
168
+ def plugin_types(*types)
169
+ if types.first
170
+ @directives["plugin-types"] = types
171
+ else
172
+ @directives.delete("plugin-types")
173
+ end
174
+ end
175
+
176
+ def report_uri(uri)
177
+ @directives["report-uri"] = [uri]
178
+ end
179
+
180
+ def require_sri_for(*types)
181
+ if types.first
182
+ @directives["require-sri-for"] = types
183
+ else
184
+ @directives.delete("require-sri-for")
185
+ end
186
+ end
187
+
188
+ def sandbox(*values)
189
+ if values.empty?
190
+ @directives["sandbox"] = true
191
+ elsif values.first
192
+ @directives["sandbox"] = values
193
+ else
194
+ @directives.delete("sandbox")
195
+ end
196
+ end
197
+
198
+ def upgrade_insecure_requests(enabled = true)
199
+ if enabled
200
+ @directives["upgrade-insecure-requests"] = true
201
+ else
202
+ @directives.delete("upgrade-insecure-requests")
203
+ end
204
+ end
205
+
206
+ def build(context = nil, nonce = nil)
207
+ build_directives(context, nonce).compact.join("; ")
208
+ end
209
+
210
+ private
211
+ def apply_mappings(sources)
212
+ sources.map do |source|
213
+ case source
214
+ when Symbol
215
+ apply_mapping(source)
216
+ when String, Proc
217
+ source
218
+ else
219
+ raise ArgumentError, "Invalid content security policy source: #{source.inspect}"
220
+ end
221
+ end
222
+ end
223
+
224
+ def apply_mapping(source)
225
+ MAPPINGS.fetch(source) do
226
+ raise ArgumentError, "Unknown content security policy source mapping: #{source.inspect}"
227
+ end
228
+ end
229
+
230
+ def build_directives(context, nonce)
231
+ @directives.map do |directive, sources|
232
+ if sources.is_a?(Array)
233
+ if nonce && nonce_directive?(directive)
234
+ "#{directive} #{build_directive(sources, context).join(' ')} 'nonce-#{nonce}'"
235
+ else
236
+ "#{directive} #{build_directive(sources, context).join(' ')}"
237
+ end
238
+ elsif sources
239
+ directive
240
+ else
241
+ nil
242
+ end
243
+ end
244
+ end
245
+
246
+ def build_directive(sources, context)
247
+ sources.map { |source| resolve_source(source, context) }
248
+ end
249
+
250
+ def resolve_source(source, context)
251
+ case source
252
+ when String
253
+ source
254
+ when Symbol
255
+ source.to_s
256
+ when Proc
257
+ if context.nil?
258
+ raise RuntimeError, "Missing context for the dynamic content security policy source: #{source.inspect}"
259
+ else
260
+ resolved = context.instance_exec(&source)
261
+ resolved.is_a?(Symbol) ? apply_mapping(resolved) : resolved
262
+ end
263
+ else
264
+ raise RuntimeError, "Unexpected content security policy source: #{source.inspect}"
265
+ end
266
+ end
267
+
268
+ def nonce_directive?(directive)
269
+ NONCE_DIRECTIVES.include?(directive)
270
+ end
271
+ end
272
+ end