actionpack 5.2.8.1 → 6.0.0.beta1

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 (108) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +109 -482
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +1 -1
  5. data/lib/abstract_controller/base.rb +4 -2
  6. data/lib/abstract_controller/caching/fragments.rb +6 -21
  7. data/lib/abstract_controller/callbacks.rb +12 -0
  8. data/lib/abstract_controller/collector.rb +1 -1
  9. data/lib/abstract_controller/helpers.rb +2 -2
  10. data/lib/abstract_controller/railties/routes_helpers.rb +1 -1
  11. data/lib/action_controller/api.rb +2 -1
  12. data/lib/action_controller/base.rb +2 -7
  13. data/lib/action_controller/caching.rb +1 -1
  14. data/lib/action_controller/log_subscriber.rb +8 -5
  15. data/lib/action_controller/metal/conditional_get.rb +9 -3
  16. data/lib/action_controller/metal/data_streaming.rb +5 -6
  17. data/lib/action_controller/metal/default_headers.rb +17 -0
  18. data/lib/action_controller/metal/exceptions.rb +22 -1
  19. data/lib/action_controller/metal/flash.rb +5 -5
  20. data/lib/action_controller/metal/force_ssl.rb +17 -57
  21. data/lib/action_controller/metal/head.rb +1 -1
  22. data/lib/action_controller/metal/helpers.rb +1 -2
  23. data/lib/action_controller/metal/http_authentication.rb +21 -22
  24. data/lib/action_controller/metal/implicit_render.rb +2 -12
  25. data/lib/action_controller/metal/instrumentation.rb +3 -5
  26. data/lib/action_controller/metal/live.rb +28 -26
  27. data/lib/action_controller/metal/mime_responds.rb +13 -2
  28. data/lib/action_controller/metal/params_wrapper.rb +18 -14
  29. data/lib/action_controller/metal/redirecting.rb +32 -11
  30. data/lib/action_controller/metal/rendering.rb +1 -1
  31. data/lib/action_controller/metal/request_forgery_protection.rb +32 -97
  32. data/lib/action_controller/metal/strong_parameters.rb +57 -34
  33. data/lib/action_controller/metal/url_for.rb +1 -1
  34. data/lib/action_controller/metal.rb +2 -2
  35. data/lib/action_controller/railties/helpers.rb +1 -1
  36. data/lib/action_controller/renderer.rb +15 -2
  37. data/lib/action_controller/test_case.rb +5 -9
  38. data/lib/action_controller.rb +1 -0
  39. data/lib/action_dispatch/http/cache.rb +14 -10
  40. data/lib/action_dispatch/http/content_disposition.rb +45 -0
  41. data/lib/action_dispatch/http/content_security_policy.rb +17 -8
  42. data/lib/action_dispatch/http/filter_parameters.rb +8 -6
  43. data/lib/action_dispatch/http/filter_redirect.rb +1 -1
  44. data/lib/action_dispatch/http/headers.rb +1 -1
  45. data/lib/action_dispatch/http/mime_negotiation.rb +7 -10
  46. data/lib/action_dispatch/http/mime_type.rb +1 -5
  47. data/lib/action_dispatch/http/parameter_filter.rb +5 -79
  48. data/lib/action_dispatch/http/parameters.rb +13 -3
  49. data/lib/action_dispatch/http/request.rb +10 -13
  50. data/lib/action_dispatch/http/response.rb +14 -14
  51. data/lib/action_dispatch/http/upload.rb +5 -0
  52. data/lib/action_dispatch/http/url.rb +81 -81
  53. data/lib/action_dispatch/journey/formatter.rb +1 -1
  54. data/lib/action_dispatch/journey/nfa/simulator.rb +0 -2
  55. data/lib/action_dispatch/journey/nodes/node.rb +9 -8
  56. data/lib/action_dispatch/journey/path/pattern.rb +3 -4
  57. data/lib/action_dispatch/journey/router/utils.rb +10 -10
  58. data/lib/action_dispatch/journey/router.rb +0 -3
  59. data/lib/action_dispatch/journey/scanner.rb +11 -4
  60. data/lib/action_dispatch/journey/visitors.rb +1 -1
  61. data/lib/action_dispatch/middleware/callbacks.rb +2 -4
  62. data/lib/action_dispatch/middleware/cookies.rb +49 -70
  63. data/lib/action_dispatch/middleware/debug_exceptions.rb +32 -58
  64. data/lib/action_dispatch/middleware/debug_locks.rb +5 -5
  65. data/lib/action_dispatch/middleware/debug_view.rb +50 -0
  66. data/lib/action_dispatch/middleware/exception_wrapper.rb +36 -7
  67. data/lib/action_dispatch/middleware/executor.rb +1 -1
  68. data/lib/action_dispatch/middleware/flash.rb +1 -1
  69. data/lib/action_dispatch/middleware/host_authorization.rb +103 -0
  70. data/lib/action_dispatch/middleware/remote_ip.rb +6 -8
  71. data/lib/action_dispatch/middleware/request_id.rb +2 -2
  72. data/lib/action_dispatch/middleware/session/abstract_store.rb +0 -14
  73. data/lib/action_dispatch/middleware/session/cache_store.rb +6 -11
  74. data/lib/action_dispatch/middleware/session/cookie_store.rb +11 -27
  75. data/lib/action_dispatch/middleware/ssl.rb +8 -8
  76. data/lib/action_dispatch/middleware/stack.rb +2 -2
  77. data/lib/action_dispatch/middleware/static.rb +5 -6
  78. data/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +4 -2
  79. data/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +45 -35
  80. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb +7 -0
  81. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.text.erb +5 -0
  82. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +20 -2
  83. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +4 -4
  84. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +2 -2
  85. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb +19 -0
  86. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.text.erb +3 -0
  87. data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +2 -2
  88. data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +1 -1
  89. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +2 -2
  90. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +3 -0
  91. data/lib/action_dispatch/railtie.rb +1 -0
  92. data/lib/action_dispatch/request/session.rb +8 -6
  93. data/lib/action_dispatch/routing/inspector.rb +99 -50
  94. data/lib/action_dispatch/routing/mapper.rb +36 -29
  95. data/lib/action_dispatch/routing/polymorphic_routes.rb +7 -12
  96. data/lib/action_dispatch/routing/route_set.rb +11 -12
  97. data/lib/action_dispatch/routing/url_for.rb +1 -0
  98. data/lib/action_dispatch/routing.rb +3 -2
  99. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +3 -3
  100. data/lib/action_dispatch/testing/assertions/response.rb +2 -3
  101. data/lib/action_dispatch/testing/assertions/routing.rb +7 -2
  102. data/lib/action_dispatch/testing/integration.rb +11 -5
  103. data/lib/action_dispatch/testing/test_process.rb +2 -2
  104. data/lib/action_dispatch/testing/test_response.rb +4 -32
  105. data/lib/action_dispatch.rb +7 -6
  106. data/lib/action_pack/gem_version.rb +4 -4
  107. data/lib/action_pack.rb +1 -1
  108. metadata +25 -23
