actionpack 5.0.0.1 → 5.0.1.rc1

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 +195 -0
  3. data/README.rdoc +1 -1
  4. data/lib/abstract_controller/callbacks.rb +3 -1
  5. data/lib/action_controller/metal/data_streaming.rb +1 -0
  6. data/lib/action_controller/metal/etag_with_template_digest.rb +7 -1
  7. data/lib/action_controller/metal/force_ssl.rb +1 -1
  8. data/lib/action_controller/metal/implicit_render.rb +1 -1
  9. data/lib/action_controller/metal/live.rb +6 -1
  10. data/lib/action_controller/metal/renderers.rb +0 -2
  11. data/lib/action_controller/metal/rendering.rb +1 -1
  12. data/lib/action_controller/metal/request_forgery_protection.rb +3 -3
  13. data/lib/action_controller/metal/strong_parameters.rb +53 -15
  14. data/lib/action_controller/test_case.rb +39 -21
  15. data/lib/action_dispatch.rb +1 -0
  16. data/lib/action_dispatch/http/headers.rb +1 -1
  17. data/lib/action_dispatch/http/mime_negotiation.rb +2 -2
  18. data/lib/action_dispatch/http/parameters.rb +8 -1
  19. data/lib/action_dispatch/http/request.rb +0 -12
  20. data/lib/action_dispatch/http/response.rb +15 -8
  21. data/lib/action_dispatch/journey/formatter.rb +3 -1
  22. data/lib/action_dispatch/journey/parser.rb +2 -0
  23. data/lib/action_dispatch/journey/parser_extras.rb +4 -2
  24. data/lib/action_dispatch/journey/route.rb +5 -3
  25. data/lib/action_dispatch/journey/router.rb +3 -1
  26. data/lib/action_dispatch/journey/visitors.rb +3 -1
  27. data/lib/action_dispatch/middleware/cookies.rb +3 -3
  28. data/lib/action_dispatch/middleware/debug_exceptions.rb +5 -1
  29. data/lib/action_dispatch/middleware/debug_locks.rb +122 -0
  30. data/lib/action_dispatch/middleware/exception_wrapper.rb +2 -2
  31. data/lib/action_dispatch/middleware/ssl.rb +14 -5
  32. data/lib/action_dispatch/middleware/static.rb +3 -3
  33. data/lib/action_dispatch/routing/mapper.rb +74 -61
  34. data/lib/action_dispatch/routing/redirection.rb +0 -1
  35. data/lib/action_dispatch/testing/integration.rb +150 -196
  36. data/lib/action_dispatch/testing/request_encoder.rb +53 -0
  37. data/lib/action_dispatch/testing/test_process.rb +2 -1
  38. data/lib/action_dispatch/testing/test_request.rb +1 -1
  39. data/lib/action_dispatch/testing/test_response.rb +7 -2
  40. data/lib/action_pack/gem_version.rb +2 -2
  41. metadata +13 -12
@@ -50,6 +50,7 @@ module ActionDispatch
50
50
  autoload :Callbacks
51
51
  autoload :Cookies
52
52
  autoload :DebugExceptions
53
+ autoload :DebugLocks
53
54
  autoload :ExceptionWrapper
54
55
  autoload :Executor
55
56
  autoload :Flash
@@ -3,7 +3,7 @@ module ActionDispatch
3
3
  # Provides access to the request's HTTP headers from the environment.
4
4
  #
5
5
  # env = { "CONTENT_TYPE" => "text/plain", "HTTP_USER_AGENT" => "curl/7.43.0" }
6
- # headers = ActionDispatch::Http::Headers.new(env)
6
+ # headers = ActionDispatch::Http::Headers.from_hash(env)
7
7
  # headers["Content-Type"] # => "text/plain"
8
8
  # headers["User-Agent"] # => "curl/7.43.0"
9
9
  #
