actionpack 6.0.0

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 (181) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +311 -0
  3. data/MIT-LICENSE +21 -0
  4. data/README.rdoc +58 -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 +267 -0
  8. data/lib/abstract_controller/caching.rb +66 -0
  9. data/lib/abstract_controller/caching/fragments.rb +150 -0
  10. data/lib/abstract_controller/callbacks.rb +224 -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 +32 -0
  18. data/lib/abstract_controller/url_for.rb +35 -0
  19. data/lib/action_controller.rb +67 -0
  20. data/lib/action_controller/api.rb +150 -0
  21. data/lib/action_controller/api/api_rendering.rb +16 -0
  22. data/lib/action_controller/base.rb +271 -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 +81 -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 +280 -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 +151 -0
  32. data/lib/action_controller/metal/default_headers.rb +17 -0
  33. data/lib/action_controller/metal/etag_with_flash.rb +18 -0
  34. data/lib/action_controller/metal/etag_with_template_digest.rb +57 -0
  35. data/lib/action_controller/metal/exceptions.rb +74 -0
  36. data/lib/action_controller/metal/flash.rb +61 -0
  37. data/lib/action_controller/metal/force_ssl.rb +58 -0
  38. data/lib/action_controller/metal/head.rb +60 -0
  39. data/lib/action_controller/metal/helpers.rb +122 -0
  40. data/lib/action_controller/metal/http_authentication.rb +518 -0
  41. data/lib/action_controller/metal/implicit_render.rb +63 -0
  42. data/lib/action_controller/metal/instrumentation.rb +105 -0
  43. data/lib/action_controller/metal/live.rb +314 -0
  44. data/lib/action_controller/metal/mime_responds.rb +324 -0
  45. data/lib/action_controller/metal/parameter_encoding.rb +51 -0
  46. data/lib/action_controller/metal/params_wrapper.rb +297 -0
  47. data/lib/action_controller/metal/redirecting.rb +133 -0
  48. data/lib/action_controller/metal/renderers.rb +181 -0
  49. data/lib/action_controller/metal/rendering.rb +122 -0
  50. data/lib/action_controller/metal/request_forgery_protection.rb +456 -0
  51. data/lib/action_controller/metal/rescue.rb +28 -0
  52. data/lib/action_controller/metal/streaming.rb +223 -0
  53. data/lib/action_controller/metal/strong_parameters.rb +1105 -0
  54. data/lib/action_controller/metal/testing.rb +16 -0
  55. data/lib/action_controller/metal/url_for.rb +58 -0
  56. data/lib/action_controller/railtie.rb +89 -0
  57. data/lib/action_controller/railties/helpers.rb +24 -0
  58. data/lib/action_controller/renderer.rb +130 -0
  59. data/lib/action_controller/template_assertions.rb +11 -0
  60. data/lib/action_controller/test_case.rb +626 -0
  61. data/lib/action_dispatch.rb +114 -0
  62. data/lib/action_dispatch/http/cache.rb +226 -0
  63. data/lib/action_dispatch/http/content_disposition.rb +45 -0
  64. data/lib/action_dispatch/http/content_security_policy.rb +284 -0
  65. data/lib/action_dispatch/http/filter_parameters.rb +86 -0
  66. data/lib/action_dispatch/http/filter_redirect.rb +37 -0
  67. data/lib/action_dispatch/http/headers.rb +132 -0
  68. data/lib/action_dispatch/http/mime_negotiation.rb +177 -0
  69. data/lib/action_dispatch/http/mime_type.rb +350 -0
  70. data/lib/action_dispatch/http/mime_types.rb +50 -0
  71. data/lib/action_dispatch/http/parameter_filter.rb +12 -0
  72. data/lib/action_dispatch/http/parameters.rb +136 -0
  73. data/lib/action_dispatch/http/rack_cache.rb +63 -0
  74. data/lib/action_dispatch/http/request.rb +427 -0
  75. data/lib/action_dispatch/http/response.rb +534 -0
  76. data/lib/action_dispatch/http/upload.rb +92 -0
  77. data/lib/action_dispatch/http/url.rb +350 -0
  78. data/lib/action_dispatch/journey.rb +7 -0
  79. data/lib/action_dispatch/journey/formatter.rb +189 -0
  80. data/lib/action_dispatch/journey/gtg/builder.rb +164 -0
  81. data/lib/action_dispatch/journey/gtg/simulator.rb +41 -0
  82. data/lib/action_dispatch/journey/gtg/transition_table.rb +158 -0
  83. data/lib/action_dispatch/journey/nfa/builder.rb +78 -0
  84. data/lib/action_dispatch/journey/nfa/dot.rb +36 -0
  85. data/lib/action_dispatch/journey/nfa/simulator.rb +47 -0
  86. data/lib/action_dispatch/journey/nfa/transition_table.rb +120 -0
  87. data/lib/action_dispatch/journey/nodes/node.rb +141 -0
  88. data/lib/action_dispatch/journey/parser.rb +199 -0
  89. data/lib/action_dispatch/journey/parser.y +50 -0
  90. data/lib/action_dispatch/journey/parser_extras.rb +31 -0
  91. data/lib/action_dispatch/journey/path/pattern.rb +203 -0
  92. data/lib/action_dispatch/journey/route.rb +204 -0
  93. data/lib/action_dispatch/journey/router.rb +153 -0
  94. data/lib/action_dispatch/journey/router/utils.rb +102 -0
  95. data/lib/action_dispatch/journey/routes.rb +81 -0
  96. data/lib/action_dispatch/journey/scanner.rb +71 -0
  97. data/lib/action_dispatch/journey/visitors.rb +268 -0
  98. data/lib/action_dispatch/journey/visualizer/fsm.css +30 -0
  99. data/lib/action_dispatch/journey/visualizer/fsm.js +134 -0
  100. data/lib/action_dispatch/journey/visualizer/index.html.erb +52 -0
  101. data/lib/action_dispatch/middleware/actionable_exceptions.rb +39 -0
  102. data/lib/action_dispatch/middleware/callbacks.rb +34 -0
  103. data/lib/action_dispatch/middleware/cookies.rb +663 -0
  104. data/lib/action_dispatch/middleware/debug_exceptions.rb +185 -0
  105. data/lib/action_dispatch/middleware/debug_locks.rb +124 -0
  106. data/lib/action_dispatch/middleware/debug_view.rb +68 -0
  107. data/lib/action_dispatch/middleware/exception_wrapper.rb +181 -0
  108. data/lib/action_dispatch/middleware/executor.rb +21 -0
  109. data/lib/action_dispatch/middleware/flash.rb +300 -0
  110. data/lib/action_dispatch/middleware/host_authorization.rb +103 -0
  111. data/lib/action_dispatch/middleware/public_exceptions.rb +61 -0
  112. data/lib/action_dispatch/middleware/reloader.rb +12 -0
  113. data/lib/action_dispatch/middleware/remote_ip.rb +181 -0
  114. data/lib/action_dispatch/middleware/request_id.rb +43 -0
  115. data/lib/action_dispatch/middleware/session/abstract_store.rb +92 -0
  116. data/lib/action_dispatch/middleware/session/cache_store.rb +54 -0
  117. data/lib/action_dispatch/middleware/session/cookie_store.rb +113 -0
  118. data/lib/action_dispatch/middleware/session/mem_cache_store.rb +28 -0
  119. data/lib/action_dispatch/middleware/show_exceptions.rb +62 -0
  120. data/lib/action_dispatch/middleware/ssl.rb +150 -0
  121. data/lib/action_dispatch/middleware/stack.rb +148 -0
  122. data/lib/action_dispatch/middleware/static.rb +129 -0
  123. data/lib/action_dispatch/middleware/templates/rescues/_actions.html.erb +13 -0
  124. data/lib/action_dispatch/middleware/templates/rescues/_actions.text.erb +0 -0
  125. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb +24 -0
  126. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.text.erb +23 -0
  127. data/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +29 -0
  128. data/lib/action_dispatch/middleware/templates/rescues/_source.text.erb +8 -0
  129. data/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +62 -0
  130. data/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb +9 -0
  131. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb +7 -0
  132. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.text.erb +5 -0
  133. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +38 -0
  134. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb +9 -0
  135. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +24 -0
  136. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +15 -0
  137. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +165 -0
  138. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb +19 -0
  139. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.text.erb +3 -0
  140. data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +11 -0
  141. data/lib/action_dispatch/middleware/templates/rescues/missing_template.text.erb +3 -0
  142. data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +32 -0
  143. data/lib/action_dispatch/middleware/templates/rescues/routing_error.text.erb +11 -0
  144. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +20 -0
  145. data/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb +7 -0
  146. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +6 -0
  147. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.text.erb +3 -0
  148. data/lib/action_dispatch/middleware/templates/routes/_route.html.erb +16 -0
  149. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +203 -0
  150. data/lib/action_dispatch/railtie.rb +58 -0
  151. data/lib/action_dispatch/request/session.rb +242 -0
  152. data/lib/action_dispatch/request/utils.rb +78 -0
  153. data/lib/action_dispatch/routing.rb +261 -0
  154. data/lib/action_dispatch/routing/endpoint.rb +17 -0
  155. data/lib/action_dispatch/routing/inspector.rb +274 -0
  156. data/lib/action_dispatch/routing/mapper.rb +2289 -0
  157. data/lib/action_dispatch/routing/polymorphic_routes.rb +351 -0
  158. data/lib/action_dispatch/routing/redirection.rb +201 -0
  159. data/lib/action_dispatch/routing/route_set.rb +887 -0
  160. data/lib/action_dispatch/routing/routes_proxy.rb +69 -0
  161. data/lib/action_dispatch/routing/url_for.rb +237 -0
  162. data/lib/action_dispatch/system_test_case.rb +168 -0
  163. data/lib/action_dispatch/system_testing/browser.rb +80 -0
  164. data/lib/action_dispatch/system_testing/driver.rb +68 -0
  165. data/lib/action_dispatch/system_testing/server.rb +31 -0
  166. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +97 -0
  167. data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +33 -0
  168. data/lib/action_dispatch/system_testing/test_helpers/undef_methods.rb +26 -0
  169. data/lib/action_dispatch/testing/assertion_response.rb +47 -0
  170. data/lib/action_dispatch/testing/assertions.rb +24 -0
  171. data/lib/action_dispatch/testing/assertions/response.rb +106 -0
  172. data/lib/action_dispatch/testing/assertions/routing.rb +234 -0
  173. data/lib/action_dispatch/testing/integration.rb +659 -0
  174. data/lib/action_dispatch/testing/request_encoder.rb +55 -0
  175. data/lib/action_dispatch/testing/test_process.rb +50 -0
  176. data/lib/action_dispatch/testing/test_request.rb +71 -0
  177. data/lib/action_dispatch/testing/test_response.rb +25 -0
  178. data/lib/action_pack.rb +26 -0
  179. data/lib/action_pack/gem_version.rb +17 -0
  180. data/lib/action_pack/version.rb +10 -0
  181. metadata +329 -0
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ #--
4
+ # Copyright (c) 2004-2019 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 :HostAuthorization
53
+ autoload :RequestId
54
+ autoload :Callbacks
55
+ autoload :Cookies
56
+ autoload :ActionableExceptions
57
+ autoload :DebugExceptions
58
+ autoload :DebugLocks
59
+ autoload :DebugView
60
+ autoload :ExceptionWrapper
61
+ autoload :Executor
62
+ autoload :Flash
63
+ autoload :PublicExceptions
64
+ autoload :Reloader
65
+ autoload :RemoteIp
66
+ autoload :ShowExceptions
67
+ autoload :SSL
68
+ autoload :Static
69
+ end
70
+
71
+ autoload :Journey
72
+ autoload :MiddlewareStack, "action_dispatch/middleware/stack"
73
+ autoload :Routing
74
+
75
+ module Http
76
+ extend ActiveSupport::Autoload
77
+
78
+ autoload :Cache
79
+ autoload :Headers
80
+ autoload :MimeNegotiation
81
+ autoload :Parameters
82
+ autoload :ParameterFilter
83
+ autoload :UploadedFile, "action_dispatch/http/upload"
84
+ autoload :URL
85
+ end
86
+
87
+ module Session
88
+ autoload :AbstractStore, "action_dispatch/middleware/session/abstract_store"
89
+ autoload :CookieStore, "action_dispatch/middleware/session/cookie_store"
90
+ autoload :MemCacheStore, "action_dispatch/middleware/session/mem_cache_store"
91
+ autoload :CacheStore, "action_dispatch/middleware/session/cache_store"
92
+ end
93
+
94
+ mattr_accessor :test_app
95
+
96
+ autoload_under "testing" do
97
+ autoload :Assertions
98
+ autoload :Integration
99
+ autoload :IntegrationTest, "action_dispatch/testing/integration"
100
+ autoload :TestProcess
101
+ autoload :TestRequest
102
+ autoload :TestResponse
103
+ autoload :AssertionResponse
104
+ end
105
+
106
+ autoload :SystemTestCase, "action_dispatch/system_test_case"
107
+ end
108
+
109
+ autoload :Mime, "action_dispatch/http/mime_type"
110
+
111
+ ActiveSupport.on_load(:action_view) do
112
+ ActionView::Base.default_formats ||= Mime::SET.symbols
113
+ ActionView::Template::Types.delegate_to Mime
114
+ end
@@ -0,0 +1,226 @@
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"
8
+ HTTP_IF_NONE_MATCH = "HTTP_IF_NONE_MATCH"
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"
128
+ LAST_MODIFIED = "Last-Modified"
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"
170
+ NO_CACHE = "no-cache"
171
+ PUBLIC = "public"
172
+ PRIVATE = "private"
173
+ MUST_REVALIDATE = "must-revalidate"
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
+ stale_while_revalidate = control[:stale_while_revalidate]
210
+ stale_if_error = control[:stale_if_error]
211
+
212
+ options = []
213
+ options << "max-age=#{max_age.to_i}" if max_age
214
+ options << (control[:public] ? PUBLIC : PRIVATE)
215
+ options << MUST_REVALIDATE if control[:must_revalidate]
216
+ options << "stale-while-revalidate=#{stale_while_revalidate.to_i}" if stale_while_revalidate
217
+ options << "stale-if-error=#{stale_if_error.to_i}" if stale_if_error
218
+ options.concat(extras) if extras
219
+
220
+ self._cache_control = options.join(", ")
221
+ end
222
+ end
223
+ end
224
+ end
225
+ end
226
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionDispatch
4
+ module Http
5
+ class ContentDisposition # :nodoc:
6
+ def self.format(disposition:, filename:)
7
+ new(disposition: disposition, filename: filename).to_s
8
+ end
9
+
10
+ attr_reader :disposition, :filename
11
+
12
+ def initialize(disposition:, filename:)
13
+ @disposition = disposition
14
+ @filename = filename
15
+ end
16
+
17
+ TRADITIONAL_ESCAPED_CHAR = /[^ A-Za-z0-9!#$+.^_`|~-]/
18
+
19
+ def ascii_filename
20
+ 'filename="' + percent_escape(I18n.transliterate(filename), TRADITIONAL_ESCAPED_CHAR) + '"'
21
+ end
22
+
23
+ RFC_5987_ESCAPED_CHAR = /[^A-Za-z0-9!#$&+.^_`|~-]/
24
+
25
+ def utf8_filename
26
+ "filename*=UTF-8''" + percent_escape(filename, RFC_5987_ESCAPED_CHAR)
27
+ end
28
+
29
+ def to_s
30
+ if filename
31
+ "#{disposition}; #{ascii_filename}; #{utf8_filename}"
32
+ else
33
+ "#{disposition}"
34
+ end
35
+ end
36
+
37
+ private
38
+ def percent_escape(string, pattern)
39
+ string.gsub(pattern) do |char|
40
+ char.bytes.map { |byte| "%%%02X" % byte }.join
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,284 @@
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"
9
+ POLICY = "Content-Security-Policy"
10
+ POLICY_REPORT_ONLY = "Content-Security-Policy-Report-Only"
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
+ nonce_directives = request.content_security_policy_nonce_directives
26
+ context = request.controller_instance || request
27
+ headers[header_name(request)] = policy.build(context, nonce, nonce_directives)
28
+ end
29
+
30
+ response
31
+ end
32
+
33
+ private
34
+
35
+ def html_response?(headers)
36
+ if content_type = headers[CONTENT_TYPE]
37
+ content_type =~ /html/
38
+ end
39
+ end
40
+
41
+ def header_name(request)
42
+ if request.content_security_policy_report_only
43
+ POLICY_REPORT_ONLY
44
+ else
45
+ POLICY
46
+ end
47
+ end
48
+
49
+ def policy_present?(headers)
50
+ headers[POLICY] || headers[POLICY_REPORT_ONLY]
51
+ end
52
+ end
53
+
54
+ module Request
55
+ POLICY = "action_dispatch.content_security_policy"
56
+ POLICY_REPORT_ONLY = "action_dispatch.content_security_policy_report_only"
57
+ NONCE_GENERATOR = "action_dispatch.content_security_policy_nonce_generator"
58
+ NONCE = "action_dispatch.content_security_policy_nonce"
59
+ NONCE_DIRECTIVES = "action_dispatch.content_security_policy_nonce_directives"
60
+
61
+ def content_security_policy
62
+ get_header(POLICY)
63
+ end
64
+
65
+ def content_security_policy=(policy)
66
+ set_header(POLICY, policy)
67
+ end
68
+
69
+ def content_security_policy_report_only
70
+ get_header(POLICY_REPORT_ONLY)
71
+ end
72
+
73
+ def content_security_policy_report_only=(value)
74
+ set_header(POLICY_REPORT_ONLY, value)
75
+ end
76
+
77
+ def content_security_policy_nonce_generator
78
+ get_header(NONCE_GENERATOR)
79
+ end
80
+
81
+ def content_security_policy_nonce_generator=(generator)
82
+ set_header(NONCE_GENERATOR, generator)
83
+ end
84
+
85
+ def content_security_policy_nonce_directives
86
+ get_header(NONCE_DIRECTIVES)
87
+ end
88
+
89
+ def content_security_policy_nonce_directives=(generator)
90
+ set_header(NONCE_DIRECTIVES, generator)
91
+ end
92
+
93
+ def content_security_policy_nonce
94
+ if content_security_policy_nonce_generator
95
+ if nonce = get_header(NONCE)
96
+ nonce
97
+ else
98
+ set_header(NONCE, generate_content_security_policy_nonce)
99
+ end
100
+ end
101
+ end
102
+
103
+ private
104
+
105
+ def generate_content_security_policy_nonce
106
+ content_security_policy_nonce_generator.call(self)
107
+ end
108
+ end
109
+
110
+ MAPPINGS = {
111
+ self: "'self'",
112
+ unsafe_eval: "'unsafe-eval'",
113
+ unsafe_inline: "'unsafe-inline'",
114
+ none: "'none'",
115
+ http: "http:",
116
+ https: "https:",
117
+ data: "data:",
118
+ mediastream: "mediastream:",
119
+ blob: "blob:",
120
+ filesystem: "filesystem:",
121
+ report_sample: "'report-sample'",
122
+ strict_dynamic: "'strict-dynamic'",
123
+ ws: "ws:",
124
+ wss: "wss:"
125
+ }.freeze
126
+
127
+ DIRECTIVES = {
128
+ base_uri: "base-uri",
129
+ child_src: "child-src",
130
+ connect_src: "connect-src",
131
+ default_src: "default-src",
132
+ font_src: "font-src",
133
+ form_action: "form-action",
134
+ frame_ancestors: "frame-ancestors",
135
+ frame_src: "frame-src",
136
+ img_src: "img-src",
137
+ manifest_src: "manifest-src",
138
+ media_src: "media-src",
139
+ object_src: "object-src",
140
+ prefetch_src: "prefetch-src",
141
+ script_src: "script-src",
142
+ style_src: "style-src",
143
+ worker_src: "worker-src"
144
+ }.freeze
145
+
146
+ DEFAULT_NONCE_DIRECTIVES = %w[script-src style-src].freeze
147
+
148
+ private_constant :MAPPINGS, :DIRECTIVES, :DEFAULT_NONCE_DIRECTIVES
149
+
150
+ attr_reader :directives
151
+
152
+ def initialize
153
+ @directives = {}
154
+ yield self if block_given?
155
+ end
156
+
157
+ def initialize_copy(other)
158
+ @directives = other.directives.deep_dup
159
+ end
160
+
161
+ DIRECTIVES.each do |name, directive|
162
+ define_method(name) do |*sources|
163
+ if sources.first
164
+ @directives[directive] = apply_mappings(sources)
165
+ else
166
+ @directives.delete(directive)
167
+ end
168
+ end
169
+ end
170
+
171
+ def block_all_mixed_content(enabled = true)
172
+ if enabled
173
+ @directives["block-all-mixed-content"] = true
174
+ else
175
+ @directives.delete("block-all-mixed-content")
176
+ end
177
+ end
178
+
179
+ def plugin_types(*types)
180
+ if types.first
181
+ @directives["plugin-types"] = types
182
+ else
183
+ @directives.delete("plugin-types")
184
+ end
185
+ end
186
+
187
+ def report_uri(uri)
188
+ @directives["report-uri"] = [uri]
189
+ end
190
+
191
+ def require_sri_for(*types)
192
+ if types.first
193
+ @directives["require-sri-for"] = types
194
+ else
195
+ @directives.delete("require-sri-for")
196
+ end
197
+ end
198
+
199
+ def sandbox(*values)
200
+ if values.empty?
201
+ @directives["sandbox"] = true
202
+ elsif values.first
203
+ @directives["sandbox"] = values
204
+ else
205
+ @directives.delete("sandbox")
206
+ end
207
+ end
208
+
209
+ def upgrade_insecure_requests(enabled = true)
210
+ if enabled
211
+ @directives["upgrade-insecure-requests"] = true
212
+ else
213
+ @directives.delete("upgrade-insecure-requests")
214
+ end
215
+ end
216
+
217
+ def build(context = nil, nonce = nil, nonce_directives = nil)
218
+ nonce_directives = DEFAULT_NONCE_DIRECTIVES if nonce_directives.nil?
219
+ build_directives(context, nonce, nonce_directives).compact.join("; ")
220
+ end
221
+
222
+ private
223
+ def apply_mappings(sources)
224
+ sources.map do |source|
225
+ case source
226
+ when Symbol
227
+ apply_mapping(source)
228
+ when String, Proc
229
+ source
230
+ else
231
+ raise ArgumentError, "Invalid content security policy source: #{source.inspect}"
232
+ end
233
+ end
234
+ end
235
+
236
+ def apply_mapping(source)
237
+ MAPPINGS.fetch(source) do
238
+ raise ArgumentError, "Unknown content security policy source mapping: #{source.inspect}"
239
+ end
240
+ end
241
+
242
+ def build_directives(context, nonce, nonce_directives)
243
+ @directives.map do |directive, sources|
244
+ if sources.is_a?(Array)
245
+ if nonce && nonce_directive?(directive, nonce_directives)
246
+ "#{directive} #{build_directive(sources, context).join(' ')} 'nonce-#{nonce}'"
247
+ else
248
+ "#{directive} #{build_directive(sources, context).join(' ')}"
249
+ end
250
+ elsif sources
251
+ directive
252
+ else
253
+ nil
254
+ end
255
+ end
256
+ end
257
+
258
+ def build_directive(sources, context)
259
+ sources.map { |source| resolve_source(source, context) }
260
+ end
261
+
262
+ def resolve_source(source, context)
263
+ case source
264
+ when String
265
+ source
266
+ when Symbol
267
+ source.to_s
268
+ when Proc
269
+ if context.nil?
270
+ raise RuntimeError, "Missing context for the dynamic content security policy source: #{source.inspect}"
271
+ else
272
+ resolved = context.instance_exec(&source)
273
+ resolved.is_a?(Symbol) ? apply_mapping(resolved) : resolved
274
+ end
275
+ else
276
+ raise RuntimeError, "Unexpected content security policy source: #{source.inspect}"
277
+ end
278
+ end
279
+
280
+ def nonce_directive?(directive, nonce_directives)
281
+ nonce_directives.include?(directive)
282
+ end
283
+ end
284
+ end