@@ -24,10 +24,8 @@ module ActionDispatch
24
24
  def call(env)
25
25
  error = nil
26
26
  result = run_callbacks :call do
27
- begin
28
- @app.call(env)
29
- rescue => error
30
- end
27
+ @app.call(env)
28
+ rescue => error
31
29
  end
32
30
  raise error if error
33
31
  result
@@ -9,7 +9,7 @@ require "rack/utils"
9
9
  module ActionDispatch
10
10
  class Request
11
11
  def cookie_jar
12
- fetch_header("action_dispatch.cookies".freeze) do
12
+ fetch_header("action_dispatch.cookies") do
13
13
  self.cookie_jar = Cookies::CookieJar.build(self, cookies)
14
14
  end
15
15
  end
@@ -22,11 +22,11 @@ module ActionDispatch
22
22
  }
23
23
 
24
24
  def have_cookie_jar?
25
- has_header? "action_dispatch.cookies".freeze
25
+ has_header? "action_dispatch.cookies"
26
26
  end
27
27
 
28
28
  def cookie_jar=(jar)
29
- set_header "action_dispatch.cookies".freeze, jar
29
+ set_header "action_dispatch.cookies", jar
30
30
  end
31
31
 
32
32
  def key_generator
@@ -61,10 +61,6 @@ module ActionDispatch
61
61
  get_header Cookies::SIGNED_COOKIE_DIGEST
62
62
  end
63
63
 
64
- def secret_token
65
- get_header Cookies::SECRET_TOKEN
66
- end
67
-
68
64
  def secret_key_base
69
65
  get_header Cookies::SECRET_KEY_BASE
70
66
  end
@@ -81,6 +77,10 @@ module ActionDispatch
81
77
  get_header Cookies::COOKIES_ROTATIONS
