actionpack 7.0.0.alpha2 → 7.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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +163 -0
  3. data/lib/abstract_controller/callbacks.rb +15 -2
  4. data/lib/abstract_controller/translation.rb +4 -1
  5. data/lib/action_controller/log_subscriber.rb +1 -2
  6. data/lib/action_controller/metal/helpers.rb +1 -1
  7. data/lib/action_controller/metal/http_authentication.rb +2 -1
  8. data/lib/action_controller/metal/instrumentation.rb +2 -0
  9. data/lib/action_controller/metal/params_wrapper.rb +13 -4
  10. data/lib/action_controller/metal/redirecting.rb +58 -22
  11. data/lib/action_controller/metal/request_forgery_protection.rb +40 -35
  12. data/lib/action_controller/metal/strong_parameters.rb +61 -20
  13. data/lib/action_controller/metal/testing.rb +9 -0
  14. data/lib/action_controller/railtie.rb +16 -10
  15. data/lib/action_controller/test_case.rb +19 -2
  16. data/lib/action_controller.rb +0 -1
  17. data/lib/action_dispatch/http/response.rb +0 -12
  18. data/lib/action_dispatch/http/url.rb +2 -9
  19. data/lib/action_dispatch/journey/nodes/node.rb +2 -2
  20. data/lib/action_dispatch/journey/route.rb +1 -1
  21. data/lib/action_dispatch/middleware/cookies.rb +1 -1
  22. data/lib/action_dispatch/middleware/executor.rb +3 -0
  23. data/lib/action_dispatch/middleware/host_authorization.rb +72 -35
  24. data/lib/action_dispatch/middleware/server_timing.rb +33 -0
  25. data/lib/action_dispatch/middleware/show_exceptions.rb +10 -0
  26. data/lib/action_dispatch/middleware/static.rb +0 -1
  27. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb +1 -0
  28. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.text.erb +2 -0
  29. data/lib/action_dispatch/routing/inspector.rb +1 -1
  30. data/lib/action_dispatch/routing/mapper.rb +10 -6
  31. data/lib/action_dispatch/routing/route_set.rb +5 -0
  32. data/lib/action_dispatch/system_test_case.rb +7 -1
  33. data/lib/action_dispatch/system_testing/browser.rb +2 -12
  34. data/lib/action_dispatch/system_testing/driver.rb +13 -9
  35. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +1 -1
  36. data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +0 -8
  37. data/lib/action_dispatch/testing/test_process.rb +1 -27
  38. data/lib/action_dispatch.rb +1 -0
  39. data/lib/action_pack/gem_version.rb +1 -1
  40. metadata +16 -15
  41. data/lib/action_controller/metal/query_tags.rb +0 -16
@@ -81,7 +81,7 @@ module ActionController
81
81
  # })
82
82
  #
83
83
  # permitted = params.require(:person).permit(:name, :age)
84
- # permitted # => <ActionController::Parameters {"name"=>"Francesco", "age"=>22} permitted: true>
84
+ # permitted # => #<ActionController::Parameters {"name"=>"Francesco", "age"=>22} permitted: true>
85
85
  # permitted.permitted? # => true
86
86
  #
87
87
  # Person.first.update!(permitted)
@@ -111,7 +111,7 @@ module ActionController
111
111
  #
112
112
  # params = ActionController::Parameters.new(a: "123", b: "456")
113
113
  # params.permit(:c)
114
- # # => <ActionController::Parameters {} permitted: true>
114
+ # # => #<ActionController::Parameters {} permitted: true>
115
115
  #
116
116
  # ActionController::Parameters.action_on_unpermitted_parameters = :raise
117
117
  #
@@ -442,7 +442,7 @@ module ActionController
442
442
  # either present or the singleton +false+, returns said value:
443
443
  #
444
444
  # ActionController::Parameters.new(person: { name: "Francesco" }).require(:person)
445
- # # => <ActionController::Parameters {"name"=>"Francesco"} permitted: false>
445
+ # # => #<ActionController::Parameters {"name"=>"Francesco"} permitted: false>
446
446
  #
447
447
  # Otherwise raises <tt>ActionController::ParameterMissing</tt>:
448
448
  #