@@ -29,8 +29,8 @@ module ActionDispatch
29
29
  content_mime_type && content_mime_type.to_s
30
30
  end
31
31
 
32
- def has_content_type?
33
- has_header? 'CONTENT_TYPE'
32
+ def has_content_type? # :nodoc:
33
+ get_header "CONTENT_TYPE"
34
34
  end
35
35
 
36
36
  # Returns the accepted MIME type for the request.
@@ -44,7 +44,14 @@ module ActionDispatch
44
44
 
45
45
  def path_parameters=(parameters) #:nodoc:
46
46
  delete_header('action_dispatch.request.parameters')
47
+
48
+ # If any of the path parameters has an invalid encoding then
49
+ # raise since it's likely to trigger errors further on.
50
+ Request::Utils.check_param_encoding(parameters)
51
+
47
52
  set_header PARAMETERS_KEY, parameters
53
+ rescue Rack::Utils::ParameterTypeError, Rack::Utils::InvalidParameterError => e
54
+ raise ActionController::BadRequest.new("Invalid path parameters: #{e.message}")
48
55
  end
49
56
 
50
57
  # Returns a hash with the \parameters used to form the \path of the request.
@@ -58,7 +65,7 @@ module ActionDispatch
58
65
  private
59
66
 
60
67
  def parse_formatted_parameters(parsers)
61
- return yield if content_length.zero?
68
+ return yield if content_length.zero? || content_mime_type.nil?
62
69
 
63
70
  strategy = parsers.fetch(content_mime_type.symbol) { return yield }
64
71
 
@@ -66,24 +66,12 @@ module ActionDispatch
66
66
  def commit_cookie_jar! # :nodoc:
67
67
  end
68
68
 
69
- def check_path_parameters!
70
- # If any of the path parameters has an invalid encoding then
71
- # raise since it's likely to trigger errors further on.
72
- path_parameters.each do |key, value|
73
- next unless value.respond_to?(:valid_encoding?)
74
- unless value.valid_encoding?
75
- raise ActionController::BadRequest, "Invalid parameter encoding: #{key} => #{value.inspect}"
76
- end
77
- end
78
- end
79
-
80
69
  PASS_NOT_FOUND = Class.new { # :nodoc:
81
70
  def self.action(_); self; end
82
71
  def self.call(_); [404, {'X-Cascade' => 'pass'}, []]; end
83
72
  }
84
73
 
85
74
  def controller_class
86
- check_path_parameters!
87
75
  params = path_parameters
88
76
 
89
77
  if params.key?(:controller)
@@ -224,8 +224,10 @@ module ActionDispatch # :nodoc:
224
224
 
225
225
  # Sets the HTTP content type.
226
226
  def content_type=(content_type)
227
- header_info = parse_content_type
228
- set_content_type content_type.to_s, header_info.charset || self.class.default_charset
227
+ return unless content_type
228
+ new_header_info = parse_content_type(content_type.to_s)
229
+ prev_header_info = parsed_content_type_header
230
+ set_content_type new_header_info.mime_type, new_header_info.charset || prev_header_info.charset || self.class.default_charset
229
231
  end
230
232
 
231
233
  # Sets the HTTP response's content MIME type. For example, in the controller
@@ -238,7 +240,7 @@ module ActionDispatch # :nodoc:
238
240
  # information.
239
241
 
240
242
  def content_type
241
- parse_content_type.mime_type
243
+ parsed_content_type_header.mime_type
242
244
  end
243
245
 
244
246
  def sending_file=(v)
@@ -253,7 +255,7 @@ module ActionDispatch # :nodoc:
253
255
  # response.charset = 'utf-16' # => 'utf-16'
254
256
  # response.charset = nil # => 'utf-8'
255
257
  def charset=(charset)
256
- header_info = parse_content_type
258
+ header_info = parsed_content_type_header
257
259
  if false == charset
258
260
  set_header CONTENT_TYPE, header_info.mime_type
