actionpack 5.2.0 → 6.1.4
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 +408 -190
- data/MIT-LICENSE +1 -1
- data/README.rdoc +4 -3
- data/lib/abstract_controller/base.rb +38 -4
- data/lib/abstract_controller/caching/fragments.rb +6 -22
- data/lib/abstract_controller/caching.rb +1 -1
- data/lib/abstract_controller/callbacks.rb +14 -2
- data/lib/abstract_controller/collector.rb +1 -2
- data/lib/abstract_controller/helpers.rb +106 -90
- data/lib/abstract_controller/railties/routes_helpers.rb +17 -1
- data/lib/abstract_controller/rendering.rb +9 -9
- data/lib/abstract_controller/translation.rb +11 -5
- data/lib/abstract_controller.rb +2 -0
- data/lib/action_controller/api.rb +4 -3
- data/lib/action_controller/base.rb +6 -9
- data/lib/action_controller/caching.rb +1 -3
- data/lib/action_controller/log_subscriber.rb +10 -7
- data/lib/action_controller/metal/basic_implicit_render.rb +1 -1
- data/lib/action_controller/metal/conditional_get.rb +19 -5
- data/lib/action_controller/metal/content_security_policy.rb +1 -2
- data/lib/action_controller/metal/cookies.rb +3 -1
- data/lib/action_controller/metal/data_streaming.rb +6 -7
- data/lib/action_controller/metal/default_headers.rb +17 -0
- data/lib/action_controller/metal/etag_with_template_digest.rb +4 -6
- data/lib/action_controller/metal/exceptions.rb +56 -2
- data/lib/action_controller/metal/flash.rb +5 -5
- data/lib/action_controller/metal/head.rb +7 -4
- data/lib/action_controller/metal/helpers.rb +14 -5
- data/lib/action_controller/metal/http_authentication.rb +25 -24
- data/lib/action_controller/metal/implicit_render.rb +5 -15
- data/lib/action_controller/metal/instrumentation.rb +13 -14
- data/lib/action_controller/metal/live.rb +39 -32
- data/lib/action_controller/metal/logging.rb +20 -0
- data/lib/action_controller/metal/mime_responds.rb +19 -4
- data/lib/action_controller/metal/parameter_encoding.rb +35 -4
- data/lib/action_controller/metal/params_wrapper.rb +33 -23
- data/lib/action_controller/metal/permissions_policy.rb +46 -0
- data/lib/action_controller/metal/redirecting.rb +7 -7
- data/lib/action_controller/metal/renderers.rb +4 -4
- data/lib/action_controller/metal/rendering.rb +8 -3
- data/lib/action_controller/metal/request_forgery_protection.rb +89 -36
- data/lib/action_controller/metal/rescue.rb +1 -1
- data/lib/action_controller/metal/streaming.rb +0 -1
- data/lib/action_controller/metal/strong_parameters.rb +181 -69
- data/lib/action_controller/metal/url_for.rb +1 -1
- data/lib/action_controller/metal.rb +12 -10
- data/lib/action_controller/railties/helpers.rb +1 -1
- data/lib/action_controller/renderer.rb +37 -13
- data/lib/action_controller/template_assertions.rb +1 -1
- data/lib/action_controller/test_case.rb +81 -70
- data/lib/action_controller.rb +7 -4
- data/lib/action_dispatch/http/cache.rb +34 -28
- data/lib/action_dispatch/http/content_disposition.rb +45 -0
- data/lib/action_dispatch/http/content_security_policy.rb +47 -24
- data/lib/action_dispatch/http/filter_parameters.rb +9 -8
- data/lib/action_dispatch/http/filter_redirect.rb +2 -3
- data/lib/action_dispatch/http/headers.rb +4 -4
- data/lib/action_dispatch/http/mime_negotiation.rb +31 -13
- data/lib/action_dispatch/http/mime_type.rb +43 -24
- data/lib/action_dispatch/http/parameters.rb +14 -23
- data/lib/action_dispatch/http/permissions_policy.rb +173 -0
- data/lib/action_dispatch/http/request.rb +45 -22
- data/lib/action_dispatch/http/response.rb +45 -25
- data/lib/action_dispatch/http/upload.rb +9 -1
- data/lib/action_dispatch/http/url.rb +82 -82
- data/lib/action_dispatch/journey/formatter.rb +55 -31
- data/lib/action_dispatch/journey/gtg/builder.rb +22 -37
- data/lib/action_dispatch/journey/gtg/simulator.rb +8 -7
- data/lib/action_dispatch/journey/gtg/transition_table.rb +6 -5
- data/lib/action_dispatch/journey/nfa/dot.rb +0 -11
- data/lib/action_dispatch/journey/nodes/node.rb +13 -11
- data/lib/action_dispatch/journey/parser.rb +13 -13
- data/lib/action_dispatch/journey/parser.y +1 -1
- data/lib/action_dispatch/journey/path/pattern.rb +21 -22
- data/lib/action_dispatch/journey/route.rb +10 -20
- data/lib/action_dispatch/journey/router/utils.rb +14 -12
- data/lib/action_dispatch/journey/router.rb +26 -34
- data/lib/action_dispatch/journey/routes.rb +1 -2
- data/lib/action_dispatch/journey/scanner.rb +10 -4
- data/lib/action_dispatch/journey/visitors.rb +1 -4
- data/lib/action_dispatch/journey.rb +0 -2
- data/lib/action_dispatch/middleware/actionable_exceptions.rb +46 -0
- data/lib/action_dispatch/middleware/callbacks.rb +2 -4
- data/lib/action_dispatch/middleware/cookies.rb +128 -109
- data/lib/action_dispatch/middleware/debug_exceptions.rb +43 -66
- data/lib/action_dispatch/middleware/debug_locks.rb +5 -5
- data/lib/action_dispatch/middleware/debug_view.rb +66 -0
- data/lib/action_dispatch/middleware/exception_wrapper.rb +75 -30
- data/lib/action_dispatch/middleware/flash.rb +2 -2
- data/lib/action_dispatch/middleware/host_authorization.rb +130 -0
- data/lib/action_dispatch/middleware/public_exceptions.rb +6 -3
- data/lib/action_dispatch/middleware/remote_ip.rb +14 -16
- data/lib/action_dispatch/middleware/request_id.rb +5 -6
- data/lib/action_dispatch/middleware/session/abstract_store.rb +15 -2
- data/lib/action_dispatch/middleware/session/cache_store.rb +11 -6
- data/lib/action_dispatch/middleware/session/cookie_store.rb +24 -19
- data/lib/action_dispatch/middleware/show_exceptions.rb +3 -2
- data/lib/action_dispatch/middleware/ssl.rb +20 -15
- data/lib/action_dispatch/middleware/stack.rb +57 -3
- data/lib/action_dispatch/middleware/static.rb +153 -93
- data/lib/action_dispatch/middleware/templates/rescues/_actions.html.erb +13 -0
- data/lib/action_dispatch/middleware/templates/rescues/_actions.text.erb +0 -0
- data/lib/action_dispatch/middleware/templates/rescues/_message_and_suggestions.html.erb +22 -0
- data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb +3 -1
- data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.text.erb +1 -1
- data/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +4 -2
- data/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +45 -35
- data/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb +7 -0
- data/lib/action_dispatch/middleware/templates/rescues/blocked_host.text.erb +5 -0
- data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +23 -4
- data/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb +1 -1
- data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +6 -3
- data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +4 -1
- data/lib/action_dispatch/middleware/templates/rescues/layout.erb +104 -8
- data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb +19 -0
- data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.text.erb +3 -0
- data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +2 -2
- data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +1 -1
- data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +2 -2
- data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +1 -1
- data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +24 -1
- data/lib/action_dispatch/railtie.rb +8 -2
- data/lib/action_dispatch/request/session.rb +17 -10
- data/lib/action_dispatch/request/utils.rb +28 -2
- data/lib/action_dispatch/routing/inspector.rb +101 -53
- data/lib/action_dispatch/routing/mapper.rb +156 -103
- data/lib/action_dispatch/routing/polymorphic_routes.rb +21 -19
- data/lib/action_dispatch/routing/redirection.rb +4 -4
- data/lib/action_dispatch/routing/route_set.rb +71 -69
- data/lib/action_dispatch/routing/url_for.rb +3 -3
- data/lib/action_dispatch/routing.rb +21 -20
- data/lib/action_dispatch/system_test_case.rb +54 -11
- data/lib/action_dispatch/system_testing/browser.rb +53 -16
- data/lib/action_dispatch/system_testing/driver.rb +11 -3
- data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +49 -7
- data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +8 -6
- data/lib/action_dispatch/testing/assertion_response.rb +0 -1
- data/lib/action_dispatch/testing/assertions/response.rb +4 -7
- data/lib/action_dispatch/testing/assertions/routing.rb +20 -8
- data/lib/action_dispatch/testing/assertions.rb +1 -1
- data/lib/action_dispatch/testing/integration.rb +61 -28
- data/lib/action_dispatch/testing/request_encoder.rb +3 -3
- data/lib/action_dispatch/testing/test_process.rb +29 -4
- data/lib/action_dispatch/testing/test_request.rb +3 -3
- data/lib/action_dispatch/testing/test_response.rb +4 -32
- data/lib/action_dispatch.rb +14 -7
- data/lib/action_pack/gem_version.rb +3 -3
- data/lib/action_pack.rb +1 -1
- metadata +39 -22
- data/lib/action_controller/metal/force_ssl.rb +0 -99
- data/lib/action_dispatch/http/parameter_filter.rb +0 -86
- data/lib/action_dispatch/journey/nfa/builder.rb +0 -78
- data/lib/action_dispatch/journey/nfa/simulator.rb +0 -49
- data/lib/action_dispatch/journey/nfa/transition_table.rb +0 -120
- data/lib/action_dispatch/system_testing/test_helpers/undef_methods.rb +0 -26
|
@@ -24,9 +24,12 @@ module ActionController
|
|
|
24
24
|
def new_controller_thread # :nodoc:
|
|
25
25
|
yield
|
|
26
26
|
end
|
|
27
|
+
|
|
28
|
+
# Avoid a deadlock from the queue filling up
|
|
29
|
+
Buffer.queue_size = nil
|
|
27
30
|
end
|
|
28
31
|
|
|
29
|
-
# ActionController::TestCase will be deprecated and moved to a gem in
|
|
32
|
+
# ActionController::TestCase will be deprecated and moved to a gem in the future.
|
|
30
33
|
# Please use ActionDispatch::IntegrationTest going forward.
|
|
31
34
|
class TestRequest < ActionDispatch::TestRequest #:nodoc:
|
|
32
35
|
DEFAULT_ENV = ActionDispatch::TestRequest::DEFAULT_ENV.dup
|
|
@@ -84,7 +87,7 @@ module ActionController
|
|
|
84
87
|
value = value.to_param
|
|
85
88
|
end
|
|
86
89
|
|
|
87
|
-
path_parameters[key] = value
|
|
90
|
+
path_parameters[key.to_sym] = value
|
|
88
91
|
end
|
|
89
92
|
end
|
|
90
93
|
|
|
@@ -158,7 +161,6 @@ module ActionController
|
|
|
158
161
|
end.new
|
|
159
162
|
|
|
160
163
|
private
|
|
161
|
-
|
|
162
164
|
def params_parsers
|
|
163
165
|
super.merge @custom_param_parsers
|
|
164
166
|
end
|
|
@@ -177,12 +179,12 @@ module ActionController
|
|
|
177
179
|
|
|
178
180
|
# Methods #destroy and #load! are overridden to avoid calling methods on the
|
|
179
181
|
# @store object, which does not exist for the TestSession class.
|
|
180
|
-
class TestSession < Rack::Session::Abstract::
|
|
182
|
+
class TestSession < Rack::Session::Abstract::PersistedSecure::SecureSessionHash #:nodoc:
|
|
181
183
|
DEFAULT_OPTIONS = Rack::Session::Abstract::Persisted::DEFAULT_OPTIONS
|
|
182
184
|
|
|
183
185
|
def initialize(session = {})
|
|
184
186
|
super(nil, nil)
|
|
185
|
-
@id = SecureRandom.hex(16)
|
|
187
|
+
@id = Rack::Session::SessionId.new(SecureRandom.hex(16))
|
|
186
188
|
@data = stringify_keys(session)
|
|
187
189
|
@loaded = true
|
|
188
190
|
end
|
|
@@ -203,12 +205,16 @@ module ActionController
|
|
|
203
205
|
clear
|
|
204
206
|
end
|
|
205
207
|
|
|
208
|
+
def dig(*keys)
|
|
209
|
+
keys = keys.map.with_index { |key, i| i.zero? ? key.to_s : key }
|
|
210
|
+
@data.dig(*keys)
|
|
211
|
+
end
|
|
212
|
+
|
|
206
213
|
def fetch(key, *args, &block)
|
|
207
214
|
@data.fetch(key.to_s, *args, &block)
|
|
208
215
|
end
|
|
209
216
|
|
|
210
217
|
private
|
|
211
|
-
|
|
212
218
|
def load!
|
|
213
219
|
@id
|
|
214
220
|
end
|
|
@@ -276,9 +282,6 @@ module ActionController
|
|
|
276
282
|
# after calling +post+. If the various assert methods are not sufficient, then you
|
|
277
283
|
# may use this object to inspect the HTTP response in detail.
|
|
278
284
|
#
|
|
279
|
-
# (Earlier versions of \Rails required each functional test to subclass
|
|
280
|
-
# Test::Unit::TestCase and define @controller, @request, @response in +setup+.)
|
|
281
|
-
#
|
|
282
285
|
# == Controller is automatically inferred
|
|
283
286
|
#
|
|
284
287
|
# ActionController::TestCase will automatically infer the controller under test
|
|
@@ -457,13 +460,10 @@ module ActionController
|
|
|
457
460
|
# respectively which will make tests more expressive.
|
|
458
461
|
#
|
|
459
462
|
# Note that the request method is not verified.
|
|
460
|
-
def process(action, method: "GET", params:
|
|
463
|
+
def process(action, method: "GET", params: nil, session: nil, body: nil, flash: {}, format: nil, xhr: false, as: nil)
|
|
461
464
|
check_required_ivars
|
|
462
465
|
|
|
463
|
-
|
|
464
|
-
@request.set_header "RAW_POST_DATA", body
|
|
465
|
-
end
|
|
466
|
-
|
|
466
|
+
action = +action.to_s
|
|
467
467
|
http_method = method.to_s.upcase
|
|
468
468
|
|
|
469
469
|
@html_document = nil
|
|
@@ -478,6 +478,10 @@ module ActionController
|
|
|
478
478
|
@response.request = @request
|
|
479
479
|
@controller.recycle!
|
|
480
480
|
|
|
481
|
+
if body
|
|
482
|
+
@request.set_header "RAW_POST_DATA", body
|
|
483
|
+
end
|
|
484
|
+
|
|
481
485
|
@request.set_header "REQUEST_METHOD", http_method
|
|
482
486
|
|
|
483
487
|
if as
|
|
@@ -485,63 +489,14 @@ module ActionController
|
|
|
485
489
|
format ||= as
|
|
486
490
|
end
|
|
487
491
|
|
|
488
|
-
parameters = params.symbolize_keys
|
|
492
|
+
parameters = (params || {}).symbolize_keys
|
|
489
493
|
|
|
490
494
|
if format
|
|
491
495
|
parameters[:format] = format
|
|
492
496
|
end
|
|
493
497
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
query_string_keys = query_parameter_names(generated_extras)
|
|
497
|
-
|
|
498
|
-
@request.assign_parameters(@routes, controller_class_name, action.to_s, parameters, generated_path, query_string_keys)
|
|
499
|
-
|
|
500
|
-
@request.session.update(session) if session
|
|
501
|
-
@request.flash.update(flash || {})
|
|
502
|
-
|
|
503
|
-
if xhr
|
|
504
|
-
@request.set_header "HTTP_X_REQUESTED_WITH", "XMLHttpRequest"
|
|
505
|
-
@request.fetch_header("HTTP_ACCEPT") do |k|
|
|
506
|
-
@request.set_header k, [Mime[:js], Mime[:html], Mime[:xml], "text/xml", "*/*"].join(", ")
|
|
507
|
-
end
|
|
508
|
-
end
|
|
509
|
-
|
|
510
|
-
@request.fetch_header("SCRIPT_NAME") do |k|
|
|
511
|
-
@request.set_header k, @controller.config.relative_url_root
|
|
512
|
-
end
|
|
513
|
-
|
|
514
|
-
begin
|
|
515
|
-
@controller.recycle!
|
|
516
|
-
@controller.dispatch(action, @request, @response)
|
|
517
|
-
ensure
|
|
518
|
-
@request = @controller.request
|
|
519
|
-
@response = @controller.response
|
|
520
|
-
|
|
521
|
-
if @request.have_cookie_jar?
|
|
522
|
-
unless @request.cookie_jar.committed?
|
|
523
|
-
@request.cookie_jar.write(@response)
|
|
524
|
-
cookies.update(@request.cookie_jar.instance_variable_get(:@cookies))
|
|
525
|
-
end
|
|
526
|
-
end
|
|
527
|
-
@response.prepare!
|
|
528
|
-
|
|
529
|
-
if flash_value = @request.flash.to_session_value
|
|
530
|
-
@request.session["flash"] = flash_value
|
|
531
|
-
else
|
|
532
|
-
@request.session.delete("flash")
|
|
533
|
-
end
|
|
534
|
-
|
|
535
|
-
if xhr
|
|
536
|
-
@request.delete_header "HTTP_X_REQUESTED_WITH"
|
|
537
|
-
@request.delete_header "HTTP_ACCEPT"
|
|
538
|
-
end
|
|
539
|
-
@request.query_string = ""
|
|
540
|
-
|
|
541
|
-
@response.sent!
|
|
542
|
-
end
|
|
543
|
-
|
|
544
|
-
@response
|
|
498
|
+
setup_request(controller_class_name, action, parameters, session, flash, xhr)
|
|
499
|
+
process_controller_response(action, cookies, xhr)
|
|
545
500
|
end
|
|
546
501
|
|
|
547
502
|
def controller_class_name
|
|
@@ -597,13 +552,69 @@ module ActionController
|
|
|
597
552
|
end
|
|
598
553
|
|
|
599
554
|
private
|
|
555
|
+
def setup_request(controller_class_name, action, parameters, session, flash, xhr)
|
|
556
|
+
generated_extras = @routes.generate_extras(parameters.merge(controller: controller_class_name, action: action))
|
|
557
|
+
generated_path = generated_path(generated_extras)
|
|
558
|
+
query_string_keys = query_parameter_names(generated_extras)
|
|
559
|
+
|
|
560
|
+
@request.assign_parameters(@routes, controller_class_name, action, parameters, generated_path, query_string_keys)
|
|
561
|
+
|
|
562
|
+
@request.session.update(session) if session
|
|
563
|
+
@request.flash.update(flash || {})
|
|
564
|
+
|
|
565
|
+
if xhr
|
|
566
|
+
@request.set_header "HTTP_X_REQUESTED_WITH", "XMLHttpRequest"
|
|
567
|
+
@request.fetch_header("HTTP_ACCEPT") do |k|
|
|
568
|
+
@request.set_header k, [Mime[:js], Mime[:html], Mime[:xml], "text/xml", "*/*"].join(", ")
|
|
569
|
+
end
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
@request.fetch_header("SCRIPT_NAME") do |k|
|
|
573
|
+
@request.set_header k, @controller.config.relative_url_root
|
|
574
|
+
end
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
def process_controller_response(action, cookies, xhr)
|
|
578
|
+
begin
|
|
579
|
+
@controller.recycle!
|
|
580
|
+
@controller.dispatch(action, @request, @response)
|
|
581
|
+
ensure
|
|
582
|
+
@request = @controller.request
|
|
583
|
+
@response = @controller.response
|
|
584
|
+
|
|
585
|
+
if @request.have_cookie_jar?
|
|
586
|
+
unless @request.cookie_jar.committed?
|
|
587
|
+
@request.cookie_jar.write(@response)
|
|
588
|
+
cookies.update(@request.cookie_jar.instance_variable_get(:@cookies))
|
|
589
|
+
end
|
|
590
|
+
end
|
|
591
|
+
@response.prepare!
|
|
592
|
+
|
|
593
|
+
if flash_value = @request.flash.to_session_value
|
|
594
|
+
@request.session["flash"] = flash_value
|
|
595
|
+
else
|
|
596
|
+
@request.session.delete("flash")
|
|
597
|
+
end
|
|
598
|
+
|
|
599
|
+
if xhr
|
|
600
|
+
@request.delete_header "HTTP_X_REQUESTED_WITH"
|
|
601
|
+
@request.delete_header "HTTP_ACCEPT"
|
|
602
|
+
end
|
|
603
|
+
@request.query_string = ""
|
|
604
|
+
|
|
605
|
+
@response.sent!
|
|
606
|
+
end
|
|
607
|
+
|
|
608
|
+
@response
|
|
609
|
+
end
|
|
600
610
|
|
|
601
611
|
def scrub_env!(env)
|
|
602
|
-
env.delete_if
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
env.delete "action_dispatch.request.request_parameters"
|
|
612
|
+
env.delete_if do |k, _|
|
|
613
|
+
k.start_with?("rack.request", "action_dispatch.request", "action_dispatch.rescue")
|
|
614
|
+
end
|
|
606
615
|
env["rack.input"] = StringIO.new
|
|
616
|
+
env.delete "CONTENT_LENGTH"
|
|
617
|
+
env.delete "RAW_POST_DATA"
|
|
607
618
|
env
|
|
608
619
|
end
|
|
609
620
|
|
data/lib/action_controller.rb
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "active_support/rails"
|
|
4
3
|
require "abstract_controller"
|
|
5
4
|
require "action_dispatch"
|
|
6
|
-
require "action_controller/metal/live"
|
|
7
5
|
require "action_controller/metal/strong_parameters"
|
|
8
6
|
|
|
9
7
|
module ActionController
|
|
@@ -12,7 +10,6 @@ module ActionController
|
|
|
12
10
|
autoload :API
|
|
13
11
|
autoload :Base
|
|
14
12
|
autoload :Metal
|
|
15
|
-
autoload :Middleware
|
|
16
13
|
autoload :Renderer
|
|
17
14
|
autoload :FormBuilder
|
|
18
15
|
|
|
@@ -21,20 +18,26 @@ module ActionController
|
|
|
21
18
|
end
|
|
22
19
|
|
|
23
20
|
autoload_under "metal" do
|
|
21
|
+
eager_autoload do
|
|
22
|
+
autoload :Live
|
|
23
|
+
end
|
|
24
|
+
|
|
24
25
|
autoload :ConditionalGet
|
|
25
26
|
autoload :ContentSecurityPolicy
|
|
26
27
|
autoload :Cookies
|
|
27
28
|
autoload :DataStreaming
|
|
29
|
+
autoload :DefaultHeaders
|
|
28
30
|
autoload :EtagWithTemplateDigest
|
|
29
31
|
autoload :EtagWithFlash
|
|
32
|
+
autoload :PermissionsPolicy
|
|
30
33
|
autoload :Flash
|
|
31
|
-
autoload :ForceSSL
|
|
32
34
|
autoload :Head
|
|
33
35
|
autoload :Helpers
|
|
34
36
|
autoload :HttpAuthentication
|
|
35
37
|
autoload :BasicImplicitRender
|
|
36
38
|
autoload :ImplicitRender
|
|
37
39
|
autoload :Instrumentation
|
|
40
|
+
autoload :Logging
|
|
38
41
|
autoload :MimeResponds
|
|
39
42
|
autoload :ParamsWrapper
|
|
40
43
|
autoload :Redirecting
|
|
@@ -4,8 +4,8 @@ module ActionDispatch
|
|
|
4
4
|
module Http
|
|
5
5
|
module Cache
|
|
6
6
|
module Request
|
|
7
|
-
HTTP_IF_MODIFIED_SINCE = "HTTP_IF_MODIFIED_SINCE"
|
|
8
|
-
HTTP_IF_NONE_MATCH = "HTTP_IF_NONE_MATCH"
|
|
7
|
+
HTTP_IF_MODIFIED_SINCE = "HTTP_IF_MODIFIED_SINCE"
|
|
8
|
+
HTTP_IF_NONE_MATCH = "HTTP_IF_NONE_MATCH"
|
|
9
9
|
|
|
10
10
|
def if_modified_since
|
|
11
11
|
if since = get_header(HTTP_IF_MODIFIED_SINCE)
|
|
@@ -114,7 +114,7 @@ module ActionDispatch
|
|
|
114
114
|
|
|
115
115
|
# True if an ETag is set and it's a weak validator (preceded with W/)
|
|
116
116
|
def weak_etag?
|
|
117
|
-
etag? && etag.
|
|
117
|
+
etag? && etag.start_with?('W/"')
|
|
118
118
|
end
|
|
119
119
|
|
|
120
120
|
# True if an ETag is set and it isn't a weak validator (not preceded with W/)
|
|
@@ -123,10 +123,9 @@ module ActionDispatch
|
|
|
123
123
|
end
|
|
124
124
|
|
|
125
125
|
private
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
SPECIAL_KEYS = Set.new(%w[extras no-cache max-age public private must-revalidate])
|
|
126
|
+
DATE = "Date"
|
|
127
|
+
LAST_MODIFIED = "Last-Modified"
|
|
128
|
+
SPECIAL_KEYS = Set.new(%w[extras no-store no-cache max-age public private must-revalidate])
|
|
130
129
|
|
|
131
130
|
def generate_weak_etag(validators)
|
|
132
131
|
"W/#{generate_strong_etag(validators)}"
|
|
@@ -151,8 +150,8 @@ module ActionDispatch
|
|
|
151
150
|
directive, argument = segment.split("=", 2)
|
|
152
151
|
|
|
153
152
|
if SPECIAL_KEYS.include? directive
|
|
154
|
-
|
|
155
|
-
cache_control[
|
|
153
|
+
directive.tr!("-", "_")
|
|
154
|
+
cache_control[directive.to_sym] = argument || true
|
|
156
155
|
else
|
|
157
156
|
cache_control[:extras] ||= []
|
|
158
157
|
cache_control[:extras] << segment
|
|
@@ -166,11 +165,12 @@ module ActionDispatch
|
|
|
166
165
|
@cache_control = cache_control_headers
|
|
167
166
|
end
|
|
168
167
|
|
|
169
|
-
DEFAULT_CACHE_CONTROL = "max-age=0, private, must-revalidate"
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
168
|
+
DEFAULT_CACHE_CONTROL = "max-age=0, private, must-revalidate"
|
|
169
|
+
NO_STORE = "no-store"
|
|
170
|
+
NO_CACHE = "no-cache"
|
|
171
|
+
PUBLIC = "public"
|
|
172
|
+
PRIVATE = "private"
|
|
173
|
+
MUST_REVALIDATE = "must-revalidate"
|
|
174
174
|
|
|
175
175
|
def handle_conditional_get!
|
|
176
176
|
# Normally default cache control setting is handled by ETag
|
|
@@ -183,36 +183,42 @@ module ActionDispatch
|
|
|
183
183
|
end
|
|
184
184
|
|
|
185
185
|
def merge_and_normalize_cache_control!(cache_control)
|
|
186
|
-
control =
|
|
187
|
-
|
|
188
|
-
if
|
|
186
|
+
control = cache_control_headers
|
|
187
|
+
|
|
188
|
+
return if control.empty? && cache_control.empty? # Let middleware handle default behavior
|
|
189
|
+
|
|
190
|
+
if extras = control.delete(:extras)
|
|
189
191
|
cache_control[:extras] ||= []
|
|
190
192
|
cache_control[:extras] += extras
|
|
191
193
|
cache_control[:extras].uniq!
|
|
192
194
|
end
|
|
193
195
|
|
|
194
|
-
control.merge! cc_headers
|
|
195
196
|
control.merge! cache_control
|
|
196
197
|
|
|
197
|
-
|
|
198
|
-
|
|
198
|
+
options = []
|
|
199
|
+
|
|
200
|
+
if control[:no_store]
|
|
201
|
+
options << PRIVATE if control[:private]
|
|
202
|
+
options << NO_STORE
|
|
199
203
|
elsif control[:no_cache]
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
end
|
|
204
|
+
options << PUBLIC if control[:public]
|
|
205
|
+
options << NO_CACHE
|
|
206
|
+
options.concat(control[:extras]) if control[:extras]
|
|
204
207
|
else
|
|
205
|
-
extras
|
|
208
|
+
extras = control[:extras]
|
|
206
209
|
max_age = control[:max_age]
|
|
210
|
+
stale_while_revalidate = control[:stale_while_revalidate]
|
|
211
|
+
stale_if_error = control[:stale_if_error]
|
|
207
212
|
|
|
208
|
-
options = []
|
|
209
213
|
options << "max-age=#{max_age.to_i}" if max_age
|
|
210
214
|
options << (control[:public] ? PUBLIC : PRIVATE)
|
|
211
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
|
|
212
218
|
options.concat(extras) if extras
|
|
213
|
-
|
|
214
|
-
self._cache_control = options.join(", ")
|
|
215
219
|
end
|
|
220
|
+
|
|
221
|
+
self._cache_control = options.join(", ")
|
|
216
222
|
end
|
|
217
223
|
end
|
|
218
224
|
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
|
|
@@ -5,9 +5,9 @@ require "active_support/core_ext/object/deep_dup"
|
|
|
5
5
|
module ActionDispatch #:nodoc:
|
|
6
6
|
class ContentSecurityPolicy
|
|
7
7
|
class Middleware
|
|
8
|
-
CONTENT_TYPE = "Content-Type"
|
|
9
|
-
POLICY = "Content-Security-Policy"
|
|
10
|
-
POLICY_REPORT_ONLY = "Content-Security-Policy-Report-Only"
|
|
8
|
+
CONTENT_TYPE = "Content-Type"
|
|
9
|
+
POLICY = "Content-Security-Policy"
|
|
10
|
+
POLICY_REPORT_ONLY = "Content-Security-Policy-Report-Only"
|
|
11
11
|
|
|
12
12
|
def initialize(app)
|
|
13
13
|
@app = app
|
|
@@ -21,23 +21,19 @@ module ActionDispatch #:nodoc:
|
|
|
21
21
|
return response if policy_present?(headers)
|
|
22
22
|
|
|
23
23
|
if policy = request.content_security_policy
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
headers[header_name(request)] = policy.build(request.controller_instance)
|
|
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)
|
|
31
28
|
end
|
|
32
29
|
|
|
33
30
|
response
|
|
34
31
|
end
|
|
35
32
|
|
|
36
33
|
private
|
|
37
|
-
|
|
38
34
|
def html_response?(headers)
|
|
39
35
|
if content_type = headers[CONTENT_TYPE]
|
|
40
|
-
|
|
36
|
+
/html/.match?(content_type)
|
|
41
37
|
end
|
|
42
38
|
end
|
|
43
39
|
|
|
@@ -55,10 +51,11 @@ module ActionDispatch #:nodoc:
|
|
|
55
51
|
end
|
|
56
52
|
|
|
57
53
|
module Request
|
|
58
|
-
POLICY = "action_dispatch.content_security_policy"
|
|
59
|
-
POLICY_REPORT_ONLY = "action_dispatch.content_security_policy_report_only"
|
|
60
|
-
NONCE_GENERATOR = "action_dispatch.content_security_policy_nonce_generator"
|
|
61
|
-
NONCE = "action_dispatch.content_security_policy_nonce"
|
|
54
|
+
POLICY = "action_dispatch.content_security_policy"
|
|
55
|
+
POLICY_REPORT_ONLY = "action_dispatch.content_security_policy_report_only"
|
|
56
|
+
NONCE_GENERATOR = "action_dispatch.content_security_policy_nonce_generator"
|
|
57
|
+
NONCE = "action_dispatch.content_security_policy_nonce"
|
|
58
|
+
NONCE_DIRECTIVES = "action_dispatch.content_security_policy_nonce_directives"
|
|
62
59
|
|
|
63
60
|
def content_security_policy
|
|
64
61
|
get_header(POLICY)
|
|
@@ -84,6 +81,14 @@ module ActionDispatch #:nodoc:
|
|
|
84
81
|
set_header(NONCE_GENERATOR, generator)
|
|
85
82
|
end
|
|
86
83
|
|
|
84
|
+
def content_security_policy_nonce_directives
|
|
85
|
+
get_header(NONCE_DIRECTIVES)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def content_security_policy_nonce_directives=(generator)
|
|
89
|
+
set_header(NONCE_DIRECTIVES, generator)
|
|
90
|
+
end
|
|
91
|
+
|
|
87
92
|
def content_security_policy_nonce
|
|
88
93
|
if content_security_policy_nonce_generator
|
|
89
94
|
if nonce = get_header(NONCE)
|
|
@@ -95,7 +100,6 @@ module ActionDispatch #:nodoc:
|
|
|
95
100
|
end
|
|
96
101
|
|
|
97
102
|
private
|
|
98
|
-
|
|
99
103
|
def generate_content_security_policy_nonce
|
|
100
104
|
content_security_policy_nonce_generator.call(self)
|
|
101
105
|
end
|
|
@@ -113,7 +117,9 @@ module ActionDispatch #:nodoc:
|
|
|
113
117
|
blob: "blob:",
|
|
114
118
|
filesystem: "filesystem:",
|
|
115
119
|
report_sample: "'report-sample'",
|
|
116
|
-
strict_dynamic: "'strict-dynamic'"
|
|
120
|
+
strict_dynamic: "'strict-dynamic'",
|
|
121
|
+
ws: "ws:",
|
|
122
|
+
wss: "wss:"
|
|
117
123
|
}.freeze
|
|
118
124
|
|
|
119
125
|
DIRECTIVES = {
|
|
@@ -129,12 +135,19 @@ module ActionDispatch #:nodoc:
|
|
|
129
135
|
manifest_src: "manifest-src",
|
|
130
136
|
media_src: "media-src",
|
|
131
137
|
object_src: "object-src",
|
|
138
|
+
prefetch_src: "prefetch-src",
|
|
132
139
|
script_src: "script-src",
|
|
140
|
+
script_src_attr: "script-src-attr",
|
|
141
|
+
script_src_elem: "script-src-elem",
|
|
133
142
|
style_src: "style-src",
|
|
143
|
+
style_src_attr: "style-src-attr",
|
|
144
|
+
style_src_elem: "style-src-elem",
|
|
134
145
|
worker_src: "worker-src"
|
|
135
146
|
}.freeze
|
|
136
147
|
|
|
137
|
-
|
|
148
|
+
DEFAULT_NONCE_DIRECTIVES = %w[script-src style-src].freeze
|
|
149
|
+
|
|
150
|
+
private_constant :MAPPINGS, :DIRECTIVES, :DEFAULT_NONCE_DIRECTIVES
|
|
138
151
|
|
|
139
152
|
attr_reader :directives
|
|
140
153
|
|
|
@@ -203,8 +216,9 @@ module ActionDispatch #:nodoc:
|
|
|
203
216
|
end
|
|
204
217
|
end
|
|
205
218
|
|
|
206
|
-
def build(context = nil)
|
|
207
|
-
|
|
219
|
+
def build(context = nil, nonce = nil, nonce_directives = nil)
|
|
220
|
+
nonce_directives = DEFAULT_NONCE_DIRECTIVES if nonce_directives.nil?
|
|
221
|
+
build_directives(context, nonce, nonce_directives).compact.join("; ")
|
|
208
222
|
end
|
|
209
223
|
|
|
210
224
|
private
|
|
@@ -227,10 +241,14 @@ module ActionDispatch #:nodoc:
|
|
|
227
241
|
end
|
|
228
242
|
end
|
|
229
243
|
|
|
230
|
-
def build_directives(context)
|
|
244
|
+
def build_directives(context, nonce, nonce_directives)
|
|
231
245
|
@directives.map do |directive, sources|
|
|
232
246
|
if sources.is_a?(Array)
|
|
233
|
-
|
|
247
|
+
if nonce && nonce_directive?(directive, nonce_directives)
|
|
248
|
+
"#{directive} #{build_directive(sources, context).join(' ')} 'nonce-#{nonce}'"
|
|
249
|
+
else
|
|
250
|
+
"#{directive} #{build_directive(sources, context).join(' ')}"
|
|
251
|
+
end
|
|
234
252
|
elsif sources
|
|
235
253
|
directive
|
|
236
254
|
else
|
|
@@ -253,11 +271,16 @@ module ActionDispatch #:nodoc:
|
|
|
253
271
|
if context.nil?
|
|
254
272
|
raise RuntimeError, "Missing context for the dynamic content security policy source: #{source.inspect}"
|
|
255
273
|
else
|
|
256
|
-
context.instance_exec(&source)
|
|
274
|
+
resolved = context.instance_exec(&source)
|
|
275
|
+
resolved.is_a?(Symbol) ? apply_mapping(resolved) : resolved
|
|
257
276
|
end
|
|
258
277
|
else
|
|
259
278
|
raise RuntimeError, "Unexpected content security policy source: #{source.inspect}"
|
|
260
279
|
end
|
|
261
280
|
end
|
|
281
|
+
|
|
282
|
+
def nonce_directive?(directive, nonce_directives)
|
|
283
|
+
nonce_directives.include?(directive)
|
|
284
|
+
end
|
|
262
285
|
end
|
|
263
286
|
end
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "
|
|
3
|
+
require "active_support/parameter_filter"
|
|
4
4
|
|
|
5
5
|
module ActionDispatch
|
|
6
6
|
module Http
|
|
@@ -9,8 +9,8 @@ module ActionDispatch
|
|
|
9
9
|
# sub-hashes of the params hash to filter. Filtering only certain sub-keys
|
|
10
10
|
# from a hash is possible by using the dot notation: 'credit_card.number'.
|
|
11
11
|
# If a block is given, each key and value of the params hash and all
|
|
12
|
-
# sub-hashes
|
|
13
|
-
# String#replace or similar
|
|
12
|
+
# sub-hashes are passed to it, where the value or the key can be replaced using
|
|
13
|
+
# String#replace or similar methods.
|
|
14
14
|
#
|
|
15
15
|
# env["action_dispatch.parameter_filter"] = [:password]
|
|
16
16
|
# => replaces the value to all keys matching /password/i with "[FILTERED]"
|
|
@@ -23,13 +23,13 @@ module ActionDispatch
|
|
|
23
23
|
# change { file: { code: "xxxx"} }
|
|
24
24
|
#
|
|
25
25
|
# env["action_dispatch.parameter_filter"] = -> (k, v) do
|
|
26
|
-
# v.reverse! if k
|
|
26
|
+
# v.reverse! if k.match?(/secret/i)
|
|
27
27
|
# end
|
|
28
28
|
# => reverses the value to all keys matching /secret/i
|
|
29
29
|
module FilterParameters
|
|
30
30
|
ENV_MATCH = [/RAW_POST_DATA/, "rack.request.form_vars"] # :nodoc:
|
|
31
|
-
NULL_PARAM_FILTER = ParameterFilter.new # :nodoc:
|
|
32
|
-
NULL_ENV_FILTER = ParameterFilter.new ENV_MATCH # :nodoc:
|
|
31
|
+
NULL_PARAM_FILTER = ActiveSupport::ParameterFilter.new # :nodoc:
|
|
32
|
+
NULL_ENV_FILTER = ActiveSupport::ParameterFilter.new ENV_MATCH # :nodoc:
|
|
33
33
|
|
|
34
34
|
def initialize
|
|
35
35
|
super
|
|
@@ -41,6 +41,8 @@ module ActionDispatch
|
|
|
41
41
|
# Returns a hash of parameters with all sensitive data replaced.
|
|
42
42
|
def filtered_parameters
|
|
43
43
|
@filtered_parameters ||= parameter_filter.filter(parameters)
|
|
44
|
+
rescue ActionDispatch::Http::Parameters::ParseError
|
|
45
|
+
@filtered_parameters = {}
|
|
44
46
|
end
|
|
45
47
|
|
|
46
48
|
# Returns a hash of request.env with all sensitive data replaced.
|
|
@@ -54,7 +56,6 @@ module ActionDispatch
|
|
|
54
56
|
end
|
|
55
57
|
|
|
56
58
|
private
|
|
57
|
-
|
|
58
59
|
def parameter_filter # :doc:
|
|
59
60
|
parameter_filter_for fetch_header("action_dispatch.parameter_filter") {
|
|
60
61
|
return NULL_PARAM_FILTER
|
|
@@ -69,7 +70,7 @@ module ActionDispatch
|
|
|
69
70
|
end
|
|
70
71
|
|
|
71
72
|
def parameter_filter_for(filters) # :doc:
|
|
72
|
-
ParameterFilter.new(filters)
|
|
73
|
+
ActiveSupport::ParameterFilter.new(filters)
|
|
73
74
|
end
|
|
74
75
|
|
|
75
76
|
KV_RE = "[^&;=]+"
|