actionpack 7.2.2.1 → 8.1.2

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.
Files changed (112) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +408 -95
  3. data/README.rdoc +1 -1
  4. data/lib/abstract_controller/asset_paths.rb +4 -2
  5. data/lib/abstract_controller/base.rb +12 -17
  6. data/lib/abstract_controller/caching.rb +6 -3
  7. data/lib/abstract_controller/callbacks.rb +6 -0
  8. data/lib/abstract_controller/collector.rb +1 -1
  9. data/lib/abstract_controller/helpers.rb +1 -1
  10. data/lib/abstract_controller/logger.rb +2 -1
  11. data/lib/abstract_controller/rendering.rb +0 -1
  12. data/lib/action_controller/api.rb +1 -0
  13. data/lib/action_controller/base.rb +3 -2
  14. data/lib/action_controller/caching.rb +1 -2
  15. data/lib/action_controller/form_builder.rb +4 -4
  16. data/lib/action_controller/log_subscriber.rb +22 -3
  17. data/lib/action_controller/metal/allow_browser.rb +12 -2
  18. data/lib/action_controller/metal/conditional_get.rb +30 -1
  19. data/lib/action_controller/metal/data_streaming.rb +5 -5
  20. data/lib/action_controller/metal/exceptions.rb +5 -0
  21. data/lib/action_controller/metal/flash.rb +1 -4
  22. data/lib/action_controller/metal/head.rb +3 -1
  23. data/lib/action_controller/metal/instrumentation.rb +1 -2
  24. data/lib/action_controller/metal/live.rb +66 -26
  25. data/lib/action_controller/metal/params_wrapper.rb +3 -3
  26. data/lib/action_controller/metal/permissions_policy.rb +9 -0
  27. data/lib/action_controller/metal/rate_limiting.rb +39 -9
  28. data/lib/action_controller/metal/redirecting.rb +109 -16
  29. data/lib/action_controller/metal/renderers.rb +29 -9
  30. data/lib/action_controller/metal/rendering.rb +8 -2
  31. data/lib/action_controller/metal/request_forgery_protection.rb +21 -11
  32. data/lib/action_controller/metal/rescue.rb +9 -0
  33. data/lib/action_controller/metal/streaming.rb +5 -84
  34. data/lib/action_controller/metal/strong_parameters.rb +277 -92
  35. data/lib/action_controller/railtie.rb +33 -15
  36. data/lib/action_controller/renderer.rb +0 -1
  37. data/lib/action_controller/structured_event_subscriber.rb +116 -0
  38. data/lib/action_controller/test_case.rb +12 -2
  39. data/lib/action_dispatch/constants.rb +6 -0
  40. data/lib/action_dispatch/http/cache.rb +138 -11
  41. data/lib/action_dispatch/http/content_security_policy.rb +14 -1
  42. data/lib/action_dispatch/http/filter_parameters.rb +5 -3
  43. data/lib/action_dispatch/http/mime_negotiation.rb +63 -4
  44. data/lib/action_dispatch/http/mime_types.rb +1 -0
  45. data/lib/action_dispatch/http/param_builder.rb +187 -0
  46. data/lib/action_dispatch/http/param_error.rb +26 -0
  47. data/lib/action_dispatch/http/parameters.rb +3 -3
  48. data/lib/action_dispatch/http/permissions_policy.rb +6 -0
  49. data/lib/action_dispatch/http/query_parser.rb +55 -0
  50. data/lib/action_dispatch/http/request.rb +73 -23
  51. data/lib/action_dispatch/http/response.rb +65 -17
  52. data/lib/action_dispatch/http/url.rb +112 -16
  53. data/lib/action_dispatch/journey/formatter.rb +8 -3
  54. data/lib/action_dispatch/journey/gtg/simulator.rb +33 -12
  55. data/lib/action_dispatch/journey/gtg/transition_table.rb +37 -45
  56. data/lib/action_dispatch/journey/nodes/node.rb +2 -1
  57. data/lib/action_dispatch/journey/parser.rb +99 -196
  58. data/lib/action_dispatch/journey/route.rb +45 -31
  59. data/lib/action_dispatch/journey/router/utils.rb +8 -14
  60. data/lib/action_dispatch/journey/router.rb +59 -81
  61. data/lib/action_dispatch/journey/routes.rb +7 -0
  62. data/lib/action_dispatch/journey/scanner.rb +44 -42
  63. data/lib/action_dispatch/journey/visitors.rb +55 -23
  64. data/lib/action_dispatch/journey/visualizer/fsm.js +4 -6
  65. data/lib/action_dispatch/log_subscriber.rb +7 -3
  66. data/lib/action_dispatch/middleware/cookies.rb +8 -4
  67. data/lib/action_dispatch/middleware/debug_exceptions.rb +26 -5
  68. data/lib/action_dispatch/middleware/debug_view.rb +11 -5
  69. data/lib/action_dispatch/middleware/exception_wrapper.rb +14 -14
  70. data/lib/action_dispatch/middleware/executor.rb +17 -4
  71. data/lib/action_dispatch/middleware/public_exceptions.rb +6 -6
  72. data/lib/action_dispatch/middleware/remote_ip.rb +11 -5
  73. data/lib/action_dispatch/middleware/request_id.rb +2 -1
  74. data/lib/action_dispatch/middleware/session/cache_store.rb +17 -0
  75. data/lib/action_dispatch/middleware/ssl.rb +13 -3
  76. data/lib/action_dispatch/middleware/templates/rescues/_copy_button.html.erb +1 -0
  77. data/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +3 -5
  78. data/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +9 -5
  79. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb +1 -0
  80. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +1 -0
  81. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +4 -0
  82. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +3 -0
  83. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +50 -0
  84. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb +1 -0
  85. data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +1 -0
  86. data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +1 -0
  87. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +1 -0
  88. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +1 -0
  89. data/lib/action_dispatch/railtie.rb +21 -0
  90. data/lib/action_dispatch/request/session.rb +1 -0
  91. data/lib/action_dispatch/request/utils.rb +9 -3
  92. data/lib/action_dispatch/routing/inspector.rb +80 -57
  93. data/lib/action_dispatch/routing/mapper.rb +409 -228
  94. data/lib/action_dispatch/routing/polymorphic_routes.rb +2 -2
  95. data/lib/action_dispatch/routing/redirection.rb +10 -7
  96. data/lib/action_dispatch/routing/route_set.rb +21 -12
  97. data/lib/action_dispatch/routing/routes_proxy.rb +1 -0
  98. data/lib/action_dispatch/structured_event_subscriber.rb +20 -0
  99. data/lib/action_dispatch/system_test_case.rb +3 -3
  100. data/lib/action_dispatch/system_testing/browser.rb +12 -21
  101. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +2 -2
  102. data/lib/action_dispatch/testing/assertion_response.rb +1 -1
  103. data/lib/action_dispatch/testing/assertions/response.rb +26 -2
  104. data/lib/action_dispatch/testing/assertions/routing.rb +27 -15
  105. data/lib/action_dispatch/testing/integration.rb +16 -7
  106. data/lib/action_dispatch/testing/request_encoder.rb +9 -9
  107. data/lib/action_dispatch/testing/test_process.rb +1 -2
  108. data/lib/action_dispatch.rb +14 -4
  109. data/lib/action_pack/gem_version.rb +3 -3
  110. metadata +19 -38
  111. data/lib/action_dispatch/journey/parser.y +0 -50
  112. data/lib/action_dispatch/journey/parser_extras.rb +0 -33