@@ -570,13 +570,48 @@ module ActionController
570
570
  # })
571
571
  #
572
572
  # params.require(:person).permit(:contact)
573
- # # => <ActionController::Parameters {} permitted: true>
573
+ # # => #<ActionController::Parameters {} permitted: true>
574
574
  #
575
575
  # params.require(:person).permit(contact: :phone)
576
- # # => <ActionController::Parameters {"contact"=><ActionController::Parameters {"phone"=>"555-1234"} permitted: true>} permitted: true>
576
+ # # => #<ActionController::Parameters {"contact"=>#<ActionController::Parameters {"phone"=>"555-1234"} permitted: true>} permitted: true>
577
577
  #
578
578
  # params.require(:person).permit(contact: [ :email, :phone ])
579
- # # => <ActionController::Parameters {"contact"=><ActionController::Parameters {"email"=>"none@test.com", "phone"=>"555-1234"} permitted: true>} permitted: true>
579
+ # # => #<ActionController::Parameters {"contact"=>#<ActionController::Parameters {"email"=>"none@test.com", "phone"=>"555-1234"} permitted: true>} permitted: true>
580
+ #
581
+ # If your parameters specify multiple parameters indexed by a number,
582
+ # you can permit each set of parameters under the numeric key to be the same using the same syntax as permitting a single item.
583
+ #
584
+ # params = ActionController::Parameters.new({
585
+ # person: {
586
+ # '0': {
587
+ # email: "none@test.com",
588
+ # phone: "555-1234"
589
+ # },
590
+ # '1': {
591
+ # email: "nothing@test.com",
592
+ # phone: "555-6789"
593
+ # },
594
+ # }
595
+ # })
596
+ # params.permit(person: [:email]).to_h
597
+ # # => {"person"=>{"0"=>{"email"=>"none@test.com"}, "1"=>{"email"=>"nothing@test.com"}}}
598
+ #
599
+ # If you want to specify what keys you want from each numeric key, you can instead specify each one individually
600
+ #
601
+ # params = ActionController::Parameters.new({
602
+ # person: {
603
+ # '0': {
604
+ # email: "none@test.com",
605
+ # phone: "555-1234"
606
+ # },
607
+ # '1': {
608
+ # email: "nothing@test.com",
609
+ # phone: "555-6789"
610
+ # },
611
+ # }
612
+ # })
613
+ # params.permit(person: { '0': [:email], '1': [:phone]}).to_h
614
+ # # => {"person"=>{"0"=>{"email"=>"none@test.com"}, "1"=>{"phone"=>"555-6789"}}}
580
615
  def permit(*filters)
581
616
  params = self.class.new
582
617
 
@@ -598,7 +633,7 @@ module ActionController
598
633
  # returns +nil+.
599
634
  #
600
635
  # params = ActionController::Parameters.new(person: { name: "Francesco" })
601
- # params[:person] # => <ActionController::Parameters {"name"=>"Francesco"} permitted: false>
636
+ # params[:person] # => #<ActionController::Parameters {"name"=>"Francesco"} permitted: false>
602
637
  # params[:none] # => nil
603
638
  def [](key)
604
639
  convert_hashes_to_parameters(key, @parameters[key])
@@ -618,9 +653,9 @@ module ActionController
618
653
  # is given, then that will be run and its result returned.
619
654
  #
620
655
  # params = ActionController::Parameters.new(person: { name: "Francesco" })
621
- # params.fetch(:person) # => <ActionController::Parameters {"name"=>"Francesco"} permitted: false>
656
+ # params.fetch(:person) # => #<ActionController::Parameters {"name"=>"Francesco"} permitted: false>
622
657
  # params.fetch(:none) # => ActionController::ParameterMissing: param is missing or the value is empty: none
623
- # params.fetch(:none, {}) # => <ActionController::Parameters {} permitted: false>
658
+ # params.fetch(:none, {}) # => #<ActionController::Parameters {} permitted: false>
624
659
  # params.fetch(:none, "Francesco") # => "Francesco"
625
660
  # params.fetch(:none) { "Francesco" } # => "Francesco"
626
661
  def fetch(key, *args)
@@ -654,8 +689,8 @@ module ActionController
654
689
  # don't exist, returns an empty hash.