259
261
  else
@@ -265,7 +267,7 @@ module ActionDispatch # :nodoc:
265
267
  # The charset of the response. HTML wants to know the encoding of the
266
268
  # content you're giving them, so we need to send that along.
267
269
  def charset
268
- header_info = parse_content_type
270
+ header_info = parsed_content_type_header
269
271
  header_info.charset || self.class.default_charset
270
272
  end
271
273
 
@@ -403,8 +405,7 @@ module ActionDispatch # :nodoc:
403
405
  ContentTypeHeader = Struct.new :mime_type, :charset
404
406
  NullContentTypeHeader = ContentTypeHeader.new nil, nil
405
407
 
406
- def parse_content_type
407
- content_type = get_header CONTENT_TYPE
408
+ def parse_content_type(content_type)
408
409
  if content_type
409
410
  type, charset = content_type.split(/;\s*charset=/)
410
411
  type = nil if type.empty?
@@ -414,6 +415,12 @@ module ActionDispatch # :nodoc:
414
415
  end
415
416
  end
416
417
 
418
+ # Small internal convenience method to get the parsed version of the current
419
+ # content type header.
420
+ def parsed_content_type_header
421
+ parse_content_type(get_header(CONTENT_TYPE))
422
+ end
423
+
417
424
  def set_content_type(content_type, charset)
418
425
  type = (content_type || '').dup
419
426
  type << "; charset=#{charset}" if charset
@@ -450,7 +457,7 @@ module ActionDispatch # :nodoc:
450
457
  def assign_default_content_type_and_charset!
451
458
  return if content_type
452
459
 
453
- ct = parse_content_type
460
+ ct = parsed_content_type_header
454
461
  set_content_type(ct.mime_type || Mime[:html].to_s,
455
462
  ct.charset || self.class.default_charset)
456
463
  end
@@ -1,10 +1,11 @@
1
1
  require 'action_controller/metal/exceptions'
2
2
 
3
3
  module ActionDispatch
4
+ # :stopdoc:
4
5
  module Journey
5
6
  # The Formatter class is used for formatting URLs. For example, parameters
6
7
  # passed to +url_for+ in Rails will eventually call Formatter#generate.
7
- class Formatter # :nodoc:
8
+ class Formatter
8
9
  attr_reader :routes
9
10
 
10
11
  def initialize(routes)
@@ -174,4 +175,5 @@ module ActionDispatch
174
175
  end
175
176
  end
176
177
  end
178
+ # :stopdoc:
177
179
  end
@@ -9,6 +9,7 @@ require 'racc/parser.rb'
9
9
 
10
10
  require 'action_dispatch/journey/parser_extras'
11
11
  module ActionDispatch
12
+ # :stopdoc:
12
13
  module Journey
13
14
  class Parser < Racc::Parser
14
15
  ##### State transition tables begin ###
@@ -195,4 +196,5 @@ end
195
196
 
196
197
  end # class Parser
197
198
  end # module Journey
199
+ # :startdoc:
198
200
  end # module ActionDispatch
@@ -2,8 +2,9 @@ require 'action_dispatch/journey/scanner'
2
2
  require 'action_dispatch/journey/nodes/node'
3
3
 
4
4
  module ActionDispatch
5
- module Journey # :nodoc:
6
- class Parser < Racc::Parser # :nodoc:
5
+ # :stopdoc:
6
+ module Journey
7
+ class Parser < Racc::Parser
7
8
  include Journey::Nodes
8
9
 
9
10
  def self.parse(string)
@@ -24,4 +25,5 @@ module ActionDispatch
24
25
  end
25
26
  end
26
27
  end
28
+ # :startdoc:
27
29
  end
@@ -1,6 +1,7 @@
1
1
  module ActionDispatch
2
- module Journey # :nodoc:
3
- class Route # :nodoc:
2
+ # :stopdoc:
3
+ module Journey
4
+ class Route
4
5
  attr_reader :app, :path, :defaults, :name, :precedence