@@ -7,66 +7,68 @@ require "strscan"
7
7
  module ActionDispatch
8
8
  module Journey # :nodoc:
9
9
  class Scanner # :nodoc:
10
+ STATIC_TOKENS = Array.new(150)
11
+ STATIC_TOKENS[".".ord] = :DOT
12
+ STATIC_TOKENS["/".ord] = :SLASH
13
+ STATIC_TOKENS["(".ord] = :LPAREN
14
+ STATIC_TOKENS[")".ord] = :RPAREN
15
+ STATIC_TOKENS["|".ord] = :OR
16
+ STATIC_TOKENS[":".ord] = :SYMBOL
17
+ STATIC_TOKENS["*".ord] = :STAR
18
+ STATIC_TOKENS.freeze
19
+
20
+ class Scanner < StringScanner
21
+ unless method_defined?(:peek_byte) # https://github.com/ruby/strscan/pull/89
22
+ def peek_byte
23
+ string.getbyte(pos)
24
+ end
25
+ end
26
+ end
27
+
10
28
  def initialize
11
- @ss = nil
29
+ @scanner = nil
30
+ @length = nil
12
31
  end
13
32
 
14
33
  def scan_setup(str)
15
- @ss = StringScanner.new(str)
34
+ @scanner = Scanner.new(str)
16
35
  end