82
78
  end
83
79
 
80
+ def use_cookies_with_metadata
81
+ get_header Cookies::USE_COOKIES_WITH_METADATA
82
+ end
83
+
84
84
  # :startdoc:
85
85
  end
86
86
 
@@ -168,20 +168,20 @@ module ActionDispatch
168
168
  # * <tt>:httponly</tt> - Whether this cookie is accessible via scripting or
169
169
  # only HTTP. Defaults to +false+.
170
170
  class Cookies
171
- HTTP_HEADER = "Set-Cookie".freeze
172
- GENERATOR_KEY = "action_dispatch.key_generator".freeze
173
- SIGNED_COOKIE_SALT = "action_dispatch.signed_cookie_salt".freeze
174
- ENCRYPTED_COOKIE_SALT = "action_dispatch.encrypted_cookie_salt".freeze
175
- ENCRYPTED_SIGNED_COOKIE_SALT = "action_dispatch.encrypted_signed_cookie_salt".freeze
176
- AUTHENTICATED_ENCRYPTED_COOKIE_SALT = "action_dispatch.authenticated_encrypted_cookie_salt".freeze
177
- USE_AUTHENTICATED_COOKIE_ENCRYPTION = "action_dispatch.use_authenticated_cookie_encryption".freeze
178
- ENCRYPTED_COOKIE_CIPHER = "action_dispatch.encrypted_cookie_cipher".freeze
179
- SIGNED_COOKIE_DIGEST = "action_dispatch.signed_cookie_digest".freeze
180
- SECRET_TOKEN = "action_dispatch.secret_token".freeze
181
- SECRET_KEY_BASE = "action_dispatch.secret_key_base".freeze
182
- COOKIES_SERIALIZER = "action_dispatch.cookies_serializer".freeze
183
- COOKIES_DIGEST = "action_dispatch.cookies_digest".freeze
184
- COOKIES_ROTATIONS = "action_dispatch.cookies_rotations".freeze
171
+ HTTP_HEADER = "Set-Cookie"
172
+ GENERATOR_KEY = "action_dispatch.key_generator"
173
+ SIGNED_COOKIE_SALT = "action_dispatch.signed_cookie_salt"
174
+ ENCRYPTED_COOKIE_SALT = "action_dispatch.encrypted_cookie_salt"
175
+ ENCRYPTED_SIGNED_COOKIE_SALT = "action_dispatch.encrypted_signed_cookie_salt"
176
+ AUTHENTICATED_ENCRYPTED_COOKIE_SALT = "action_dispatch.authenticated_encrypted_cookie_salt"
177
+ USE_AUTHENTICATED_COOKIE_ENCRYPTION = "action_dispatch.use_authenticated_cookie_encryption"
178
+ ENCRYPTED_COOKIE_CIPHER = "action_dispatch.encrypted_cookie_cipher"
179
+ SIGNED_COOKIE_DIGEST = "action_dispatch.signed_cookie_digest"
180
+ SECRET_KEY_BASE = "action_dispatch.secret_key_base"
181
+ COOKIES_SERIALIZER = "action_dispatch.cookies_serializer"
182
+ COOKIES_DIGEST = "action_dispatch.cookies_digest"
183
+ COOKIES_ROTATIONS = "action_dispatch.cookies_rotations"
184
+ USE_COOKIES_WITH_METADATA = "action_dispatch.use_cookies_with_metadata"
185
185
 
186
186
  # Cookies can typically store 4096 bytes.
187
187
  MAX_COOKIE_SIZE = 4096
@@ -210,9 +210,6 @@ module ActionDispatch
210
210
  # the cookie again. This is useful for creating cookies with values that the user is not supposed to change. If a signed
211
211
  # cookie was tampered with by the user (or a 3rd party), +nil+ will be returned.
212
212
  #
213
- # If +secret_key_base+ and +secrets.secret_token+ (deprecated) are both set,
214
- # legacy cookies signed with the old key generator will be transparently upgraded.
215
- #
216
213
  # This jar requires that you set a suitable secret for the verification on your app's +secret_key_base+.
217
214
  #
218
215
  # Example:
@@ -228,9 +225,6 @@ module ActionDispatch
228
225
  # Returns a jar that'll automatically encrypt cookie values before sending them to the client and will decrypt them for read.
229
226
  # If the cookie was tampered with by the user (or a 3rd party), +nil+ will be returned.
230
227
  #