655
690
  #
656
691
  # params = ActionController::Parameters.new(a: 1, b: 2, c: 3)
657
- # params.slice(:a, :b) # => <ActionController::Parameters {"a"=>1, "b"=>2} permitted: false>
658
- # params.slice(:d) # => <ActionController::Parameters {} permitted: false>
692
+ # params.slice(:a, :b) # => #<ActionController::Parameters {"a"=>1, "b"=>2} permitted: false>
693
+ # params.slice(:d) # => #<ActionController::Parameters {} permitted: false>
659
694
  def slice(*keys)
660
695
  new_instance_with_inherited_permitted_status(@parameters.slice(*keys))
661
696
  end
@@ -671,8 +706,8 @@ module ActionController
671
706
  # filters out the given +keys+.
672
707
  #
673
708
  # params = ActionController::Parameters.new(a: 1, b: 2, c: 3)
674
- # params.except(:a, :b) # => <ActionController::Parameters {"c"=>3} permitted: false>
675
- # params.except(:d) # => <ActionController::Parameters {"a"=>1, "b"=>2, "c"=>3} permitted: false>
709
+ # params.except(:a, :b) # => #<ActionController::Parameters {"c"=>3} permitted: false>
710
+ # params.except(:d) # => #<ActionController::Parameters {"a"=>1, "b"=>2, "c"=>3} permitted: false>
676
711
  def except(*keys)
677
712
  new_instance_with_inherited_permitted_status(@parameters.except(*keys))
678
713
  end
@@ -680,8 +715,8 @@ module ActionController
680
715
  # Removes and returns the key/value pairs matching the given keys.
681
716
  #
682
717
  # params = ActionController::Parameters.new(a: 1, b: 2, c: 3)
683
- # params.extract!(:a, :b) # => <ActionController::Parameters {"a"=>1, "b"=>2} permitted: false>
684
- # params # => <ActionController::Parameters {"c"=>3} permitted: false>
718
+ # params.extract!(:a, :b) # => #<ActionController::Parameters {"a"=>1, "b"=>2} permitted: false>
719
+ # params # => #<ActionController::Parameters {"c"=>3} permitted: false>
685
720
  def extract!(*keys)
686
721
  new_instance_with_inherited_permitted_status(@parameters.extract!(*keys))
687
722
  end
@@ -691,7 +726,7 @@ module ActionController
691
726
  #
692
727
  # params = ActionController::Parameters.new(a: 1, b: 2, c: 3)
693
728
  # params.transform_values { |x| x * 2 }
694
- # # => <ActionController::Parameters {"a"=>2, "b"=>4, "c"=>6} permitted: false>
729
+ # # => #<ActionController::Parameters {"a"=>2, "b"=>4, "c"=>6} permitted: false>
695
730
  def transform_values
696
731
  return to_enum(:transform_values) unless block_given?