17
36
 
18
- def eos?
19
- @ss.eos?
20
- end
37
+ def next_token
38
+ return if @scanner.eos?
21
39
 
22
- def pos
23
- @ss.pos
40
+ until token = scan || @scanner.eos?; end
41
+ token
24
42
  end
25
43
 
26
- def pre_match
27
- @ss.pre_match
44
+ def last_string
45
+ -@scanner.string.byteslice(@scanner.pos - @length, @length)
28
46
  end
29
47
 
30
- def next_token
31
- return if @ss.eos?
32
-
33
- until token = scan || @ss.eos?; end
34
- token
48
+ def last_literal
49
+ last_str = @scanner.string.byteslice(@scanner.pos - @length, @length)
50
+ last_str.tr! "\\", ""
51
+ -last_str
35
52
  end
36
53
 
37
54
  private
38
- # takes advantage of String @- deduping capabilities in Ruby 2.5 upwards see:
39
- # https://bugs.ruby-lang.org/issues/13077
40
- def dedup_scan(regex)
41
- r = @ss.scan(regex)
42
- r ? -r : nil
43
- end
44
-
45
55
  def scan
56
+ next_byte = @scanner.peek_byte
46
57
  case
47
- # /
48
- when @ss.skip(/\//)
49
- [:SLASH, "/"]
50
- when @ss.skip(/\(/)
51
- [:LPAREN, "("]
52
- when @ss.skip(/\)/)
53
- [:RPAREN, ")"]
54
- when @ss.skip(/\|/)
55
- [:OR, "|"]
56
- when @ss.skip(/\./)
57
- [:DOT, "."]
58
- when text = dedup_scan(/:\w+/)
59
- [:SYMBOL, text]
60
- when text = dedup_scan(/\*\w+/)
61
- [:STAR, text]
62
- when text = @ss.scan(/(?:[\w%\-~!$&'*+,;=@]|\\[:()])+/)
63
- text.tr! "\\", ""
64
- [:LITERAL, -text]
65
- # any char
66
- when text = dedup_scan(/./)
67
- [:LITERAL, text]
58
+ when (token = STATIC_TOKENS[next_byte]) && (token != :SYMBOL || next_byte_is_not_a_token?)
59
+ @scanner.pos += 1
60
+ @length = @scanner.skip(/\w+/).to_i + 1 if token == :SYMBOL || token == :STAR
61
+ token
62
+ when @length = @scanner.skip(/(?:[\w%\-~!$&'*+,;=@]|\\[:()])+/)
63
+ :LITERAL
64
+ when @length = @scanner.skip(/./)
65
+ :LITERAL
68
66
  end
69
67
  end
68
+
69
+ def next_byte_is_not_a_token?
70
+ !STATIC_TOKENS[@scanner.string.getbyte(@scanner.pos + 1)]
71
+ end
70
72
  end
71
73
  end
72
74
  end
@@ -128,8 +128,8 @@ module ActionDispatch
128
128
  def visit_DOT(n, seed); terminal(n, seed); end
129
129
 
130
130
  instance_methods(false).each do |pim|
131
- next unless pim =~ /^visit_(.*)$/
132
- DISPATCH_CACHE[$1.to_sym] = pim
131
+ next unless pim.start_with?("visit_")
132
+ DISPATCH_CACHE[pim.name.delete_prefix("visit_").to_sym] = pim
133
133
  end
134
134
  end
135
135
 
@@ -167,32 +167,64 @@ module ActionDispatch
167
167
  INSTANCE = new
168
168
  end
169
169
 
170
- class String < FunctionalVisitor # :nodoc:
171
- private
172
- def binary(node, seed)
173
- visit(node.right, visit(node.left, seed))
174
- end
175
-
176
- def nary(node, seed)
170
+ class String # :nodoc:
171
+ def accept(node, seed)
172
+ case node.type
173
+ when :DOT
174
+ seed << node.left
175
+ when :LITERAL
176
+ seed << node.left
177
+ when :SYMBOL
178
+ seed << node.left
179
+ when :SLASH
180
+ seed << node.left
181
+ when :CAT
182
+ accept(node.right, accept(node.left, seed))
183
+ when :STAR
184
+ accept(node.left, seed)
185
+ when :OR
177
186
  last_child = node.children.last
178
- node.children.inject(seed) { |s, c|
179
- string = visit(c, s)
180
- string << "|" unless last_child == c
181
- string
182
- }
183
- end
184
-
185
- def terminal(node, seed)
186
- seed + node.left
187
- end
188
-
189
- def visit_GROUP(node, seed)
190
- visit(node.left, seed.dup << "(") << ")"
187
+ node.children.each do |c|
188
+ accept(c, seed)
189
+ seed << "|" unless last_child == c
190
+ end
191
+ seed
192
+ when :GROUP
193
+ accept(node.left, seed << "(") << ")"
194
+ else
195
+ raise "Unknown node type: #{node.type}"
191
196
  end
197
+ end
192
198
 
193
- INSTANCE = new
199
+ INSTANCE = new
194
200
  end
195
201
 
202
+ # class String < FunctionalVisitor # :nodoc:
203
+ # private
204
+ # def binary(node, seed)
205
+ # visit(node.right, visit(node.left, seed))
206
+ # end
207
+ #
208
+ # def nary(node, seed)
209
+ # last_child = node.children.last
210
+ # node.children.inject(seed) { |s, c|
211
+ # string = visit(c, s)
212
+ # string << "|" unless last_child == c
213
+ # string
214
+ # }
215
+ # end
216
+ #
217
+ # def terminal(node, seed)
218
+ # seed + node.left
219
+ # end
220
+ #
221
+ # def visit_GROUP(node, seed)
222
+ # visit(node.left, seed.dup << "(") << ")"
223
+ # end
224
+ #
225
+ # INSTANCE = new
226
+ # end
227
+
196
228
  class Dot < FunctionalVisitor # :nodoc:
197
229
  def initialize
198
230
  @nodes = []
@@ -105,12 +105,10 @@ function match(input) {
105
105
  }
106
106
 
107
107
  if(stdparam_states[state] && default_re.test(token)) {
108
- for(var key in stdparam_states[state]) {
109
- var new_state = stdparam_states[state][key];
110
- highlight_edge(state, new_state);
111
- highlight_state(new_state);
112
- new_states.push([new_state, null]);
113
- }
108
+ var new_state = stdparam_states[state];
109
+ highlight_edge(state, new_state);
110
+ highlight_state(new_state);
111
+ new_states.push([new_state, null]);
114
112
  }
115
113
  }
116
114
 
@@ -1,14 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # :markup: markdown
4
-
5
3
  module ActionDispatch
6
- class LogSubscriber < ActiveSupport::LogSubscriber
4
+ class LogSubscriber < ActiveSupport::LogSubscriber # :nodoc:
5
+ class_attribute :backtrace_cleaner, default: ActiveSupport::BacktraceCleaner.new
6
+
7
7
  def redirect(event)
8
8
  payload = event.payload
9
9
 
10
10
  info { "Redirected to #{payload[:location]}" }
11
11
 
12
+ if ActionDispatch.verbose_redirect_logs
13
+ info { "↳ #{payload[:source_location]}" }
14
+ end
15
+
12
16
  info do
13
17
  status = payload[:status]
14
18
 
@@ -116,13 +116,15 @@ module ActionDispatch
116
116
  # cookies[:login] = { value: "XJ-122", expires: Time.utc(2020, 10, 15, 5) }
117
117
  #
118
118
  # # Sets a signed cookie, which prevents users from tampering with its value.
119
- # # It can be read using the signed method `cookies.signed[:name]`
120
119
  # cookies.signed[:user_id] = current_user.id
120
+ # # It can be read using the signed method.
121
+ # cookies.signed[:user_id] # => 123
121
122
  #
122
123
  # # Sets an encrypted cookie value before sending it to the client which
123
124
  # # prevent users from reading and tampering with its value.
124
- # # It can be read using the encrypted method `cookies.encrypted[:name]`
125
125
  # cookies.encrypted[:discount] = 45
126
+ # # It can be read using the encrypted method.
127
+ # cookies.encrypted[:discount] # => 45
126
128
  #
127
129
  # # Sets a "permanent" cookie (which expires in 20 years from now).
128
130
  # cookies.permanent[:login] = "XJ-122"
@@ -608,8 +610,10 @@ module ActionDispatch
608
610
  end
609
611
 
610
612
  def check_for_overflow!(name, options)
611
- if options[:value].bytesize > MAX_COOKIE_SIZE
612
- raise CookieOverflow, "#{name} cookie overflowed with size #{options[:value].bytesize} bytes"
613
+ total_size = name.to_s.bytesize + options[:value].bytesize
614
+
615
+ if total_size > MAX_COOKIE_SIZE
616
+ raise CookieOverflow, "#{name} cookie overflowed with size #{total_size} bytes"
613
617
  end
614
618
  end
615
619
  end
@@ -65,7 +65,9 @@ module ActionDispatch
65
65
  content_type = Mime[:text]
66
66
  end
67
67
 
68
- if api_request?(content_type)
68
+ if request.head?
69
+ render(wrapper.status_code, "", content_type)
70
+ elsif api_request?(content_type)
69
71
  render_for_api_request(content_type, wrapper)
70
72
  else
71
73
  render_for_browser_request(request, wrapper)
@@ -125,6 +127,7 @@ module ActionDispatch
125
127
  trace_to_show: wrapper.trace_to_show,
126
128
  routes_inspector: routes_inspector(wrapper),
127
129
  source_extracts: wrapper.source_extracts,
130
+ exception_message_for_copy: compose_exception_message(wrapper).join("\n"),
128
131
  )
129
132
  end
130
133
 
@@ -138,22 +141,40 @@ module ActionDispatch
138
141
  return unless logger
139
142
  return if !log_rescued_responses?(request) && wrapper.rescue_response?
140
143
 
144
+ message = compose_exception_message(wrapper)
145
+ log_array(logger, message, request)
146
+ end
147
+
148
+ def compose_exception_message(wrapper)
141
149
  trace = wrapper.exception_trace
142
150
 
143
151
  message = []
144
152
  message << " "
145
- message << "#{wrapper.exception_class_name} (#{wrapper.message}):"
146
153
  if wrapper.has_cause?
147
- message << "\nCauses:"
154
+ message << "#{wrapper.exception_class_name} (#{wrapper.message})"
148
155
  wrapper.wrapped_causes.each do |wrapped_cause|
149
- message << "#{wrapped_cause.exception_class_name} (#{wrapped_cause.message})"
156
+ message << "Caused by: #{wrapped_cause.exception_class_name} (#{wrapped_cause.message})"
150
157
  end
158
+
159
+ message << "\nInformation for: #{wrapper.exception_class_name} (#{wrapper.message}):"
160
+ else
161
+ message << "#{wrapper.exception_class_name} (#{wrapper.message}):"
151
162
  end
163
+
152
164
  message.concat(wrapper.annotated_source_code)
153
165
  message << " "
154
166
  message.concat(trace)
155
167
 
156
- log_array(logger, message, request)
168
+ if wrapper.has_cause?
169
+ wrapper.wrapped_causes.each do |wrapped_cause|
170
+ message << "\nInformation for cause: #{wrapped_cause.exception_class_name} (#{wrapped_cause.message}):"
171
+ message.concat(wrapped_cause.annotated_source_code)
172
+ message << " "
173
+ message.concat(wrapped_cause.exception_trace)
174
+ end
175
+ end
176
+
177
+ message
157
178
  end
158
179
 
159
180
  def log_array(logger, lines, request)
@@ -15,17 +15,12 @@ module ActionDispatch
15
15
  paths = RESCUES_TEMPLATE_PATHS.dup
16
16
  lookup_context = ActionView::LookupContext.new(paths)
17
17
  super(lookup_context, assigns, nil)
18
- @exception_wrapper = assigns[:exception_wrapper]
19
18
  end
20
19
 
21
20
  def compiled_method_container
22
21
  self.class
23
22
  end
24
23
 
25
- def error_highlight_available?
26
- @exception_wrapper.error_highlight_available?
27
- end
28
-
29
24
  def debug_params(params)
30
25
  clean_params = params.clone
31
26
  clean_params.delete("action")
@@ -60,6 +55,17 @@ module ActionDispatch
60
55
  end
61
56
  end
62
57
 
58
+ def editor_url(location, line: nil)
59
+ if editor = ActiveSupport::Editor.current
60
+ line ||= location&.lineno
61
+ absolute_path = location&.absolute_path
62
+
63
+ if absolute_path && line && File.exist?(absolute_path)
64
+ editor.url_for(absolute_path, line)
65
+ end
66
+ end
67
+ end
68
+
63
69
  def protect_against_forgery?
64
70
  false
65
71
  end
@@ -18,11 +18,12 @@ module ActionDispatch
18
18
  "ActionController::UnknownFormat" => :not_acceptable,
19
19
  "ActionDispatch::Http::MimeNegotiation::InvalidType" => :not_acceptable,
20
20
  "ActionController::MissingExactTemplate" => :not_acceptable,
21
- "ActionController::InvalidAuthenticityToken" => :unprocessable_entity,
22
- "ActionController::InvalidCrossOriginRequest" => :unprocessable_entity,
21
+ "ActionController::InvalidAuthenticityToken" => ActionDispatch::Constants::UNPROCESSABLE_CONTENT,
22
+ "ActionController::InvalidCrossOriginRequest" => ActionDispatch::Constants::UNPROCESSABLE_CONTENT,
23
23
  "ActionDispatch::Http::Parameters::ParseError" => :bad_request,
24
24
  "ActionController::BadRequest" => :bad_request,
25
25
  "ActionController::ParameterMissing" => :bad_request,
26
+ "ActionController::TooManyRequests" => :too_many_requests,
26
27
  "Rack::QueryParser::ParameterTypeError" => :bad_request,
27
28
  "Rack::QueryParser::InvalidParameterError" => :bad_request
28
29
  )
@@ -148,15 +149,20 @@ module ActionDispatch
148
149
  application_trace_with_ids = []
149
150
  framework_trace_with_ids = []
150
151
  full_trace_with_ids = []
152
+ application_traces = application_trace.map(&:to_s)
151
153
 
154
+ full_trace = backtrace_cleaner&.clean_locations(backtrace, :all).presence || backtrace
152
155
  full_trace.each_with_index do |trace, idx|
156
+ filtered_trace = backtrace_cleaner&.clean_frame(trace, :all) || trace
157
+
153
158
  trace_with_id = {
154
159
  exception_object_id: @exception.object_id,
155
160
  id: idx,
156
- trace: trace
161
+ trace: trace,
162
+ filtered_trace: filtered_trace,
157
163
  }
158
164
 
159
- if application_trace.include?(trace)
165
+ if application_traces.include?(filtered_trace.to_s)
160
166
  application_trace_with_ids << trace_with_id
161
167
  else
162
168
  framework_trace_with_ids << trace_with_id
@@ -173,7 +179,7 @@ module ActionDispatch
173
179
  end
174
180
 
175
181
  def self.status_code_for_exception(class_name)
176
- Rack::Utils.status_code(@@rescue_responses[class_name])
182
+ ActionDispatch::Response.rack_status_code(@@rescue_responses[class_name])
177
183
  end
178
184
 
179
185
  def show?(request)
@@ -197,16 +203,10 @@ module ActionDispatch
197
203
 
198
204
  def source_extracts
199
205
  backtrace.map do |trace|
200
- extract_source(trace)
206
+ extract_source(trace).merge(trace: trace)
201
207
  end
202
208
  end
203
209
 
204
- def error_highlight_available?
205
- # ErrorHighlight.spot with backtrace_location keyword is available since
206
- # error_highlight 0.4.0
207
- defined?(ErrorHighlight) && Gem::Version.new(ErrorHighlight::VERSION) >= Gem::Version.new("0.4.0")
208
- end
209
-
210
210
  def trace_to_show
211
211
  if traces["Application Trace"].empty? && rescue_template != "routing_error"
212
212
  "Full Trace"
@@ -267,13 +267,13 @@ module ActionDispatch
267
267
  end
268
268
 
269
269
  (@exception.backtrace_locations || []).map do |loc|
270
- if built_methods.key?(loc.label.to_s)
270
+ if built_methods.key?(loc.base_label)
271
271
  thread_backtrace_location = if loc.respond_to?(:__getobj__)
272
272
  loc.__getobj__
273
273
  else
274
274
  loc
275
275
  end
276
- SourceMapLocation.new(thread_backtrace_location, built_methods[loc.label.to_s])
276
+ SourceMapLocation.new(thread_backtrace_location, built_methods[loc.base_label])
277
277
  else
278
278
  loc
279
279
  end
@@ -12,6 +12,10 @@ module ActionDispatch
12
12
 
13
13
  def call(env)
14
14
  state = @executor.run!(reset: true)
15
+ if response_finished = env["rack.response_finished"]
16
+ response_finished << proc { state.complete! }
17
+ end
18
+
15
19
  begin
16
20
  response = @app.call(env)
17
21
 
@@ -20,12 +24,21 @@ module ActionDispatch
20
24
  @executor.error_reporter.report(error, handled: false, source: "application.action_dispatch")
21
25
  end
22
26
 
23
- returned = response << ::Rack::BodyProxy.new(response.pop) { state.complete! }
24
- rescue => error
25
- @executor.error_reporter.report(error, handled: false, source: "application.action_dispatch")
27
+ unless response_finished
28
+ response << ::Rack::BodyProxy.new(response.pop) { state.complete! }
29
+ end
30
+ returned = true
31
+ response
32
+ rescue Exception => error
33
+ request = ActionDispatch::Request.new env
34
+ backtrace_cleaner = request.get_header("action_dispatch.backtrace_cleaner")
35
+ wrapper = ExceptionWrapper.new(backtrace_cleaner, error)
36
+ @executor.error_reporter.report(wrapper.unwrapped_exception, handled: false, source: "application.action_dispatch")
26
37
  raise
27
38
  ensure
28
- state.complete! unless returned
39
+ if !returned && !response_finished
40
+ state.complete!
41
+ end
29
42
  end
30
43
  end
31
44
  end
@@ -25,14 +25,14 @@ module ActionDispatch
25
25
  def call(env)
26
26
  request = ActionDispatch::Request.new(env)
27
27
  status = request.path_info[1..-1].to_i
28
- begin
29
- content_type = request.formats.first
30
- rescue ActionDispatch::Http::MimeNegotiation::InvalidType
31
- content_type = Mime[:text]
32
- end
28
+ content_type = request.formats.first
33
29
  body = { status: status, error: Rack::Utils::HTTP_STATUS_CODES.fetch(status, Rack::Utils::HTTP_STATUS_CODES[500]) }
34
30
 
35
- render(status, content_type, body)
31
+ if env["action_dispatch.original_request_method"] == "HEAD"
32
+ render_format(status, content_type, "")
33
+ else
34
+ render(status, content_type, body)
35
+ end
36
36
  end
37
37
 
38
38
  private
@@ -44,6 +44,8 @@ module ActionDispatch
44
44
  "10.0.0.0/8", # private IPv4 range 10.x.x.x
45
45
  "172.16.0.0/12", # private IPv4 range 172.16.0.0 .. 172.31.255.255
46
46
  "192.168.0.0/16", # private IPv4 range 192.168.x.x
47
+ "169.254.0.0/16", # link-local IPv4 range 169.254.x.x
48
+ "fe80::/10", # link-local IPv6 range fe80::/10
47
49
  ].map { |proxy| IPAddr.new(proxy) }
48
50
 
49
51
  attr_reader :check_ip, :proxies
@@ -126,11 +128,11 @@ module ActionDispatch
126
128
  # left, which was presumably set by one of those proxies.
127
129
  def calculate_ip
128
130
  # Set by the Rack web server, this is a single value.
129
- remote_addr = ips_from(@req.remote_addr).last
131
+ remote_addr = sanitize_ips(ips_from(@req.remote_addr)).last
130
132
 
131
133
  # Could be a CSV list and/or repeated headers that were concatenated.
132
- client_ips = ips_from(@req.client_ip).reverse!
133
- forwarded_ips = ips_from(@req.x_forwarded_for).reverse!
134
+ client_ips = sanitize_ips(ips_from(@req.client_ip)).reverse!
135
+ forwarded_ips = sanitize_ips(@req.forwarded_for || []).reverse!
134
136
 
135
137
  # `Client-Ip` and `X-Forwarded-For` should not, generally, both be set. If they
136
138
  # are both set, it means that either:
@@ -150,7 +152,8 @@ module ActionDispatch
150
152
  # We don't know which came from the proxy, and which from the user
151
153
  raise IpSpoofAttackError, "IP spoofing attack?! " \
152
154
  "HTTP_CLIENT_IP=#{@req.client_ip.inspect} " \
153
- "HTTP_X_FORWARDED_FOR=#{@req.x_forwarded_for.inspect}"
155
+ "HTTP_X_FORWARDED_FOR=#{@req.x_forwarded_for.inspect}" \
156
+ " HTTP_FORWARDED=" + @req.forwarded_for.map { "for=#{_1}" }.join(", ").inspect if @req.forwarded_for.any?
154
157
  end
155
158
 
156
159
  # We assume these things about the IP headers:
@@ -176,7 +179,10 @@ module ActionDispatch
176
179
  def ips_from(header) # :doc:
177
180
  return [] unless header
178
181
  # Split the comma-separated list into an array of strings.
179
- ips = header.strip.split(/[,\s]+/)
182
+ header.strip.split(/[,\s]+/)
183
+ end
184
+
185
+ def sanitize_ips(ips) # :doc:
180
186
  ips.select! do |ip|
181
187
  # Only return IPs that are valid according to the IPAddr#new method.
182
188
  range = IPAddr.new(ip).to_range
@@ -25,11 +25,12 @@ module ActionDispatch
25
25
  def initialize(app, header:)
26
26
  @app = app
27
27
  @header = header
28
+ @env_header = "HTTP_#{header.upcase.tr("-", "_")}"
28
29
  end
29
30
 
30
31
  def call(env)
31
32
  req = ActionDispatch::Request.new env
32
- req.request_id = make_request_id(req.headers[@header])
33
+ req.request_id = make_request_id(req.get_header(@env_header))
33
34
  @app.call(env).tap { |_status, headers, _body| headers[@header] = req.request_id }
34
35
  end
35
36
 
@@ -18,11 +18,16 @@ module ActionDispatch
18
18
  # * `expire_after` - The length of time a session will be stored before
19
19
  # automatically expiring. By default, the `:expires_in` option of the cache
20
20
  # is used.
21
+ # * `check_collisions` - Check if newly generated session ids aren't already in use.
22
+ # If for some reason 128 bits of randomness aren't considered secure enough to avoid
23
+ # collisions, this option can be enabled to ensure newly generated ids aren't in use.
24
+ # By default, it is set to `false` to avoid additional cache write operations.
21
25
  #
22
26
  class CacheStore < AbstractSecureStore
23
27
  def initialize(app, options = {})
24
28
  @cache = options[:cache] || Rails.cache
25
29
  options[:expire_after] ||= @cache.options[:expires_in]
30
+ @check_collisions = options[:check_collisions] || false
26
31
  super
27
32
  end
28
33
 
@@ -61,6 +66,18 @@ module ActionDispatch
61
66
  def get_session_with_fallback(sid)
62
67
  @cache.read(cache_key(sid.private_id)) || @cache.read(cache_key(sid.public_id))
63
68
  end
69
+
70
+ def generate_sid
71
+ if @check_collisions
72
+ loop do
73
+ sid = super
74
+ key = cache_key(sid.private_id)
75
+ break sid if @cache.write(key, {}, unless_exist: true, expires_in: default_options[:expire_after])
76
+ end
77
+ else
78
+ super
79
+ end
80
+ end
64
81
  end
65
82
  end
66
83
  end
@@ -11,9 +11,11 @@ module ActionDispatch
11
11
  #
12
12
  # 1. **TLS redirect**: Permanently redirects `http://` requests to `https://`
13
13
  # with the same URL host, path, etc. Enabled by default. Set
14
- # `config.ssl_options` to modify the destination URL (e.g. `redirect: {
15
- # host: "secure.widgets.com", port: 8080 }`), or set `redirect: false` to
16
- # disable this feature.
14
+ # `config.ssl_options` to modify the destination URL:
15
+ #
16
+ # config.ssl_options = { redirect: { host: "secure.widgets.com", port: 8080 }`
17
+ #
18
+ # Or set `redirect: false` to disable redirection.
17
19
  #
18
20
  # Requests can opt-out of redirection with `exclude`:
19
21
  #
@@ -21,6 +23,14 @@ module ActionDispatch
21
23
  #
22
24
  # Cookies will not be flagged as secure for excluded requests.
23
25
  #
26
+ # When proxying through a load balancer that terminates SSL, the forwarded
27
+ # request will appear as though it's HTTP instead of HTTPS to the application.
28
+ # This makes redirects and cookie security target HTTP instead of HTTPS.
29
+ # To make the server assume that the proxy already terminated SSL, and
30
+ # that the request really is HTTPS, set `config.assume_ssl` to `true`:
31
+ #
32
+ # config.assume_ssl = true
33
+ #
24
34
  # 2. **Secure cookies**: Sets the `secure` flag on cookies to tell browsers
25
35
  # they must not be sent along with `http://` requests. Enabled by default.
26
36
  # Set `config.ssl_options` with `secure_cookies: false` to disable this
@@ -0,0 +1 @@
1
+ <button onclick="copyAsText.bind(this)()">Copy as text</button>