231
- # If +secret_key_base+ and +secrets.secret_token+ (deprecated) are both set,
232
- # legacy cookies signed with the old key generator will be transparently upgraded.
233
- #
234
228
  # If +config.action_dispatch.encrypted_cookie_salt+ and +config.action_dispatch.encrypted_signed_cookie_salt+
235
229
  # are both set, legacy cookies encrypted with HMAC AES-256-CBC will be transparently upgraded.
236
230
  #
@@ -259,10 +253,6 @@ module ActionDispatch
259
253
 
260
254
  private
261
255
 
262
- def upgrade_legacy_signed_cookies?
263
- request.secret_token.present? && request.secret_key_base.present?
264
- end
265
-
266
256
  def upgrade_legacy_hmac_aes_cbc_cookies?
267
257
  request.secret_key_base.present? &&
268
258
  request.encrypted_signed_cookie_salt.present? &&
@@ -470,7 +460,7 @@ module ActionDispatch
470
460
 
471
461
  def [](name)
472
462
  if data = @parent_jar[name.to_s]
473
- parse name, data
463
+ parse(name, data, purpose: "cookie.#{name}") || parse(name, data)
474
464
  end
475
465
  end
476
466
 
@@ -481,7 +471,7 @@ module ActionDispatch
481
471
  options = { value: options }
482
472
  end
483
473
 
484
- commit(options)
474
+ commit(name, options)
485
475
  @parent_jar[name] = options
486
476
  end
487
477
 
@@ -490,24 +480,31 @@ module ActionDispatch
490
480
 
491
481
  private
492
482
  def expiry_options(options)
493
- if request.use_authenticated_cookie_encryption
494
- if options[:expires].respond_to?(:from_now)
495
- { expires_in: options[:expires] }
496
- else
497
- { expires_at: options[:expires] }
498
- end
483
+ if options[:expires].respond_to?(:from_now)
484
+ { expires_in: options[:expires] }
485
+ else
486
+ { expires_at: options[:expires] }
487
+ end
488
+ end
489
+
490
+ def cookie_metadata(name, options)
491
+ if request.use_cookies_with_metadata
492
+ metadata = expiry_options(options)
493
+ metadata[:purpose] = "cookie.#{name}"
494
+
495
+ metadata
499
496
  else
500
497
  {}
501
498
  end
502
499
  end
503
500
 
504
- def parse(name, data); data; end
505
- def commit(options); end
501
+ def parse(name, data, purpose: nil); data; end
502
+ def commit(name, options); end
506
503
  end
507
504
 
508
505
  class PermanentCookieJar < AbstractCookieJar # :nodoc:
509
506
  private
510
- def commit(options)
507
+ def commit(name, options)
511
508
  options[:expires] = 20.years.from_now
512
509
  end
513
510
  end
@@ -523,7 +520,7 @@ module ActionDispatch
523
520
  end
524
521
 
525
522
  module SerializedCookieJars # :nodoc:
526
- MARSHAL_SIGNATURE = "\x04\x08".freeze
523
+ MARSHAL_SIGNATURE = "\x04\x08"
527
524
  SERIALIZER = ActiveSupport::MessageEncryptor::NullSerializer
528
525
 
529
526
  protected
@@ -580,21 +577,17 @@ module ActionDispatch
580
577
  request.cookies_rotations.signed.each do |*secrets, **options|
581
578
  @verifier.rotate(*secrets, serializer: SERIALIZER, **options)
582
579
  end
583
-
584
- if upgrade_legacy_signed_cookies?
585
- @verifier.rotate request.secret_token, serializer: SERIALIZER
586
- end
587
580
  end
588
581
 
589
582
  private
590
- def parse(name, signed_message)
583
+ def parse(name, signed_message, purpose: nil)
591
584
  deserialize(name) do |rotate|
592
- @verifier.verified(signed_message, on_rotation: rotate)
585
+ @verifier.verified(signed_message, on_rotation: rotate, purpose: purpose)
593
586
  end
594
587
  end
595
588
 
596
- def commit(options)
597
- options[:value] = @verifier.generate(serialize(options[:value]), expiry_options(options))
589
+ def commit(name, options)
590
+ options[:value] = @verifier.generate(serialize(options[:value]), cookie_metadata(name, options))
598
591
 
599
592
  raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE
600
593
  end
@@ -628,36 +621,22 @@ module ActionDispatch
628
621
 
629
622
  @encryptor.rotate(secret, sign_secret, cipher: legacy_cipher, digest: digest, serializer: SERIALIZER)