5
6
 
6
7
  attr_reader :constraints, :internal
@@ -81,7 +82,7 @@ module ActionDispatch
81
82
  end
82
83
  end
83
84
 
84
- def requirements # :nodoc:
85
+ def requirements
85
86
  # needed for rails `rails routes`
86
87
  @defaults.merge(path.requirements).delete_if { |_,v|
87
88
  /.+?/ == v
@@ -177,4 +178,5 @@ module ActionDispatch
177
178
  end
178
179
  end
179
180
  end
181
+ # :startdoc:
180
182
  end
@@ -72,7 +72,9 @@ module ActionDispatch
72
72
  private
73
73
 
74
74
  def partitioned_routes
75
- routes.partitioned_routes
75
+ routes.partition { |r|
76
+ r.path.anchored && r.ast.grep(Nodes::Symbol).all? { |n| n.default_regexp? }
77
+ }
76
78
  end
77
79
 
78
80
  def ast
@@ -1,5 +1,6 @@
1
1
  module ActionDispatch
2
- module Journey # :nodoc:
2
+ # :stopdoc:
3
+ module Journey
3
4
  class Format
4
5
  ESCAPE_PATH = ->(value) { Router::Utils.escape_path(value) }
5
6
  ESCAPE_SEGMENT = ->(value) { Router::Utils.escape_segment(value) }
@@ -261,4 +262,5 @@ module ActionDispatch
261
262
  end
262
263
  end
263
264
  end
265
+ # :startdoc:
264
266
  end
@@ -372,7 +372,7 @@ module ActionDispatch
372
372
 
373
373
  handle_options(options)
374
374
 
375
- if @cookies[name.to_s] != value or options[:expires]
375
+ if @cookies[name.to_s] != value || options[:expires]
376
376
  @cookies[name.to_s] = value
377
377
  @set_cookies[name.to_s] = options
378
378
  @delete_cookies.delete(name.to_s)
@@ -576,8 +576,8 @@ module ActionDispatch
576
576
  "Read the upgrade documentation to learn more about this new config option."
577
577
  end
578
578
 
579
- secret = key_generator.generate_key(request.encrypted_cookie_salt || '')
580
- sign_secret = key_generator.generate_key(request.encrypted_signed_cookie_salt || '')
579
+ secret = key_generator.generate_key(request.encrypted_cookie_salt || "")[0, ActiveSupport::MessageEncryptor.key_len]
580
+ sign_secret = key_generator.generate_key(request.encrypted_signed_cookie_salt || "")
581
581
  @encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, digest: digest, serializer: ActiveSupport::MessageEncryptor::NullSerializer)
582
582
  end
583
583
 
@@ -165,7 +165,11 @@ module ActionDispatch
165
165
  end
166
166
 
167
167
  def log_array(logger, array)
168
- array.map { |line| logger.fatal line }
168
+ if logger.formatter && logger.formatter.respond_to?(:tags_text)
169
+ logger.fatal array.join("\n#{logger.formatter.tags_text}")
170
+ else
171
+ logger.fatal array.join("\n")
172
+ end
169
173
  end
170
174
 
171
175
  def logger(request)
