actionpack 5.2.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 (170) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +429 -0
  3. data/MIT-LICENSE +21 -0
  4. data/README.rdoc +57 -0
  5. data/lib/abstract_controller.rb +27 -0
  6. data/lib/abstract_controller/asset_paths.rb +12 -0
  7. data/lib/abstract_controller/base.rb +265 -0
  8. data/lib/abstract_controller/caching.rb +66 -0
  9. data/lib/abstract_controller/caching/fragments.rb +166 -0
  10. data/lib/abstract_controller/callbacks.rb +212 -0
  11. data/lib/abstract_controller/collector.rb +43 -0
  12. data/lib/abstract_controller/error.rb +6 -0
  13. data/lib/abstract_controller/helpers.rb +194 -0
  14. data/lib/abstract_controller/logger.rb +14 -0
  15. data/lib/abstract_controller/railties/routes_helpers.rb +20 -0
  16. data/lib/abstract_controller/rendering.rb +127 -0
  17. data/lib/abstract_controller/translation.rb +31 -0
  18. data/lib/abstract_controller/url_for.rb +35 -0
  19. data/lib/action_controller.rb +66 -0
  20. data/lib/action_controller/api.rb +149 -0
  21. data/lib/action_controller/api/api_rendering.rb +16 -0
  22. data/lib/action_controller/base.rb +276 -0
  23. data/lib/action_controller/caching.rb +46 -0
  24. data/lib/action_controller/form_builder.rb +50 -0
  25. data/lib/action_controller/log_subscriber.rb +78 -0
  26. data/lib/action_controller/metal.rb +256 -0
  27. data/lib/action_controller/metal/basic_implicit_render.rb +13 -0
  28. data/lib/action_controller/metal/conditional_get.rb +274 -0
  29. data/lib/action_controller/metal/content_security_policy.rb +52 -0
  30. data/lib/action_controller/metal/cookies.rb +16 -0
  31. data/lib/action_controller/metal/data_streaming.rb +152 -0
  32. data/lib/action_controller/metal/etag_with_flash.rb +18 -0
  33. data/lib/action_controller/metal/etag_with_template_digest.rb +57 -0
  34. data/lib/action_controller/metal/exceptions.rb +53 -0
  35. data/lib/action_controller/metal/flash.rb +61 -0
  36. data/lib/action_controller/metal/force_ssl.rb +99 -0
  37. data/lib/action_controller/metal/head.rb +60 -0
  38. data/lib/action_controller/metal/helpers.rb +123 -0
  39. data/lib/action_controller/metal/http_authentication.rb +519 -0
  40. data/lib/action_controller/metal/implicit_render.rb +73 -0
  41. data/lib/action_controller/metal/instrumentation.rb +107 -0
  42. data/lib/action_controller/metal/live.rb +312 -0
  43. data/lib/action_controller/metal/mime_responds.rb +313 -0
  44. data/lib/action_controller/metal/parameter_encoding.rb +51 -0
  45. data/lib/action_controller/metal/params_wrapper.rb +293 -0
  46. data/lib/action_controller/metal/redirecting.rb +133 -0
  47. data/lib/action_controller/metal/renderers.rb +181 -0
  48. data/lib/action_controller/metal/rendering.rb +122 -0
  49. data/lib/action_controller/metal/request_forgery_protection.rb +445 -0
  50. data/lib/action_controller/metal/rescue.rb +28 -0
  51. data/lib/action_controller/metal/streaming.rb +223 -0
  52. data/lib/action_controller/metal/strong_parameters.rb +1086 -0
  53. data/lib/action_controller/metal/testing.rb +16 -0
  54. data/lib/action_controller/metal/url_for.rb +58 -0
  55. data/lib/action_controller/railtie.rb +89 -0
  56. data/lib/action_controller/railties/helpers.rb +24 -0
  57. data/lib/action_controller/renderer.rb +117 -0
  58. data/lib/action_controller/template_assertions.rb +11 -0
  59. data/lib/action_controller/test_case.rb +629 -0
  60. data/lib/action_dispatch.rb +112 -0
  61. data/lib/action_dispatch/http/cache.rb +222 -0
  62. data/lib/action_dispatch/http/content_security_policy.rb +272 -0
  63. data/lib/action_dispatch/http/filter_parameters.rb +84 -0
  64. data/lib/action_dispatch/http/filter_redirect.rb +37 -0
  65. data/lib/action_dispatch/http/headers.rb +132 -0
  66. data/lib/action_dispatch/http/mime_negotiation.rb +175 -0
  67. data/lib/action_dispatch/http/mime_type.rb +342 -0
  68. data/lib/action_dispatch/http/mime_types.rb +50 -0
  69. data/lib/action_dispatch/http/parameter_filter.rb +86 -0
  70. data/lib/action_dispatch/http/parameters.rb +126 -0
  71. data/lib/action_dispatch/http/rack_cache.rb +63 -0
  72. data/lib/action_dispatch/http/request.rb +430 -0
  73. data/lib/action_dispatch/http/response.rb +519 -0
  74. data/lib/action_dispatch/http/upload.rb +84 -0
  75. data/lib/action_dispatch/http/url.rb +350 -0
  76. data/lib/action_dispatch/journey.rb +7 -0
  77. data/lib/action_dispatch/journey/formatter.rb +189 -0
  78. data/lib/action_dispatch/journey/gtg/builder.rb +164 -0
  79. data/lib/action_dispatch/journey/gtg/simulator.rb +41 -0
  80. data/lib/action_dispatch/journey/gtg/transition_table.rb +158 -0
  81. data/lib/action_dispatch/journey/nfa/builder.rb +78 -0
  82. data/lib/action_dispatch/journey/nfa/dot.rb +36 -0
  83. data/lib/action_dispatch/journey/nfa/simulator.rb +49 -0
  84. data/lib/action_dispatch/journey/nfa/transition_table.rb +120 -0
  85. data/lib/action_dispatch/journey/nodes/node.rb +140 -0
  86. data/lib/action_dispatch/journey/parser.rb +199 -0
  87. data/lib/action_dispatch/journey/parser.y +50 -0
  88. data/lib/action_dispatch/journey/parser_extras.rb +31 -0
  89. data/lib/action_dispatch/journey/path/pattern.rb +198 -0
  90. data/lib/action_dispatch/journey/route.rb +203 -0
  91. data/lib/action_dispatch/journey/router.rb +156 -0
  92. data/lib/action_dispatch/journey/router/utils.rb +102 -0
  93. data/lib/action_dispatch/journey/routes.rb +82 -0
  94. data/lib/action_dispatch/journey/scanner.rb +64 -0
  95. data/lib/action_dispatch/journey/visitors.rb +268 -0
  96. data/lib/action_dispatch/journey/visualizer/fsm.css +30 -0
  97. data/lib/action_dispatch/journey/visualizer/fsm.js +134 -0
  98. data/lib/action_dispatch/journey/visualizer/index.html.erb +52 -0
  99. data/lib/action_dispatch/middleware/callbacks.rb +36 -0
  100. data/lib/action_dispatch/middleware/cookies.rb +685 -0
  101. data/lib/action_dispatch/middleware/debug_exceptions.rb +205 -0
  102. data/lib/action_dispatch/middleware/debug_locks.rb +124 -0
  103. data/lib/action_dispatch/middleware/exception_wrapper.rb +147 -0
  104. data/lib/action_dispatch/middleware/executor.rb +21 -0
  105. data/lib/action_dispatch/middleware/flash.rb +300 -0
  106. data/lib/action_dispatch/middleware/public_exceptions.rb +57 -0
  107. data/lib/action_dispatch/middleware/reloader.rb +12 -0
  108. data/lib/action_dispatch/middleware/remote_ip.rb +183 -0
  109. data/lib/action_dispatch/middleware/request_id.rb +43 -0
  110. data/lib/action_dispatch/middleware/session/abstract_store.rb +92 -0
  111. data/lib/action_dispatch/middleware/session/cache_store.rb +54 -0
  112. data/lib/action_dispatch/middleware/session/cookie_store.rb +118 -0
  113. data/lib/action_dispatch/middleware/session/mem_cache_store.rb +28 -0
  114. data/lib/action_dispatch/middleware/show_exceptions.rb +62 -0
  115. data/lib/action_dispatch/middleware/ssl.rb +150 -0
  116. data/lib/action_dispatch/middleware/stack.rb +116 -0
  117. data/lib/action_dispatch/middleware/static.rb +130 -0
  118. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb +22 -0
  119. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.text.erb +23 -0
  120. data/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +27 -0
  121. data/lib/action_dispatch/middleware/templates/rescues/_source.text.erb +8 -0
  122. data/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +52 -0
  123. data/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb +9 -0
  124. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +16 -0
  125. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb +9 -0
  126. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +21 -0
  127. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +13 -0
  128. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +161 -0
  129. data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +11 -0
  130. data/lib/action_dispatch/middleware/templates/rescues/missing_template.text.erb +3 -0
  131. data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +32 -0
  132. data/lib/action_dispatch/middleware/templates/rescues/routing_error.text.erb +11 -0
  133. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +20 -0
  134. data/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb +7 -0
  135. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +6 -0
  136. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.text.erb +3 -0
  137. data/lib/action_dispatch/middleware/templates/routes/_route.html.erb +16 -0
  138. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +200 -0
  139. data/lib/action_dispatch/railtie.rb +55 -0
  140. data/lib/action_dispatch/request/session.rb +234 -0
  141. data/lib/action_dispatch/request/utils.rb +78 -0
  142. data/lib/action_dispatch/routing.rb +260 -0
  143. data/lib/action_dispatch/routing/endpoint.rb +17 -0
  144. data/lib/action_dispatch/routing/inspector.rb +225 -0
  145. data/lib/action_dispatch/routing/mapper.rb +2267 -0
  146. data/lib/action_dispatch/routing/polymorphic_routes.rb +352 -0
  147. data/lib/action_dispatch/routing/redirection.rb +201 -0
  148. data/lib/action_dispatch/routing/route_set.rb +890 -0
  149. data/lib/action_dispatch/routing/routes_proxy.rb +69 -0
  150. data/lib/action_dispatch/routing/url_for.rb +236 -0
  151. data/lib/action_dispatch/system_test_case.rb +147 -0
  152. data/lib/action_dispatch/system_testing/browser.rb +49 -0
  153. data/lib/action_dispatch/system_testing/driver.rb +59 -0
  154. data/lib/action_dispatch/system_testing/server.rb +31 -0
  155. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +96 -0
  156. data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +31 -0
  157. data/lib/action_dispatch/system_testing/test_helpers/undef_methods.rb +26 -0
  158. data/lib/action_dispatch/testing/assertion_response.rb +47 -0
  159. data/lib/action_dispatch/testing/assertions.rb +24 -0
  160. data/lib/action_dispatch/testing/assertions/response.rb +107 -0
  161. data/lib/action_dispatch/testing/assertions/routing.rb +222 -0
  162. data/lib/action_dispatch/testing/integration.rb +652 -0
  163. data/lib/action_dispatch/testing/request_encoder.rb +55 -0
  164. data/lib/action_dispatch/testing/test_process.rb +50 -0
  165. data/lib/action_dispatch/testing/test_request.rb +71 -0
  166. data/lib/action_dispatch/testing/test_response.rb +53 -0
  167. data/lib/action_pack.rb +26 -0
  168. data/lib/action_pack/gem_version.rb +17 -0
  169. data/lib/action_pack/version.rb +10 -0
  170. metadata +318 -0
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ #--
4
+ # Copyright (c) 2004-2018 David Heinemeier Hansson
5
+ #
6
+ # Permission is hereby granted, free of charge, to any person obtaining
7
+ # a copy of this software and associated documentation files (the
8
+ # "Software"), to deal in the Software without restriction, including
9
+ # without limitation the rights to use, copy, modify, merge, publish,
10
+ # distribute, sublicense, and/or sell copies of the Software, and to
11
+ # permit persons to whom the Software is furnished to do so, subject to
12
+ # the following conditions:
13
+ #
14
+ # The above copyright notice and this permission notice shall be
15
+ # included in all copies or substantial portions of the Software.
16
+ #
17
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
19
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
21
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
22
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
23
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24
+ #++
25
+
26
+ require "active_support"
27
+ require "active_support/rails"
28
+ require "active_support/core_ext/module/attribute_accessors"
29
+
30
+ require "action_pack"
31
+ require "rack"
32
+
33
+ module Rack
34
+ autoload :Test, "rack/test"
35
+ end
36
+
37
+ module ActionDispatch
38
+ extend ActiveSupport::Autoload
39
+
40
+ class IllegalStateError < StandardError
41
+ end
42
+
43
+ eager_autoload do
44
+ autoload_under "http" do
45
+ autoload :ContentSecurityPolicy
46
+ autoload :Request
47
+ autoload :Response
48
+ end
49
+ end
50
+
51
+ autoload_under "middleware" do
52
+ autoload :RequestId
53
+ autoload :Callbacks
54
+ autoload :Cookies
55
+ autoload :DebugExceptions
56
+ autoload :DebugLocks
57
+ autoload :ExceptionWrapper
58
+ autoload :Executor
59
+ autoload :Flash
60
+ autoload :PublicExceptions
61
+ autoload :Reloader
62
+ autoload :RemoteIp
63
+ autoload :ShowExceptions
64
+ autoload :SSL
65
+ autoload :Static
66
+ end
67
+
68
+ autoload :Journey
69
+ autoload :MiddlewareStack, "action_dispatch/middleware/stack"
70
+ autoload :Routing
71
+
72
+ module Http
73
+ extend ActiveSupport::Autoload
74
+
75
+ autoload :Cache
76
+ autoload :Headers
77
+ autoload :MimeNegotiation
78
+ autoload :Parameters
79
+ autoload :ParameterFilter
80
+ autoload :Upload
81
+ autoload :UploadedFile, "action_dispatch/http/upload"
82
+ autoload :URL
83
+ end
84
+
85
+ module Session
86
+ autoload :AbstractStore, "action_dispatch/middleware/session/abstract_store"
87
+ autoload :CookieStore, "action_dispatch/middleware/session/cookie_store"
88
+ autoload :MemCacheStore, "action_dispatch/middleware/session/mem_cache_store"
89
+ autoload :CacheStore, "action_dispatch/middleware/session/cache_store"
90
+ end
91
+
92
+ mattr_accessor :test_app
93
+
94
+ autoload_under "testing" do
95
+ autoload :Assertions
96
+ autoload :Integration
97
+ autoload :IntegrationTest, "action_dispatch/testing/integration"
98
+ autoload :TestProcess
99
+ autoload :TestRequest
100
+ autoload :TestResponse
101
+ autoload :AssertionResponse
102
+ end
103
+
104
+ autoload :SystemTestCase, "action_dispatch/system_test_case"
105
+ end
106
+
107
+ autoload :Mime, "action_dispatch/http/mime_type"
108
+
109
+ ActiveSupport.on_load(:action_view) do
110
+ ActionView::Base.default_formats ||= Mime::SET.symbols
111
+ ActionView::Template::Types.delegate_to Mime
112
+ end
@@ -0,0 +1,222 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionDispatch
4
+ module Http
5
+ module Cache
6
+ module Request
7
+ HTTP_IF_MODIFIED_SINCE = "HTTP_IF_MODIFIED_SINCE".freeze
8
+ HTTP_IF_NONE_MATCH = "HTTP_IF_NONE_MATCH".freeze
9
+
10
+ def if_modified_since
11
+ if since = get_header(HTTP_IF_MODIFIED_SINCE)
12
+ Time.rfc2822(since) rescue nil
13
+ end
14
+ end
15
+
16
+ def if_none_match
17
+ get_header HTTP_IF_NONE_MATCH
18
+ end
19
+
20
+ def if_none_match_etags
21
+ if_none_match ? if_none_match.split(/\s*,\s*/) : []
22
+ end
23
+
24
+ def not_modified?(modified_at)
25
+ if_modified_since && modified_at && if_modified_since >= modified_at
26
+ end
27
+
28
+ def etag_matches?(etag)
29
+ if etag
30
+ validators = if_none_match_etags
31
+ validators.include?(etag) || validators.include?("*")
32
+ end
33
+ end
34
+
35
+ # Check response freshness (Last-Modified and ETag) against request
36
+ # If-Modified-Since and If-None-Match conditions. If both headers are
37
+ # supplied, both must match, or the request is not considered fresh.
38
+ def fresh?(response)
39
+ last_modified = if_modified_since
40
+ etag = if_none_match
41
+
42
+ return false unless last_modified || etag
43
+
44
+ success = true
45
+ success &&= not_modified?(response.last_modified) if last_modified
46
+ success &&= etag_matches?(response.etag) if etag
47
+ success
48
+ end
49
+ end
50
+
51
+ module Response
52
+ attr_reader :cache_control
53
+
54
+ def last_modified
55
+ if last = get_header(LAST_MODIFIED)
56
+ Time.httpdate(last)
57
+ end
58
+ end
59
+
60
+ def last_modified?
61
+ has_header? LAST_MODIFIED
62
+ end
63
+
64
+ def last_modified=(utc_time)
65
+ set_header LAST_MODIFIED, utc_time.httpdate
66
+ end
67
+
68
+ def date
69
+ if date_header = get_header(DATE)
70
+ Time.httpdate(date_header)
71
+ end
72
+ end
73
+
74
+ def date?
75
+ has_header? DATE
76
+ end
77
+
78
+ def date=(utc_time)
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
103
+ end
104
+
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?
123
+ end
124
+
125
+ private
126
+
127
+ DATE = "Date".freeze
128
+ LAST_MODIFIED = "Last-Modified".freeze
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
138
+
139
+ def cache_control_segments
140
+ if cache_control = _cache_control
141
+ cache_control.delete(" ").split(",")
142
+ else
143
+ []
144
+ end
145
+ end
146
+
147
+ def cache_control_headers
148
+ cache_control = {}
149
+
150
+ cache_control_segments.each do |segment|
151
+ directive, argument = segment.split("=", 2)
152
+
153
+ if SPECIAL_KEYS.include? directive
154
+ key = directive.tr("-", "_")
155
+ cache_control[key.to_sym] = argument || true
156
+ else
157
+ cache_control[:extras] ||= []
158
+ cache_control[:extras] << segment
159
+ end
160
+ end
161
+
162
+ cache_control
163
+ end
164
+
165
+ def prepare_cache_control!
166
+ @cache_control = cache_control_headers
167
+ end
168
+
169
+ DEFAULT_CACHE_CONTROL = "max-age=0, private, must-revalidate".freeze
170
+ NO_CACHE = "no-cache".freeze
171
+ PUBLIC = "public".freeze
172
+ PRIVATE = "private".freeze
173
+ MUST_REVALIDATE = "must-revalidate".freeze
174
+
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)
186
+ control = {}
187
+ cc_headers = cache_control_headers
188
+ if extras = cc_headers.delete(:extras)
189
+ cache_control[:extras] ||= []
190
+ cache_control[:extras] += extras
191
+ cache_control[:extras].uniq!
192
+ end
193
+
194
+ control.merge! cc_headers
195
+ control.merge! cache_control
196
+
197
+ if control.empty?
198
+ # Let middleware handle default behavior
199
+ elsif control[:no_cache]
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(", ")
206
+ else
207
+ extras = control[:extras]
208
+ max_age = control[:max_age]
209
+
210
+ options = []
211
+ options << "max-age=#{max_age.to_i}" if max_age
212
+ options << (control[:public] ? PUBLIC : PRIVATE)
213
+ options << MUST_REVALIDATE if control[:must_revalidate]
214
+ options.concat(extras) if extras
215
+
216
+ self._cache_control = options.join(", ")
217
+ end
218
+ end
219
+ end
220
+ end
221
+ end
222
+ 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