630
623
  end
631
-
632
- if upgrade_legacy_signed_cookies?
633
- @legacy_verifier = ActiveSupport::MessageVerifier.new(request.secret_token, digest: digest, serializer: SERIALIZER)
634
- end
635
624
  end
636
625
 
637
626
  private
638
- def parse(name, encrypted_message)
627
+ def parse(name, encrypted_message, purpose: nil)
639
628
  deserialize(name) do |rotate|
640
- @encryptor.decrypt_and_verify(encrypted_message, on_rotation: rotate)
629
+ @encryptor.decrypt_and_verify(encrypted_message, on_rotation: rotate, purpose: purpose)
641
630
  end
642
631
  rescue ActiveSupport::MessageEncryptor::InvalidMessage, ActiveSupport::MessageVerifier::InvalidSignature
643
- parse_legacy_signed_message(name, encrypted_message)
632
+ nil
644
633
  end
645
634
 
646
- def commit(options)
647
- options[:value] = @encryptor.encrypt_and_sign(serialize(options[:value]), expiry_options(options))
635
+ def commit(name, options)
636
+ options[:value] = @encryptor.encrypt_and_sign(serialize(options[:value]), cookie_metadata(name, options))
648
637
 
649
638
  raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE
650
639
  end
651
-
652
- def parse_legacy_signed_message(name, legacy_signed_message)
653
- if defined?(@legacy_verifier)
654
- deserialize(name) do |rotate|
655
- rotate.call
656
-
657
- @legacy_verifier.verified(legacy_signed_message)
658
- end
659
- end
660
- end
661
640
  end
662
641
 
663
642
  def initialize(app)
@@ -3,57 +3,26 @@
3
3
  require "action_dispatch/http/request"
4
4
  require "action_dispatch/middleware/exception_wrapper"
5
5
  require "action_dispatch/routing/inspector"
6
+
6
7
  require "action_view"
7
8
  require "action_view/base"
8
9
 
9
- require "pp"
10
-
11
10
  module ActionDispatch
12
11
  # This middleware is responsible for logging exceptions and
13
12
  # showing a debugging page in case the request is local.
14
13
  class DebugExceptions
15
- RESCUES_TEMPLATE_PATH = File.expand_path("templates", __dir__)
16
-
17
- class DebugView < ActionView::Base
18
- def debug_params(params)
19
- clean_params = params.clone
20
- clean_params.delete("action")
21
- clean_params.delete("controller")
22
-
23
- if clean_params.empty?
24
- "None"
25
- else
26
- PP.pp(clean_params, "".dup, 200)
27
- end
28
- end
29
-
30
- def debug_headers(headers)
31
- if headers.present?
32
- headers.inspect.gsub(",", ",\n")
33
- else
34
- "None"
35
- end
36
- end
37
-
38
- def debug_hash(object)
39
- object.to_hash.sort_by { |k, _| k.to_s }.map { |k, v| "#{k}: #{v.inspect rescue $!.message}" }.join("\n")
40
- end
41
-
42
- def render(*)
43
- logger = ActionView::Base.logger
14
+ cattr_reader :interceptors, instance_accessor: false, default: []
44
15
 
45
- if logger && logger.respond_to?(:silence)
46
- logger.silence { super }
47
- else
48
- super
49
- end
50
- end
16
+ def self.register_interceptor(object = nil, &block)
17
+ interceptor = object || block
18
+ interceptors << interceptor
51
19
  end
52
20
 
53
- def initialize(app, routes_app = nil, response_format = :default)
21
+ def initialize(app, routes_app = nil, response_format = :default, interceptors = self.class.interceptors)
54
22
  @app = app
55
23
  @routes_app = routes_app
56
24
  @response_format = response_format
25
+ @interceptors = interceptors
57
26
  end
58
27
 
59
28
  def call(env)
@@ -67,12 +36,24 @@ module ActionDispatch
67
36
 
68
37
  response
69
38
  rescue Exception => exception
39
+ invoke_interceptors(request, exception)
70
40
  raise exception unless request.show_exceptions?
71
41
  render_exception(request, exception)
72
42
  end
73
43
 
74
44
  private
75
45
 
46
+ def invoke_interceptors(request, exception)
47
+ backtrace_cleaner = request.get_header("action_dispatch.backtrace_cleaner")
48
+ wrapper = ExceptionWrapper.new(backtrace_cleaner, exception)
49
+
50
+ @interceptors.each do |interceptor|
51
+ interceptor.call(request, exception)
52
+ rescue Exception
53
+ log_error(request, wrapper)
54
+ end
55
+ end
56
+
76
57
  def render_exception(request, exception)