@@ -0,0 +1,122 @@
1
+ module ActionDispatch
2
+ # This middleware can be used to diagnose deadlocks in the autoload interlock.
3
+ #
4
+ # To use it, insert it near the top of the middleware stack, using
5
+ # <tt>config/application.rb</tt>:
6
+ #
7
+ # config.middleware.insert_before Rack::Sendfile, ActionDispatch::DebugLocks
8
+ #
9
+ # After restarting the application and re-triggering the deadlock condition,
10
+ # <tt>/rails/locks</tt> will show a summary of all threads currently known to
11
+ # the interlock, which lock level they are holding or awaiting, and their
12
+ # current backtrace.
13
+ #
14
+ # Generally a deadlock will be caused by the interlock conflicting with some
15
+ # other external lock or blocking I/O call. These cannot be automatically
16
+ # identified, but should be visible in the displayed backtraces.
17
+ #
18
+ # NOTE: The formatting and content of this middleware's output is intended for
19
+ # human consumption, and should be expected to change between releases.
20
+ #
21
+ # This middleware exposes operational details of the server, with no access
22
+ # control. It should only be enabled when in use, and removed thereafter.
23
+ class DebugLocks
24
+ def initialize(app, path = '/rails/locks')
25
+ @app = app
26
+ @path = path
27
+ end
28
+
29
+ def call(env)
30
+ req = ActionDispatch::Request.new env
31
+
32
+ if req.get?
33
+ path = req.path_info.chomp('/'.freeze)
34
+ if path == @path
35
+ return render_details(req)
36
+ end
37
+ end
38
+
39
+ @app.call(env)
40
+ end
41
+
42
+ private
43
+ def render_details(req)
44
+ threads = ActiveSupport::Dependencies.interlock.raw_state do |threads|
45
+ # The Interlock itself comes to a complete halt as long as this block
46
+ # is executing. That gives us a more consistent picture of everything,
47
+ # but creates a pretty strong Observer Effect.
48
+ #
49
+ # Most directly, that means we need to do as little as possible in
50
+ # this block. More widely, it means this middleware should remain a
51
+ # strictly diagnostic tool (to be used when something has gone wrong),
52
+ # and not for any sort of general monitoring.
53
+
54
+ threads.each.with_index do |(thread, info), idx|
55
+ info[:index] = idx
56
+ info[:backtrace] = thread.backtrace
57
+ end
58
+
59
+ threads
60
+ end
61
+
62
+ str = threads.map do |thread, info|
63
+ if info[:exclusive]
64
+ lock_state = 'Exclusive'
65
+ elsif info[:sharing] > 0
66
+ lock_state = 'Sharing'
67
+ lock_state << " x#{info[:sharing]}" if info[:sharing] > 1
68
+ else
69
+ lock_state = 'No lock'
70
+ end
71
+
72
+ if info[:waiting]
73
+ lock_state << ' (yielded share)'
74
+ end
75
+
76
+ msg = "Thread #{info[:index]} [0x#{thread.__id__.to_s(16)} #{thread.status || 'dead'}] #{lock_state}\n"
77
+
78
+ if info[:sleeper]
79
+ msg << " Waiting in #{info[:sleeper]}"
80
+ msg << " to #{info[:purpose].to_s.inspect}" unless info[:purpose].nil?
81
+ msg << "\n"
82
+
83
+ if info[:compatible]
84
+ compat = info[:compatible].map { |c| c == false ? "share" : c.to_s.inspect }
85
+ msg << " may be pre-empted for: #{compat.join(', ')}\n"
86
+ end
87
+
88
+ blockers = threads.values.select { |binfo| blocked_by?(info, binfo, threads.values) }
89
+ msg << " blocked by: #{blockers.map {|i| i[:index] }.join(', ')}\n" if blockers.any?
90
+ end
91
+
92
+ blockees = threads.values.select { |binfo| blocked_by?(binfo, info, threads.values) }
93
+ msg << " blocking: #{blockees.map {|i| i[:index] }.join(', ')}\n" if blockees.any?
94
+
95
+ msg << "\n#{info[:backtrace].join("\n")}\n" if info[:backtrace]
96
+ end.join("\n\n---\n\n\n")
97
+
98
+ [200, { "Content-Type" => "text/plain", "Content-Length" => str.size }, [str]]
99
+ end
100
+
101
+ def blocked_by?(victim, blocker, all_threads)
102
+ return false if victim.equal?(blocker)
103
+
104
+ case victim[:sleeper]
105
+ when :start_sharing
106
+ blocker[:exclusive] ||
107
+ (!victim[:waiting] && blocker[:compatible] && !blocker[:compatible].include?(false))
108
+ when :start_exclusive
109
+ blocker[:sharing] > 0 ||
110
+ blocker[:exclusive] ||
111
+ (blocker[:compatible] && !blocker[:compatible].include?(victim[:purpose]))
112
+ when :yield_shares
113
+ blocker[:exclusive]
114
+ when :stop_exclusive
115
+ blocker[:exclusive] ||
116
+ victim[:compatible] &&
117
+ victim[:compatible].include?(blocker[:purpose]) &&
118
+ all_threads.all? { |other| !other[:compatible] || blocker.equal?(other) || other[:compatible].include?(blocker[:purpose]) }
119
+ end
120
+ end
121
+ end
122
+ end
@@ -17,8 +17,8 @@ module ActionDispatch
17
17
  'ActionDispatch::ParamsParser::ParseError' => :bad_request,