697
732
  new_instance_with_inherited_permitted_status(
@@ -928,7 +963,7 @@ module ActionController
928
963
  when Array
929
964
  return value if converted_arrays.member?(value)
930
965
  converted = value.map { |_| convert_value_to_parameters(_) }
931
- converted_arrays << converted
966
+ converted_arrays << converted.dup
932
967
  converted
933
968
  when Hash
934
969
  self.class.new(value)
@@ -937,12 +972,18 @@ module ActionController
937
972
  end
938
973
  end
939
974
 
940
- def each_element(object, &block)
975
+ def specify_numeric_keys?(filter)
976
+ if filter.respond_to?(:keys)
977
+ filter.keys.any? { |key| /\A-?\d+\z/.match?(key) }
978
+ end
979
+ end
980
+
981
+ def each_element(object, filter, &block)
941
982
  case object
942
983
  when Array
943
984
  object.grep(Parameters).filter_map(&block)
944
985
  when Parameters
945
- if object.nested_attributes?
986
+ if object.nested_attributes? && !specify_numeric_keys?(filter)
946
987
  object.each_nested_attribute(&block)
947
988
  else
948
989
  yield object
@@ -1055,7 +1096,7 @@ module ActionController
1055
1096
  end
1056
1097
  elsif non_scalar?(value)
1057
1098
  # Declaration { user: :name } or { user: [:name, :age, { address: ... }] }.
1058
- params[key] = each_element(value) do |element|
1099
+ params[key] = each_element(value, filter[key]) do |element|
1059
1100
  element.permit(*Array.wrap(filter[key]))
1060
1101
  end
1061
1102
  end
@@ -4,6 +4,15 @@ module ActionController
4
4
  module Testing
5
5
  # Behavior specific to functional tests
6
6
  module Functional # :nodoc:
7
+ def clear_instance_variables_between_requests
8
+ if defined?(@_ivars)
9
+ new_ivars = instance_variables - @_ivars
10
+ new_ivars.each { |ivar| remove_instance_variable(ivar) }
11
+ end
12
+
13
+ @_ivars = instance_variables
14
+ end
15
+
7
16
  def recycle!
8
17
  @_url_options = nil
9
18
  self.formats = nil
@@ -12,6 +12,7 @@ module ActionController
12
12
  config.action_controller = ActiveSupport::OrderedOptions.new
13
13
  config.action_controller.raise_on_open_redirects = false
14
14
  config.action_controller.log_query_tags_around_actions = true
15
+ config.action_controller.wrap_parameters_by_default = false
15
16
 
16
17
  config.eager_load_namespaces << ActionController
17
18
 
@@ -62,15 +63,18 @@ module ActionController
62
63
  extend ::AbstractController::Railties::RoutesHelpers.with(app.routes)
63
64
  extend ::ActionController::Railties::Helpers
64
65
 
66
+ wrap_parameters format: [:json] if options.wrap_parameters_by_default && respond_to?(:wrap_parameters)
67
+
65
68
  # Configs used in other initializers
66
- options = options.except(
69
+ filtered_options = options.except(
67
70
  :log_query_tags_around_actions,
68
71
  :permit_all_parameters,
69
72
  :action_on_unpermitted_parameters,
70
- :always_permitted_parameters
73
+ :always_permitted_parameters,
74
+ :wrap_parameters_by_default
71
75
  )
72
76
 
73
- options.each do |k, v|
77
+ filtered_options.each do |k, v|
74
78
  k = "#{k}="
75
79
  if respond_to?(k)
76
80
  send(k, v)
@@ -109,18 +113,20 @@ module ActionController
109
113
  if query_logs_tags_enabled
110
114
  app.config.active_record.query_log_tags += [:controller, :action]
111
115
 
112
- ActiveSupport.on_load(:action_controller) do
113
- include ActionController::QueryTags
114
- end
115
-
116
116
  ActiveSupport.on_load(:active_record) do
117
117
  ActiveRecord::QueryLogs.taggings.merge!(
118
- controller: ->(context) { context[:controller].controller_name },
119
- action: ->(context) { context[:controller].action_name },
120
- namespaced_controller: ->(context) { context[:controller].class.name }
118
+ controller: ->(context) { context[:controller]&.controller_name },
119
+ action: ->(context) { context[:controller]&.action_name },
120
+ namespaced_controller: ->(context) { context[:controller].class.name if context[:controller] }
121
121
  )
122
122
  end
123
123
  end
124
124
  end
125
+
126
+ initializer "action_controller.test_case" do |app|
127
+ ActiveSupport.on_load(:action_controller_test_case) do
128
+ ActionController::TestCase.executor_around_each_request = app.config.active_support.executor_around_test_case
129
+ end
130
+ end
125
131
  end
126
132
  end
@@ -333,6 +333,8 @@ module ActionController
333
333
  #
334
334
  # assert_redirected_to page_url(title: 'foo')
335
335
  class TestCase < ActiveSupport::TestCase
336
+ singleton_class.attr_accessor :executor_around_each_request
337
+
336
338
  module Behavior
337
339
  extend ActiveSupport::Concern
338
340
  include ActionDispatch::TestProcess
@@ -463,9 +465,15 @@ module ActionController
463
465
  # prefer using #get, #post, #patch, #put, #delete and #head methods
464
466
  # respectively which will make tests more expressive.
465
467
  #
468
+ # It's not recommended to make more than one request in the same test. Instance
469
+ # variables that are set in one request will not persist to the next request,
470
+ # but it's not guaranteed that all Rails internal state will be reset. Prefer
471
+ # ActionDispatch::IntegrationTest for making multiple requests in the same test.
472
+ #
466
473
  # Note that the request method is not verified.
467
474
  def process(action, method: "GET", params: nil, session: nil, body: nil, flash: {}, format: nil, xhr: false, as: nil)
468
475
  check_required_ivars
476
+ @controller.clear_instance_variables_between_requests
469
477
 
470
478
  action = +action.to_s
471
479
  http_method = method.to_s.upcase
@@ -578,10 +586,19 @@ module ActionController
578
586
  end
579
587
  end
580
588
 
589
+ def wrap_execution(&block)
590
+ if ActionController::TestCase.executor_around_each_request && defined?(Rails.application) && Rails.application
591
+ Rails.application.executor.wrap(&block)
592
+ else
593
+ yield
594
+ end
595
+ end
596
+
581
597
  def process_controller_response(action, cookies, xhr)
582
598
  begin
583
599
  @controller.recycle!
584
- @controller.dispatch(action, @request, @response)
600
+
601
+ wrap_execution { @controller.dispatch(action, @request, @response) }
585
602
  ensure
586
603
  @request = @controller.request
587
604
  @response = @controller.response
@@ -627,7 +644,7 @@ module ActionController
627
644
  end
628
645
 
629
646
  def check_required_ivars
630
- # Sanity check for required instance variables so we can give an
647
+ # Check for required instance variables so we can give an
631
648
  # understandable error message.
632
649
  [:@routes, :@controller, :@request, :@response].each do |iv_name|
633
650
  if !instance_variable_defined?(iv_name) || instance_variable_get(iv_name).nil?
@@ -37,7 +37,6 @@ module ActionController
37
37
  autoload :Logging
38
38
  autoload :MimeResponds
39
39
  autoload :ParamsWrapper
40
- autoload :QueryTags
41
40
  autoload :Redirecting
42
41
  autoload :Renderers
43
42
  autoload :Rendering
@@ -86,18 +86,6 @@ module ActionDispatch # :nodoc:
86
86
  cattr_accessor :default_charset, default: "utf-8"
87
87
  cattr_accessor :default_headers
88
88
 
89
- def self.return_only_media_type_on_content_type=(*)
90
- ActiveSupport::Deprecation.warn(
91
- ".return_only_media_type_on_content_type= is deprecated with no replacement and will be removed in 7.0."
92
- )
93
- end
94
-
95
- def self.return_only_media_type_on_content_type
96
- ActiveSupport::Deprecation.warn(
97
- ".return_only_media_type_on_content_type is deprecated with no replacement and will be removed in 7.0."
98
- )
99
- end
100
-
101
89
  include Rack::Response::Helpers
102
90
  # Aliasing these off because AD::Http::Cache::Response defines them.
103
91
  alias :_cache_control :cache_control
@@ -71,7 +71,8 @@ module ActionDispatch
71
71
  path = options[:script_name].to_s.chomp("/")
72
72
  path << options[:path] if options.key?(:path)
73
73
 
74
- add_trailing_slash(path) if options[:trailing_slash]
74
+ path = "/" if options[:trailing_slash] && path.blank?
75
+
75
76
  add_params(path, options[:params]) if options.key?(:params)
76
77
  add_anchor(path, options[:anchor]) if options.key?(:anchor)
77
78
 
@@ -101,14 +102,6 @@ module ActionDispatch
101
102
  parts[0..-(tld_length + 2)]
102
103
  end
103
104
 
104
- def add_trailing_slash(path)
105
- if path.include?("?")
106
- path.sub!(/\?/, '/\&')
107
- elsif !path.include?(".")
108
- path.sub!(/[^\/]\z|\A\z/, '\&/')
109
- end
110
- end
111
-
112
105
  def build_host_url(host, port, protocol, options, path)
113
106
  if match = host.match(HOST_REGEXP)
114
107
  protocol ||= match[1] unless protocol == false
@@ -53,7 +53,7 @@ module ActionDispatch
53
53
  if formatted != false
54
54
  # Add a constraint for wildcard route to make it non-greedy and
55
55
  # match the optional format part of the route by default.
56
- wildcard_options[node.name.to_sym] ||= /.+?/
56
+ wildcard_options[node.name.to_sym] ||= /.+?/m
57
57
  end
58
58
  end
59
59
 
@@ -166,7 +166,7 @@ module ActionDispatch
166
166
  super(left)
167
167
 
168
168
  # By default wildcard routes are non-greedy and must match something.
169
- @regexp = /.+?/
169
+ @regexp = /.+?/m
170
170
  end
171
171
 
172
172
  def star?; true; end
@@ -91,7 +91,7 @@ module ActionDispatch
91
91
  # as requirements.
92
92
  def requirements
93
93
  @defaults.merge(path.requirements).delete_if { |_, v|
94
- /.+?/ == v
94
+ /.+?/m == v
95
95
  }
96
96
  end
97
97
 
@@ -439,7 +439,7 @@ module ActionDispatch
439
439
  end
440
440
 
441
441
  def write_cookie?(cookie)
442
- request.ssl? || !cookie[:secure] || always_write_cookie
442
+ request.ssl? || !cookie[:secure] || always_write_cookie || request.host.end_with?(".onion")
443
443
  end
444
444
 
445
445
  def handle_options(options)
@@ -13,6 +13,9 @@ module ActionDispatch
13
13
  begin
14
14
  response = @app.call(env)
15
15
  returned = response << ::Rack::BodyProxy.new(response.pop) { state.complete! }
16
+ rescue => error
17
+ @executor.error_reporter.report(error, handled: false)
18
+ raise
16
19
  ensure
17
20
  state.complete! unless returned
18
21
  end
@@ -11,8 +11,22 @@ module ActionDispatch
11
11
  #
12
12
  # When a request comes to an unauthorized host, the +response_app+
13
13
  # application will be executed and rendered. If no +response_app+ is given, a
14
- # default one will run, which responds with <tt>403 Forbidden</tt>.
14
+ # default one will run.
15
+ # The default response app logs blocked host info with level 'error' and
16
+ # responds with <tt>403 Forbidden</tt>. The body of the response contains debug info
17
+ # if +config.consider_all_requests_local+ is set to true, otherwise the body is empty.
15
18
  class HostAuthorization
19
+ ALLOWED_HOSTS_IN_DEVELOPMENT = [".localhost", IPAddr.new("0.0.0.0/0"), IPAddr.new("::/0")]
20
+ PORT_REGEX = /(?::\d+)/ # :nodoc:
21
+ IPV4_HOSTNAME = /(?<host>\d+\.\d+\.\d+\.\d+)#{PORT_REGEX}?/ # :nodoc:
22
+ IPV6_HOSTNAME = /(?<host>[a-f0-9]*:[a-f0-9.:]+)/i # :nodoc:
23
+ IPV6_HOSTNAME_WITH_PORT = /\[#{IPV6_HOSTNAME}\]#{PORT_REGEX}/i # :nodoc:
24
+ VALID_IP_HOSTNAME = Regexp.union( # :nodoc:
25
+ /\A#{IPV4_HOSTNAME}\z/,
26
+ /\A#{IPV6_HOSTNAME}\z/,
27
+ /\A#{IPV6_HOSTNAME_WITH_PORT}\z/,
28
+ )
29
+
16
30
  class Permissions # :nodoc:
17
31
  def initialize(hosts)
18
32
  @hosts = sanitize_hosts(hosts)
@@ -24,11 +38,17 @@ module ActionDispatch
24
38
 
25
39
  def allows?(host)
26
40
  @hosts.any? do |allowed|
27
- allowed === host
28
- rescue
29
- # IPAddr#=== raises an error if you give it a hostname instead of
30
- # IP. Treat similar errors as blocked access.
31
- false
41
+ if allowed.is_a?(IPAddr)
42
+ begin
43
+ allowed === extract_hostname(host)
44
+ rescue
45
+ # IPAddr#=== raises an error if you give it a hostname instead of
46
+ # IP. Treat similar errors as blocked access.
47
+ false
48
+ end
49
+ else
50
+ allowed === host
51
+ end
32
52
  end
33
53
  end
34
54
 
@@ -44,46 +64,67 @@ module ActionDispatch
44
64
  end
45
65
 
46
66
  def sanitize_regexp(host)
47
- /\A#{host}\z/
67
+ /\A#{host}#{PORT_REGEX}?\z/
48
68
  end
49
69
 
50
70
  def sanitize_string(host)
51
71
  if host.start_with?(".")
52
- /\A(.+\.)?#{Regexp.escape(host[1..-1])}\z/i
72
+ /\A([a-z0-9-]+\.)?#{Regexp.escape(host[1..-1])}#{PORT_REGEX}?\z/i
53
73
  else
54
- /\A#{Regexp.escape host}\z/i
74
+ /\A#{Regexp.escape host}#{PORT_REGEX}?\z/i
55
75
  end
56
76
  end
77
+
78
+ def extract_hostname(host)
79
+ host.slice(VALID_IP_HOSTNAME, "host") || host
80
+ end
57
81
  end
58
82
 
59
- DEFAULT_RESPONSE_APP = -> env do
60
- request = Request.new(env)
83
+ class DefaultResponseApp # :nodoc:
84
+ RESPONSE_STATUS = 403
85
+
86
+ def call(env)
87
+ request = Request.new(env)
88
+ format = request.xhr? ? "text/plain" : "text/html"
89
+
90
+ log_error(request)
91
+ response(format, response_body(request))
92
+ end
93
+
94
+ private
95
+ def response_body(request)
96
+ return "" unless request.get_header("action_dispatch.show_detailed_exceptions")
61
97
 
62
- format = request.xhr? ? "text/plain" : "text/html"
63
- template = DebugView.new(host: request.host)
64
- body = template.render(template: "rescues/blocked_host", layout: "rescues/layout")
98
+ template = DebugView.new(host: request.host)
99
+ template.render(template: "rescues/blocked_host", layout: "rescues/layout")
100
+ end
101
+
102
+ def response(format, body)
103
+ [RESPONSE_STATUS,
104
+ { "Content-Type" => "#{format}; charset=#{Response.default_charset}",
105
+ "Content-Length" => body.bytesize.to_s },
106
+ [body]]
107
+ end
108
+
109
+ def log_error(request)
110
+ logger = available_logger(request)
111
+
112
+ return unless logger
113
+
114
+ logger.error("[#{self.class.name}] Blocked host: #{request.host}")
115
+ end
65
116
 
66
- [403, {
67
- "Content-Type" => "#{format}; charset=#{Response.default_charset}",
68
- "Content-Length" => body.bytesize.to_s,
69
- }, [body]]
117
+ def available_logger(request)
118
+ request.logger || ActionView::Base.logger
119
+ end
70
120
  end
71
121
 
72
- def initialize(app, hosts, deprecated_response_app = nil, exclude: nil, response_app: nil)
122
+ def initialize(app, hosts, exclude: nil, response_app: nil)
73
123
  @app = app
74
124
  @permissions = Permissions.new(hosts)
75
125
  @exclude = exclude
76
126
 
77
- unless deprecated_response_app.nil?
78
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
79
- `action_dispatch.hosts_response_app` is deprecated and will be ignored in Rails 7.0.
80
- Use the Host Authorization `response_app` setting instead.
81
- MSG
82
-
83
- response_app ||= deprecated_response_app
84
- end
85
-
86
- @response_app = response_app || DEFAULT_RESPONSE_APP
127
+ @response_app = response_app || DefaultResponseApp.new
87
128
  end
88
129
 
89
130
  def call(env)
@@ -100,13 +141,9 @@ module ActionDispatch
100
141
  end
101
142
 
102
143
  private
103
- HOSTNAME = /[a-z0-9.-]+|\[[a-f0-9]*:[a-f0-9.:]+\]/i
104
- VALID_ORIGIN_HOST = /\A(#{HOSTNAME})(?::\d+)?\z/
105
- VALID_FORWARDED_HOST = /(?:\A|,[ ]?)(#{HOSTNAME})(?::\d+)?\z/
106
-
107
144
  def authorized?(request)
108
- origin_host = request.get_header("HTTP_HOST")&.slice(VALID_ORIGIN_HOST, 1) || ""
109
- forwarded_host = request.x_forwarded_host&.slice(VALID_FORWARDED_HOST, 1) || ""
145
+ origin_host = request.get_header("HTTP_HOST")
146
+ forwarded_host = request.x_forwarded_host&.split(/,\s?/)&.last
110
147
 
111
148
  @permissions.allows?(origin_host) && (forwarded_host.blank? || @permissions.allows?(forwarded_host))
112
149
  end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/notifications"
4
+
5
+ module ActionDispatch
6
+ class ServerTiming
7
+ SERVER_TIMING_HEADER = "Server-Timing"
8
+
9
+ def initialize(app)
10
+ @app = app
11
+ end
12
+
13
+ def call(env)
14
+ events = []
15
+ subscriber = ActiveSupport::Notifications.subscribe(/.*/) do |*args|
16
+ events << ActiveSupport::Notifications::Event.new(*args)
17
+ end
18
+
19
+ status, headers, body = begin
20
+ @app.call(env)
21
+ ensure
22
+ ActiveSupport::Notifications.unsubscribe(subscriber)
23
+ end
24
+
25
+ header_info = events.group_by(&:name).map do |event_name, events_collection|
26
+ "#{event_name};dur=#{events_collection.sum(&:duration)}"
27
+ end
28
+ headers[SERVER_TIMING_HEADER] = header_info.join(", ")
29
+
30
+ [ status, headers, body ]
31
+ end
32
+ end
33
+ end
@@ -40,6 +40,7 @@ module ActionDispatch
40
40
  request.set_header "action_dispatch.exception", wrapper.unwrapped_exception
41
41
  request.set_header "action_dispatch.original_path", request.path_info
42
42
  request.set_header "action_dispatch.original_request_method", request.raw_request_method
43
+ fallback_to_html_format_if_invalid_mime_type(request)
43
44
  request.path_info = "/#{status}"
44
45
  request.request_method = "GET"
45
46
  response = @exceptions_app.call(request.env)
@@ -54,6 +55,15 @@ module ActionDispatch
54
55
  "went wrong."]]
55
56
  end
56
57
 
58
+ def fallback_to_html_format_if_invalid_mime_type(request)
59
+ # If the MIME type for the request is invalid then the
60
+ # @exceptions_app may not be able to handle it. To make it
61
+ # easier to handle, we switch to HTML.
62
+ request.formats
63
+ rescue ActionDispatch::Http::MimeNegotiation::InvalidType
64
+ request.set_header "HTTP_ACCEPT", "text/html"
65
+ end
66
+
57
67
  def pass_response(status)
58
68
  [status, { "Content-Type" => "text/html; charset=#{Response.default_charset}", "Content-Length" => "0" }, []]
59
69
  end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "rack/utils"
4
- require "active_support/core_ext/uri"
5
4
 
6
5
  module ActionDispatch
7
6
  # This middleware serves static files from disk, if available.
@@ -4,4 +4,5 @@
4
4
  <main role="main" id="container">
5
5
  <h2>To allow requests to <%= @host %> make sure it is a valid hostname (containing only numbers, letters, dashes and dots), then add the following to your environment configuration:</h2>
6
6
  <pre>config.hosts &lt;&lt; "<%= @host %>"</pre>
7
+ <p>For more details view: <a href="https://guides.rubyonrails.org/configuring.html#actiondispatch-hostauthorization">the Host Authorization guide</a></p>
7
8
  </main>
@@ -3,3 +3,5 @@ Blocked host: <%= @host %>
3
3
  To allow requests to <%= @host %> make sure it is a valid hostname (containing only numbers, letters, dashes and dots), then add the following to your environment configuration:
4
4
 
5
5
  config.hosts << "<%= @host %>"
6
+
7
+ For more details on host authorization view: https://guides.rubyonrails.org/configuring.html#actiondispatch-hostauthorization
@@ -5,7 +5,7 @@ require "io/console/size"
5
5
 
6
6
  module ActionDispatch
7
7
  module Routing
8
- class RouteWrapper < SimpleDelegator
8
+ class RouteWrapper < SimpleDelegator # :nodoc:
9
9
  def endpoint
10
10
  app.dispatcher? ? "#{controller}##{action}" : rack_app.inspect
11
11
  end