77
58
  backtrace_cleaner = request.get_header("action_dispatch.backtrace_cleaner")
78
59
  wrapper = ExceptionWrapper.new(backtrace_cleaner, exception)
@@ -130,23 +111,13 @@ module ActionDispatch
130
111
  end
131
112
 
132
113
  def create_template(request, wrapper)
133
- traces = wrapper.traces
134
-
135
- trace_to_show = "Application Trace"
136
- if traces[trace_to_show].empty? && wrapper.rescue_template != "routing_error"
137
- trace_to_show = "Full Trace"
138
- end
139
-
140
- if source_to_show = traces[trace_to_show].first
141
- source_to_show_id = source_to_show[:id]
142
- end
143
-
144
- DebugView.new([RESCUES_TEMPLATE_PATH],
114
+ DebugView.new(
145
115
  request: request,
116
+ exception_wrapper: wrapper,
146
117
  exception: wrapper.exception,
147
- traces: traces,
148
- show_source_idx: source_to_show_id,
149
- trace_to_show: trace_to_show,
118
+ traces: wrapper.traces,
119
+ show_source_idx: wrapper.source_to_show_id,
120
+ trace_to_show: wrapper.trace_to_show,
150
121
  routes_inspector: routes_inspector(wrapper.exception),
151
122
  source_extracts: wrapper.source_extracts,
152
123
  line_number: wrapper.line_number,
@@ -168,11 +139,14 @@ module ActionDispatch
168
139
  trace = wrapper.framework_trace if trace.empty?
169
140
 
170
141
  ActiveSupport::Deprecation.silence do
171
- logger.fatal " "
172
- logger.fatal "#{exception.class} (#{exception.message}):"
173
- log_array logger, exception.annoted_source_code if exception.respond_to?(:annoted_source_code)
174
- logger.fatal " "
175
- log_array logger, trace
142
+ message = []
143
+ message << " "
144
+ message << "#{exception.class} (#{exception.message}):"
145
+ message.concat(exception.annoted_source_code) if exception.respond_to?(:annoted_source_code)
146
+ message << " "
147
+ message.concat(trace)
148
+
149
+ log_array(logger, message)
176
150
  end
177
151
  end
178
152
 
@@ -32,7 +32,7 @@ module ActionDispatch
32
32
  req = ActionDispatch::Request.new env
33
33
 
34
34
  if req.get?
35
- path = req.path_info.chomp("/".freeze)
35
+ path = req.path_info.chomp("/")
36
36
  if path == @path
37
37
  return render_details(req)
38
38
  end
@@ -63,19 +63,19 @@ module ActionDispatch
63
63
 
64
64
  str = threads.map do |thread, info|
65
65
  if info[:exclusive]
66
- lock_state = "Exclusive".dup
66
+ lock_state = +"Exclusive"
67
67
  elsif info[:sharing] > 0
68
- lock_state = "Sharing".dup
68
+ lock_state = +"Sharing"
69
69
  lock_state << " x#{info[:sharing]}" if info[:sharing] > 1
70
70
  else
71
- lock_state = "No lock".dup
71
+ lock_state = +"No lock"
72
72
  end
73
73
 
74
74
  if info[:waiting]
75
75
  lock_state << " (yielded share)"
76
76
  end
77
77
 
78
- msg = "Thread #{info[:index]} [0x#{thread.__id__.to_s(16)} #{thread.status || 'dead'}] #{lock_state}\n".dup
78
+ msg = +"Thread #{info[:index]} [0x#{thread.__id__.to_s(16)} #{thread.status || 'dead'}] #{lock_state}\n"
79
79
 
80
80
  if info[:sleeper]
81
81
  msg << " Waiting in #{info[:sleeper]}"
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pp"
4
+
5
+ require "action_view"
6
+ require "action_view/base"
7
+
8
+ module ActionDispatch
9
+ class DebugView < ActionView::Base # :nodoc:
10
+ RESCUES_TEMPLATE_PATH = File.expand_path("templates", __dir__)
11
+
12
+ def initialize(assigns)
13
+ super([RESCUES_TEMPLATE_PATH], assigns)
14
+ end
15
+
16
+ def debug_params(params)
17
+ clean_params = params.clone
18
+ clean_params.delete("action")
19
+ clean_params.delete("controller")
20
+
21
+ if clean_params.empty?
22
+ "None"
23
+ else
24
+ PP.pp(clean_params, +"", 200)
25
+ end
26
+ end
27
+
28
+ def debug_headers(headers)
29
+ if headers.present?
30
+ headers.inspect.gsub(",", ",\n")
31
+ else
32
+ "None"
33
+ end
34
+ end
35
+
36
+ def debug_hash(object)
37
+ object.to_hash.sort_by { |k, _| k.to_s }.map { |k, v| "#{k}: #{v.inspect rescue $!.message}" }.join("\n")
38
+ end
39
+
40
+ def render(*)
41
+ logger = ActionView::Base.logger
42
+
43
+ if logger && logger.respond_to?(:silence)
44
+ logger.silence { super }
45
+ else
46
+ super
47
+ end
48
+ end
49
+ end
50
+ end
@@ -12,6 +12,7 @@ module ActionDispatch
12
12
  "ActionController::UnknownHttpMethod" => :method_not_allowed,
13
13
  "ActionController::NotImplemented" => :not_implemented,
14
14
  "ActionController::UnknownFormat" => :not_acceptable,
15
+ "ActionController::MissingExactTemplate" => :not_acceptable,
15
16
  "ActionController::InvalidAuthenticityToken" => :unprocessable_entity,
16
17
  "ActionController::InvalidCrossOriginRequest" => :unprocessable_entity,
17
18
  "ActionDispatch::Http::Parameters::ParseError" => :bad_request,
@@ -22,18 +23,20 @@ module ActionDispatch
22
23
  )
23
24
 
24
25
  cattr_accessor :rescue_templates, default: Hash.new("diagnostics").merge!(
25
- "ActionView::MissingTemplate" => "missing_template",
26
- "ActionController::RoutingError" => "routing_error",
27
- "AbstractController::ActionNotFound" => "unknown_action",
28
- "ActiveRecord::StatementInvalid" => "invalid_statement",
29
- "ActionView::Template::Error" => "template_error"
26
+ "ActionView::MissingTemplate" => "missing_template",
27
+ "ActionController::RoutingError" => "routing_error",
28
+ "AbstractController::ActionNotFound" => "unknown_action",
29
+ "ActiveRecord::StatementInvalid" => "invalid_statement",
30
+ "ActionView::Template::Error" => "template_error",
31
+ "ActionController::MissingExactTemplate" => "missing_exact_template",
30
32
  )
31
33
 
32
- attr_reader :backtrace_cleaner, :exception, :line_number, :file
34
+ attr_reader :backtrace_cleaner, :exception, :wrapped_causes, :line_number, :file
33
35
 
34
36
  def initialize(backtrace_cleaner, exception)
35
37
  @backtrace_cleaner = backtrace_cleaner
36
38
  @exception = original_exception(exception)
39
+ @wrapped_causes = wrapped_causes_for(exception, backtrace_cleaner)
37
40
 
38
41
  expand_backtrace if exception.is_a?(SyntaxError) || exception.cause.is_a?(SyntaxError)
39
42
  end
@@ -64,7 +67,11 @@ module ActionDispatch
64
67
  full_trace_with_ids = []
65
68
 
66
69
  full_trace.each_with_index do |trace, idx|
67
- trace_with_id = { id: idx, trace: trace }
70
+ trace_with_id = {
71
+ exception_object_id: @exception.object_id,
72
+ id: idx,
73
+ trace: trace
74
+ }
68
75
 
69
76
  if application_trace.include?(trace)
70
77
  application_trace_with_ids << trace_with_id
@@ -97,6 +104,18 @@ module ActionDispatch
97
104
  end
98
105
  end
99
106
 
107
+ def trace_to_show
108
+ if traces["Application Trace"].empty? && rescue_template != "routing_error"
109
+ "Full Trace"
110
+ else
111
+ "Application Trace"
112
+ end
113
+ end
114
+
115
+ def source_to_show_id
116
+ (traces[trace_to_show].first || {})[:id]
117
+ end
118
+
100
119
  private
101
120
 
102
121
  def backtrace
@@ -111,6 +130,16 @@ module ActionDispatch
111
130
  end
112
131
  end
113
132
 
133
+ def causes_for(exception)
134
+ return enum_for(__method__, exception) unless block_given?
135
+
136
+ yield exception while exception = exception.cause
137
+ end
138
+
139
+ def wrapped_causes_for(exception, backtrace_cleaner)
140
+ causes_for(exception).map { |cause| self.class.new(backtrace_cleaner, cause) }
141
+ end
142
+
114
143
  def clean_backtrace(*args)
115
144
  if backtrace_cleaner
116
145
  backtrace_cleaner.clean(backtrace, *args)
@@ -9,7 +9,7 @@ module ActionDispatch
9
9
  end
10
10
 
11
11
  def call(env)
12
- state = @executor.run!(reset: true)
12
+ state = @executor.run!
13
13
  begin
14
14
  response = @app.call(env)
15
15
  returned = response << ::Rack::BodyProxy.new(response.pop) { state.complete! }
@@ -38,7 +38,7 @@ module ActionDispatch
38
38
  #
39
39
  # See docs on the FlashHash class for more details about the flash.
40
40
  class Flash
41
- KEY = "action_dispatch.request.flash_hash".freeze
41
+ KEY = "action_dispatch.request.flash_hash"
42
42
 
43
43
  module RequestMethods
44
44
  # Access the contents of the flash. Use <tt>flash["notice"]</tt> to
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "action_dispatch/http/request"
4
+
5
+ module ActionDispatch
6
+ # This middleware guards from DNS rebinding attacks by white-listing the
7
+ # hosts a request can be sent to.
8
+ #
9
+ # When a request comes to an unauthorized host, the +response_app+
10
+ # application will be executed and rendered. If no +response_app+ is given, a
11
+ # default one will run, which responds with +403 Forbidden+.
12
+ class HostAuthorization
13
+ class Permissions # :nodoc:
14
+ def initialize(hosts)
15
+ @hosts = sanitize_hosts(hosts)
16
+ end
17
+
18
+ def empty?
19
+ @hosts.empty?
20
+ end
21
+
22
+ def allows?(host)
23
+ @hosts.any? do |allowed|
24
+ allowed === host
25
+ rescue
26
+ # IPAddr#=== raises an error if you give it a hostname instead of
27
+ # IP. Treat similar errors as blocked access.
28
+ false
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def sanitize_hosts(hosts)
35
+ Array(hosts).map do |host|
36
+ case host
37
+ when Regexp then sanitize_regexp(host)
38
+ when String then sanitize_string(host)
39
+ else host
40
+ end
41
+ end
42
+ end
43
+
44
+ def sanitize_regexp(host)
45
+ /\A#{host}\z/
46
+ end
47
+
48
+ def sanitize_string(host)
49
+ if host.start_with?(".")
50
+ /\A(.+\.)?#{Regexp.escape(host[1..-1])}\z/
51
+ else
52
+ host
53
+ end
54
+ end
55
+ end
56
+
57
+ DEFAULT_RESPONSE_APP = -> env do
58
+ request = Request.new(env)
59
+
60
+ format = request.xhr? ? "text/plain" : "text/html"
61
+ template = DebugView.new(host: request.host)
62
+ body = template.render(template: "rescues/blocked_host", layout: "rescues/layout")
63
+
64
+ [403, {
65
+ "Content-Type" => "#{format}; charset=#{Response.default_charset}",
66
+ "Content-Length" => body.bytesize.to_s,
67
+ }, [body]]
68
+ end
69
+
70
+ def initialize(app, hosts, response_app = nil)
71
+ @app = app
72
+ @permissions = Permissions.new(hosts)
73
+ @response_app = response_app || DEFAULT_RESPONSE_APP
74
+ end
75
+
76
+ def call(env)
77
+ return @app.call(env) if @permissions.empty?
78
+
79
+ request = Request.new(env)
80
+
81
+ if authorized?(request)
82
+ mark_as_authorized(request)
83
+ @app.call(env)
84
+ else
85
+ @response_app.call(env)
86
+ end
87
+ end
88
+
89
+ private
90
+
91
+ def authorized?(request)
92
+ origin_host = request.get_header("HTTP_HOST").to_s.sub(/:\d+\z/, "")
93
+ forwarded_host = request.x_forwarded_host.to_s.split(/,\s?/).last.to_s.sub(/:\d+\z/, "")
94
+
95
+ @permissions.allows?(origin_host) &&
96
+ (forwarded_host.blank? || @permissions.allows?(forwarded_host))
97
+ end
98
+
99
+ def mark_as_authorized(request)
100
+ request.set_header("action_dispatch.authorized_host", request.host)
101
+ end
102
+ end
103
+ end