18
18
  'ActionController::BadRequest' => :bad_request,
19
19
  'ActionController::ParameterMissing' => :bad_request,
20
- 'Rack::Utils::ParameterTypeError' => :bad_request,
21
- 'Rack::Utils::InvalidParameterError' => :bad_request
20
+ 'Rack::QueryParser::ParameterTypeError' => :bad_request,
21
+ 'Rack::QueryParser::InvalidParameterError' => :bad_request
22
22
  )
23
23
 
24
24
  cattr_accessor :rescue_templates
@@ -18,17 +18,18 @@ module ActionDispatch
18
18
  # Enabled by default. Configure `config.ssl_options` with `hsts: false` to disable.
19
19
  #
20
20
  # Set `config.ssl_options` with `hsts: { … }` to configure HSTS:
21
- # * `expires`: How long, in seconds, these settings will stick. Defaults to
22
- # `180.days` (recommended). The minimum required to qualify for browser
23
- # preload lists is `18.weeks`.
21
+ # * `expires`: How long, in seconds, these settings will stick. The minimum
22
+ # required to qualify for browser preload lists is `18.weeks`. Defaults to
23
+ # `180.days` (recommended).
24
24
  # * `subdomains`: Set to `true` to tell the browser to apply these settings
25
25
  # to all subdomains. This protects your cookies from interception by a
26
- # vulnerable site on a subdomain. Defaults to `true`.
26
+ # vulnerable site on a subdomain. Defaults to `false`.
27
27
  # * `preload`: Advertise that this site may be included in browsers'
28
28
  # preloaded HSTS lists. HSTS protects your site on every visit *except the
29
29
  # first visit* since it hasn't seen your HSTS header yet. To close this
30
30
  # gap, browser vendors include a baked-in list of HSTS-enabled sites.
31
31
  # Go to https://hstspreload.appspot.com to submit your site for inclusion.
32
+ # Defaults to `false`.
32
33
  #
33
34
  # To turn off HSTS, omitting the header is not enough. Browsers will remember the
34
35
  # original HSTS directive until it expires. Instead, use the header to tell browsers to
@@ -132,12 +133,20 @@ module ActionDispatch
132
133
  end
133
134
 
134
135
  def redirect_to_https(request)
135
- [ @redirect.fetch(:status, 301),
136
+ [ @redirect.fetch(:status, redirection_status(request)),
136
137
  { 'Content-Type' => 'text/html',
137
138
  'Location' => https_location_for(request) },
138
139
  @redirect.fetch(:body, []) ]
139
140
  end
140
141
 
142
+ def redirection_status(request)
143
+ if request.get? || request.head?
144
+ 301 # Issue a permanent redirect via a GET request.
145
+ else
146
+ 307 # Issue a fresh request redirect to preserve the HTTP method.
147
+ end
148
+ end
149
+
141
150
  def https_location_for(request)
142
151
  host = @redirect[:host] || request.host
143
152
  port = @redirect[:port] || request.port