actionpack 6.1.7.3 → 7.0.8

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 (128) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +320 -390
  3. data/MIT-LICENSE +1 -0
  4. data/README.rdoc +4 -5
  5. data/lib/abstract_controller/asset_paths.rb +1 -1
  6. data/lib/abstract_controller/base.rb +13 -26
  7. data/lib/abstract_controller/caching/fragments.rb +2 -2
  8. data/lib/abstract_controller/caching.rb +1 -1
  9. data/lib/abstract_controller/callbacks.rb +21 -7
  10. data/lib/abstract_controller/collector.rb +2 -2
  11. data/lib/abstract_controller/error.rb +1 -1
  12. data/lib/abstract_controller/helpers.rb +17 -12
  13. data/lib/abstract_controller/logger.rb +1 -1
  14. data/lib/abstract_controller/railties/routes_helpers.rb +2 -0
  15. data/lib/abstract_controller/rendering.rb +9 -11
  16. data/lib/abstract_controller/translation.rb +5 -4
  17. data/lib/abstract_controller/url_for.rb +4 -6
  18. data/lib/action_controller/api.rb +7 -7
  19. data/lib/action_controller/base.rb +5 -4
  20. data/lib/action_controller/form_builder.rb +2 -2
  21. data/lib/action_controller/log_subscriber.rb +4 -3
  22. data/lib/action_controller/metal/basic_implicit_render.rb +3 -1
  23. data/lib/action_controller/metal/conditional_get.rb +137 -102
  24. data/lib/action_controller/metal/content_security_policy.rb +36 -2
  25. data/lib/action_controller/metal/cookies.rb +1 -1
  26. data/lib/action_controller/metal/data_streaming.rb +23 -31
  27. data/lib/action_controller/metal/etag_with_flash.rb +1 -1
  28. data/lib/action_controller/metal/exceptions.rb +19 -30
  29. data/lib/action_controller/metal/flash.rb +6 -2
  30. data/lib/action_controller/metal/head.rb +1 -1
  31. data/lib/action_controller/metal/helpers.rb +2 -2
  32. data/lib/action_controller/metal/http_authentication.rb +66 -39
  33. data/lib/action_controller/metal/instrumentation.rb +57 -52
  34. data/lib/action_controller/metal/live.rb +43 -2
  35. data/lib/action_controller/metal/mime_responds.rb +3 -3
  36. data/lib/action_controller/metal/params_wrapper.rb +20 -11
  37. data/lib/action_controller/metal/permissions_policy.rb +19 -28
  38. data/lib/action_controller/metal/redirecting.rb +111 -19
  39. data/lib/action_controller/metal/renderers.rb +12 -13
  40. data/lib/action_controller/metal/rendering.rb +121 -9
  41. data/lib/action_controller/metal/request_forgery_protection.rb +83 -32
  42. data/lib/action_controller/metal/rescue.rb +5 -4
  43. data/lib/action_controller/metal/streaming.rb +7 -9
  44. data/lib/action_controller/metal/strong_parameters.rb +138 -115
  45. data/lib/action_controller/metal/testing.rb +9 -2
  46. data/lib/action_controller/metal/url_for.rb +3 -5
  47. data/lib/action_controller/metal.rb +10 -13
  48. data/lib/action_controller/railtie.rb +50 -6
  49. data/lib/action_controller/renderer.rb +1 -20
  50. data/lib/action_controller/test_case.rb +28 -7
  51. data/lib/action_controller.rb +2 -5
  52. data/lib/action_dispatch/http/cache.rb +20 -13
  53. data/lib/action_dispatch/http/content_security_policy.rb +113 -36
  54. data/lib/action_dispatch/http/filter_parameters.rb +4 -19
  55. data/lib/action_dispatch/http/headers.rb +1 -1
  56. data/lib/action_dispatch/http/mime_negotiation.rb +15 -5
  57. data/lib/action_dispatch/http/mime_type.rb +9 -11
  58. data/lib/action_dispatch/http/parameters.rb +5 -5
  59. data/lib/action_dispatch/http/permissions_policy.rb +17 -1
  60. data/lib/action_dispatch/http/request.rb +27 -37
  61. data/lib/action_dispatch/http/response.rb +3 -20
  62. data/lib/action_dispatch/http/upload.rb +13 -2
  63. data/lib/action_dispatch/http/url.rb +11 -19
  64. data/lib/action_dispatch/journey/gtg/builder.rb +11 -12
  65. data/lib/action_dispatch/journey/gtg/simulator.rb +10 -4
  66. data/lib/action_dispatch/journey/gtg/transition_table.rb +77 -21
  67. data/lib/action_dispatch/journey/nodes/node.rb +70 -5
  68. data/lib/action_dispatch/journey/path/pattern.rb +22 -13
  69. data/lib/action_dispatch/journey/route.rb +6 -13
  70. data/lib/action_dispatch/journey/router/utils.rb +2 -2
  71. data/lib/action_dispatch/journey/router.rb +1 -1
  72. data/lib/action_dispatch/journey/routes.rb +3 -3
  73. data/lib/action_dispatch/journey/visualizer/fsm.js +49 -24
  74. data/lib/action_dispatch/journey/visualizer/index.html.erb +1 -1
  75. data/lib/action_dispatch/middleware/actionable_exceptions.rb +0 -1
  76. data/lib/action_dispatch/middleware/cookies.rb +20 -13
  77. data/lib/action_dispatch/middleware/debug_exceptions.rb +6 -4
  78. data/lib/action_dispatch/middleware/debug_locks.rb +3 -3
  79. data/lib/action_dispatch/middleware/exception_wrapper.rb +4 -0
  80. data/lib/action_dispatch/middleware/executor.rb +3 -0
  81. data/lib/action_dispatch/middleware/flash.rb +17 -18
  82. data/lib/action_dispatch/middleware/host_authorization.rb +13 -17
  83. data/lib/action_dispatch/middleware/remote_ip.rb +20 -8
  84. data/lib/action_dispatch/middleware/request_id.rb +3 -3
  85. data/lib/action_dispatch/middleware/server_timing.rb +76 -0
  86. data/lib/action_dispatch/middleware/session/abstract_store.rb +1 -1
  87. data/lib/action_dispatch/middleware/session/cookie_store.rb +9 -9
  88. data/lib/action_dispatch/middleware/show_exceptions.rb +17 -16
  89. data/lib/action_dispatch/middleware/stack.rb +27 -9
  90. data/lib/action_dispatch/middleware/static.rb +5 -9
  91. data/lib/action_dispatch/middleware/templates/rescues/_message_and_suggestions.html.erb +1 -1
  92. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb +4 -11
  93. data/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +2 -2
  94. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb +10 -5
  95. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.text.erb +7 -3
  96. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +4 -4
  97. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +3 -3
  98. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +28 -18
  99. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb +3 -3
  100. data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +3 -3
  101. data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +3 -3
  102. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +3 -3
  103. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +3 -3
  104. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +22 -22
  105. data/lib/action_dispatch/railtie.rb +8 -2
  106. data/lib/action_dispatch/request/session.rb +43 -13
  107. data/lib/action_dispatch/routing/inspector.rb +1 -1
  108. data/lib/action_dispatch/routing/mapper.rb +82 -83
  109. data/lib/action_dispatch/routing/redirection.rb +5 -2
  110. data/lib/action_dispatch/routing/route_set.rb +17 -7
  111. data/lib/action_dispatch/routing/routes_proxy.rb +1 -1
  112. data/lib/action_dispatch/routing/url_for.rb +24 -25
  113. data/lib/action_dispatch/routing.rb +5 -6
  114. data/lib/action_dispatch/system_test_case.rb +5 -5
  115. data/lib/action_dispatch/system_testing/browser.rb +3 -13
  116. data/lib/action_dispatch/system_testing/driver.rb +34 -10
  117. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +11 -7
  118. data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +0 -8
  119. data/lib/action_dispatch/testing/assertions/response.rb +1 -1
  120. data/lib/action_dispatch/testing/assertions/routing.rb +3 -2
  121. data/lib/action_dispatch/testing/assertions.rb +2 -5
  122. data/lib/action_dispatch/testing/integration.rb +6 -8
  123. data/lib/action_dispatch/testing/test_process.rb +3 -29
  124. data/lib/action_dispatch/testing/test_response.rb +20 -2
  125. data/lib/action_dispatch.rb +1 -0
  126. data/lib/action_pack/gem_version.rb +5 -5
  127. data/lib/action_pack/version.rb +1 -1
  128. metadata +16 -15
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "action_dispatch/http/request"
4
3
  require "action_dispatch/middleware/exception_wrapper"
5
4
  require "action_dispatch/routing/inspector"
6
5
 
@@ -135,6 +134,7 @@ module ActionDispatch
135
134
  logger = logger(request)
136
135
 
137
136
  return unless logger
137
+ return if !log_rescued_responses?(request) && wrapper.rescue_response?
138
138
 
139
139
  exception = wrapper.exception
140
140
  trace = wrapper.exception_trace
@@ -149,9 +149,7 @@ module ActionDispatch
149
149
  log_array(logger, message)
150
150
  end
151
151
 
152
- def log_array(logger, array)
153
- lines = Array(array)
154
-
152
+ def log_array(logger, lines)
155
153
  return if lines.empty?
156
154
 
157
155
  if logger.formatter && logger.formatter.respond_to?(:tags_text)
@@ -178,5 +176,9 @@ module ActionDispatch
178
176
  def api_request?(content_type)
179
177
  @response_format == :api && !content_type.html?
180
178
  end
179
+
180
+ def log_rescued_responses?(request)
181
+ request.get_header("action_dispatch.log_rescued_responses")
182
+ end
181
183
  end
182
184
  end
@@ -9,9 +9,9 @@ module ActionDispatch
9
9
  # config.middleware.insert_before Rack::Sendfile, ActionDispatch::DebugLocks
10
10
  #
11
11
  # After restarting the application and re-triggering the deadlock condition,
12
- # <tt>/rails/locks</tt> will show a summary of all threads currently known to
13
- # the interlock, which lock level they are holding or awaiting, and their
14
- # current backtrace.
12
+ # the route <tt>/rails/locks</tt> will show a summary of all threads currently
13
+ # known to the interlock, which lock level they are holding or awaiting, and
14
+ # their current backtrace.
15
15
  #
16
16
  # Generally a deadlock will be caused by the interlock conflicting with some
17
17
  # other external lock or blocking I/O call. These cannot be automatically
@@ -118,6 +118,10 @@ module ActionDispatch
118
118
  Rack::Utils.status_code(@@rescue_responses[class_name])
119
119
  end
120
120
 
121
+ def rescue_response?
122
+ @@rescue_responses.key?(exception.class.name)
123
+ end
124
+
121
125
  def source_extracts
122
126
  backtrace.map do |trace|
123
127
  file, line_number = extract_file_and_line_number(trace)
@@ -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
@@ -20,10 +20,11 @@ module ActionDispatch
20
20
  # end
21
21
  # end
22
22
  #
23
- # show.html.erb
24
- # <% if flash[:notice] %>
25
- # <div class="notice"><%= flash[:notice] %></div>
26
- # <% end %>
23
+ # Then in +show.html.erb+:
24
+ #
25
+ # <% if flash[:notice] %>
26
+ # <div class="notice"><%= flash[:notice] %></div>
27
+ # <% end %>
27
28
  #
28
29
  # Since the +notice+ and +alert+ keys are a common idiom, convenience accessors are available:
29
30
  #
@@ -41,9 +42,9 @@ module ActionDispatch
41
42
  KEY = "action_dispatch.request.flash_hash"
42
43
 
43
44
  module RequestMethods
44
- # Access the contents of the flash. Use <tt>flash["notice"]</tt> to
45
- # read a notice you put there or <tt>flash["notice"] = "hello"</tt>
46
- # to put a new one.
45
+ # Access the contents of the flash. Returns a ActionDispatch::Flash::FlashHash.
46
+ #
47
+ # See ActionDispatch::Flash for example usage.
47
48
  def flash
48
49
  flash = flash_hash
49
50
  return flash if flash
@@ -59,16 +60,14 @@ module ActionDispatch
59
60
  end
60
61
 
61
62
  def commit_flash # :nodoc:
62
- session = self.session || {}
63
- flash_hash = self.flash_hash
63
+ return unless session.enabled?
64
64
 
65
65
  if flash_hash && (flash_hash.present? || session.key?("flash"))
66
66
  session["flash"] = flash_hash.to_session_value
67
67
  self.flash = flash_hash.dup
68
68
  end
69
69
 
70
- if (!session.respond_to?(:loaded?) || session.loaded?) && # reset_session uses {}, which doesn't implement #loaded?
71
- session.key?("flash") && session["flash"].nil?
70
+ if session.loaded? && session.key?("flash") && session["flash"].nil?
72
71
  session.delete("flash")
73
72
  end
74
73
  end
@@ -79,7 +78,7 @@ module ActionDispatch
79
78
  end
80
79
  end
81
80
 
82
- class FlashNow #:nodoc:
81
+ class FlashNow # :nodoc:
83
82
  attr_accessor :flash
84
83
 
85
84
  def initialize(flash)
@@ -111,7 +110,7 @@ module ActionDispatch
111
110
  class FlashHash
112
111
  include Enumerable
113
112
 
114
- def self.from_session_value(value) #:nodoc:
113
+ def self.from_session_value(value) # :nodoc:
115
114
  case value
116
115
  when FlashHash # Rails 3.1, 3.2
117
116
  flashes = value.instance_variable_get(:@flashes)
@@ -132,13 +131,13 @@ module ActionDispatch
132
131
 
133
132
  # Builds a hash containing the flashes to keep for the next request.
134
133
  # If there are none to keep, returns +nil+.
135
- def to_session_value #:nodoc:
134
+ def to_session_value # :nodoc:
136
135
  flashes_to_keep = @flashes.except(*@discard)
137
136
  return nil if flashes_to_keep.empty?
138
137
  { "discard" => [], "flashes" => flashes_to_keep }
139
138
  end
140
139
 
141
- def initialize(flashes = {}, discard = []) #:nodoc:
140
+ def initialize(flashes = {}, discard = []) # :nodoc:
142
141
  @discard = Set.new(stringify_array(discard))
143
142
  @flashes = flashes.stringify_keys
144
143
  @now = nil
@@ -162,7 +161,7 @@ module ActionDispatch
162
161
  @flashes[k.to_s]
163
162
  end
164
163
 
165
- def update(h) #:nodoc:
164
+ def update(h) # :nodoc:
166
165
  @discard.subtract stringify_array(h.keys)
167
166
  @flashes.update h.stringify_keys
168
167
  self
@@ -202,7 +201,7 @@ module ActionDispatch
202
201
 
203
202
  alias :merge! :update
204
203
 
205
- def replace(h) #:nodoc:
204
+ def replace(h) # :nodoc:
206
205
  @discard.clear
207
206
  @flashes.replace h.stringify_keys
208
207
  self
@@ -253,7 +252,7 @@ module ActionDispatch
253
252
  # Mark for removal entries that were kept, and delete unkept ones.
254
253
  #
255
254
  # This method is called automatically by filters, so you generally don't need to care about it.
256
- def sweep #:nodoc:
255
+ def sweep # :nodoc:
257
256
  @discard.each { |k| @flashes.delete k }
258
257
  @discard.replace @flashes.keys
259
258
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "action_dispatch/http/request"
4
-
5
3
  module ActionDispatch
6
4
  # This middleware guards from DNS rebinding attacks by explicitly permitting
7
5
  # the hosts a request can be sent to, and is passed the options set in
@@ -97,7 +95,7 @@ module ActionDispatch
97
95
  def response_body(request)
98
96
  return "" unless request.get_header("action_dispatch.show_detailed_exceptions")
99
97
 
100
- template = DebugView.new(host: request.host)
98
+ template = DebugView.new(hosts: request.env["action_dispatch.blocked_hosts"])
101
99
  template.render(template: "rescues/blocked_host", layout: "rescues/layout")
102
100
  end
103
101
 
@@ -113,7 +111,7 @@ module ActionDispatch
113
111
 
114
112
  return unless logger
115
113
 
116
- logger.error("[#{self.class.name}] Blocked host: #{request.host}")
114
+ logger.error("[#{self.class.name}] Blocked hosts: #{request.env["action_dispatch.blocked_hosts"].join(", ")}")
117
115
  end
118
116
 
119
117
  def available_logger(request)
@@ -121,20 +119,11 @@ module ActionDispatch
121
119
  end
122
120
  end
123
121
 
124
- def initialize(app, hosts, deprecated_response_app = nil, exclude: nil, response_app: nil)
122
+ def initialize(app, hosts, exclude: nil, response_app: nil)
125
123
  @app = app
126
124
  @permissions = Permissions.new(hosts)
127
125
  @exclude = exclude
128
126
 
129
- unless deprecated_response_app.nil?
130
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
131
- `action_dispatch.hosts_response_app` is deprecated and will be ignored in Rails 7.0.
132
- Use the Host Authorization `response_app` setting instead.
133
- MSG
134
-
135
- response_app ||= deprecated_response_app
136
- end
137
-
138
127
  @response_app = response_app || DefaultResponseApp.new
139
128
  end
140
129
 
@@ -142,21 +131,28 @@ module ActionDispatch
142
131
  return @app.call(env) if @permissions.empty?
143
132
 
144
133
  request = Request.new(env)
134
+ hosts = blocked_hosts(request)
145
135
 
146
- if authorized?(request) || excluded?(request)
136
+ if hosts.empty? || excluded?(request)
147
137
  mark_as_authorized(request)
148
138
  @app.call(env)
149
139
  else
140
+ env["action_dispatch.blocked_hosts"] = hosts
150
141
  @response_app.call(env)
151
142
  end
152
143
  end
153
144
 
154
145
  private
155
- def authorized?(request)
146
+ def blocked_hosts(request)
147
+ hosts = []
148
+
156
149
  origin_host = request.get_header("HTTP_HOST")
150
+ hosts << origin_host unless @permissions.allows?(origin_host)
151
+
157
152
  forwarded_host = request.x_forwarded_host&.split(/,\s?/)&.last
153
+ hosts << forwarded_host unless forwarded_host.blank? || @permissions.allows?(forwarded_host)
158
154
 
159
- @permissions.allows?(origin_host) && (forwarded_host.blank? || @permissions.allows?(forwarded_host))
155
+ hosts
160
156
  end
161
157
 
162
158
  def excluded?(request)
@@ -22,7 +22,7 @@ module ActionDispatch
22
22
  # This middleware assumes that there is at least one proxy sitting around
23
23
  # and setting headers with the client's remote IP address. If you don't use
24
24
  # a proxy, because you are hosted on e.g. Heroku without SSL, any client can
25
- # claim to have any IP address by setting the X-Forwarded-For header. If you
25
+ # claim to have any IP address by setting the +X-Forwarded-For+ header. If you
26
26
  # care about that, then you need to explicitly drop or ignore those headers
27
27
  # sometime before this middleware runs.
28
28
  class RemoteIp
@@ -51,11 +51,9 @@ module ActionDispatch
51
51
  # clients (like WAP devices), or behind proxies that set headers in an
52
52
  # incorrect or confusing way (like AWS ELB).
53
53
  #
54
- # The +custom_proxies+ argument can take an Array of string, IPAddr, or
55
- # Regexp objects which will be used instead of +TRUSTED_PROXIES+. If a
56
- # single string, IPAddr, or Regexp object is provided, it will be used in
57
- # addition to +TRUSTED_PROXIES+. Any proxy setup will put the value you
58
- # want in the middle (or at the beginning) of the X-Forwarded-For list,
54
+ # The +custom_proxies+ argument can take an enumerable which will be used
55
+ # instead of +TRUSTED_PROXIES+. Any proxy setup will put the value you
56
+ # want in the middle (or at the beginning) of the +X-Forwarded-For+ list,
59
57
  # with your proxy servers after it. If your proxies aren't removed, pass
60
58
  # them in via the +custom_proxies+ parameter. That way, the middleware will
61
59
  # ignore those IP addresses, and return the one that you want.
@@ -67,6 +65,20 @@ module ActionDispatch
67
65
  elsif custom_proxies.respond_to?(:any?)
68
66
  custom_proxies
69
67
  else
68
+ ActiveSupport::Deprecation.warn(<<~EOM)
69
+ Setting config.action_dispatch.trusted_proxies to a single value has
70
+ been deprecated. Please set this to an enumerable instead. For
71
+ example, instead of:
72
+
73
+ config.action_dispatch.trusted_proxies = IPAddr.new("10.0.0.0/8")
74
+
75
+ Wrap the value in an Array:
76
+
77
+ config.action_dispatch.trusted_proxies = [IPAddr.new("10.0.0.0/8")]
78
+
79
+ Note that unlike passing a single argument, passing an enumerable
80
+ will *replace* the default set of trusted proxies.
81
+ EOM
70
82
  Array(custom_proxies) + TRUSTED_PROXIES
71
83
  end
72
84
  end
@@ -98,9 +110,9 @@ module ActionDispatch
98
110
  # REMOTE_ADDR will be correct if the request is made directly against the
99
111
  # Ruby process, on e.g. Heroku. When the request is proxied by another
100
112
  # server like HAProxy or NGINX, the IP address that made the original
101
- # request will be put in an X-Forwarded-For header. If there are multiple
113
+ # request will be put in an +X-Forwarded-For+ header. If there are multiple
102
114
  # proxies, that header may contain a list of IPs. Other proxy services
103
- # set the Client-Ip header instead, so we check that too.
115
+ # set the +Client-Ip+ header instead, so we check that too.
104
116
  #
105
117
  # As discussed in {this post about Rails IP Spoofing}[https://blog.gingerlime.com/2012/rails-ip-spoofing-vulnerabilities-and-protection/],
106
118
  # while the first IP in the list is likely to be the "originating" IP,
@@ -5,10 +5,10 @@ require "active_support/core_ext/string/access"
5
5
 
6
6
  module ActionDispatch
7
7
  # Makes a unique request id available to the +action_dispatch.request_id+ env variable (which is then accessible
8
- # through <tt>ActionDispatch::Request#request_id</tt> or the alias <tt>ActionDispatch::Request#uuid</tt>) and sends
9
- # the same id to the client via the X-Request-Id header.
8
+ # through ActionDispatch::Request#request_id or the alias ActionDispatch::Request#uuid) and sends
9
+ # the same id to the client via the +X-Request-Id+ header.
10
10
  #
11
- # The unique request id is either based on the X-Request-Id header in the request, which would typically be generated
11
+ # The unique request id is either based on the +X-Request-Id+ header in the request, which would typically be generated
12
12
  # by a firewall, load balancer, or the web server, or, if this header is not available, a random uuid. If the
13
13
  # header is accepted from the outside world, we sanitize it to a max of 255 chars and alphanumeric and dashes only.
14
14
  #
@@ -0,0 +1,76 @@
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
+ class Subscriber # :nodoc:
10
+ include Singleton
11
+ KEY = :action_dispatch_server_timing_events
12
+
13
+ def initialize
14
+ @mutex = Mutex.new
15
+ end
16
+
17
+ def call(event)
18
+ if events = ActiveSupport::IsolatedExecutionState[KEY]
19
+ events << event
20
+ end
21
+ end
22
+
23
+ def collect_events
24
+ events = []
25
+ ActiveSupport::IsolatedExecutionState[KEY] = events
26
+ yield
27
+ events
28
+ ensure
29
+ ActiveSupport::IsolatedExecutionState.delete(KEY)
30
+ end
31
+
32
+ def ensure_subscribed
33
+ @mutex.synchronize do
34
+ # Subscribe to all events, except those beginning with "!"
35
+ # Ideally we would be more selective of what is being measured
36
+ @subscriber ||= ActiveSupport::Notifications.subscribe(/\A[^!]/, self)
37
+ end
38
+ end
39
+
40
+ def unsubscribe
41
+ @mutex.synchronize do
42
+ ActiveSupport::Notifications.unsubscribe @subscriber
43
+ @subscriber = nil
44
+ end
45
+ end
46
+ end
47
+
48
+ def self.unsubscribe # :nodoc:
49
+ Subscriber.instance.unsubscribe
50
+ end
51
+
52
+ def initialize(app)
53
+ @app = app
54
+ @subscriber = Subscriber.instance
55
+ @subscriber.ensure_subscribed
56
+ end
57
+
58
+ def call(env)
59
+ response = nil
60
+ events = @subscriber.collect_events do
61
+ response = @app.call(env)
62
+ end
63
+
64
+ headers = response[1]
65
+
66
+ header_info = events.group_by(&:name).map do |event_name, events_collection|
67
+ "%s;dur=%.2f" % [event_name, events_collection.sum(&:duration)]
68
+ end
69
+
70
+ header_info.prepend(headers[SERVER_TIMING_HEADER]) if headers[SERVER_TIMING_HEADER].present?
71
+ headers[SERVER_TIMING_HEADER] = header_info.join(", ")
72
+
73
+ response
74
+ end
75
+ end
76
+ end
@@ -8,7 +8,7 @@ require "action_dispatch/request/session"
8
8
 
9
9
  module ActionDispatch
10
10
  module Session
11
- class SessionRestoreError < StandardError #:nodoc:
11
+ class SessionRestoreError < StandardError # :nodoc:
12
12
  def initialize
13
13
  super("Session contains objects whose class definition isn't available.\n" \
14
14
  "Remember to require the classes for all objects kept in the session.\n" \
@@ -9,14 +9,14 @@ module ActionDispatch
9
9
  # This cookie-based session store is the Rails default. It is
10
10
  # dramatically faster than the alternatives.
11
11
  #
12
- # Sessions typically contain at most a user_id and flash message; both fit
13
- # within the 4096 bytes cookie size limit. A CookieOverflow exception is raised if
12
+ # Sessions typically contain at most a user ID and flash message; both fit
13
+ # within the 4096 bytes cookie size limit. A +CookieOverflow+ exception is raised if
14
14
  # you attempt to store more than 4096 bytes of data.
15
15
  #
16
16
  # The cookie jar used for storage is automatically configured to be the
17
17
  # best possible option given your application's configuration.
18
18
  #
19
- # Your cookies will be encrypted using your apps secret_key_base. This
19
+ # Your cookies will be encrypted using your application's +secret_key_base+. This
20
20
  # goes a step further than signed cookies in that encrypted cookies cannot
21
21
  # be altered or read by users. This is the default starting in Rails 4.
22
22
  #
@@ -24,28 +24,28 @@ module ActionDispatch
24
24
  #
25
25
  # Rails.application.config.session_store :cookie_store, key: '_your_app_session'
26
26
  #
27
- # In the development and test environments your application's secret key base is
27
+ # In the development and test environments your application's +secret_key_base+ is
28
28
  # generated by Rails and stored in a temporary file in <tt>tmp/development_secret.txt</tt>.
29
29
  # In all other environments, it is stored encrypted in the
30
30
  # <tt>config/credentials.yml.enc</tt> file.
31
31
  #
32
- # If your application was not updated to Rails 5.2 defaults, the secret_key_base
32
+ # If your application was not updated to Rails 5.2 defaults, the +secret_key_base+
33
33
  # will be found in the old <tt>config/secrets.yml</tt> file.
34
34
  #
35
- # Note that changing your secret_key_base will invalidate all existing session.
35
+ # Note that changing your +secret_key_base+ will invalidate all existing session.
36
36
  # Additionally, you should take care to make sure you are not relying on the
37
37
  # ability to decode signed cookies generated by your app in external
38
38
  # applications or JavaScript before changing it.
39
39
  #
40
- # Because CookieStore extends Rack::Session::Abstract::Persisted, many of the
40
+ # Because CookieStore extends +Rack::Session::Abstract::Persisted+, many of the
41
41
  # options described there can be used to customize the session cookie that
42
42
  # is generated. For example:
43
43
  #
44
44
  # Rails.application.config.session_store :cookie_store, expire_after: 14.days
45
45
  #
46
46
  # would set the session cookie to expire automatically 14 days after creation.
47
- # Other useful options include <tt>:key</tt>, <tt>:secure</tt> and
48
- # <tt>:httponly</tt>.
47
+ # Other useful options include <tt>:key</tt>, <tt>:secure</tt>,
48
+ # <tt>:httponly</tt>, and <tt>:same_site</tt>.
49
49
  class CookieStore < AbstractSecureStore
50
50
  class SessionId < DelegateClass(Rack::Session::SessionId)
51
51
  attr_reader :cookie_value
@@ -1,28 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "action_dispatch/http/request"
4
3
  require "action_dispatch/middleware/exception_wrapper"
5
4
 
6
5
  module ActionDispatch
7
6
  # This middleware rescues any exception returned by the application
8
7
  # and calls an exceptions app that will wrap it in a format for the end user.
9
8
  #
10
- # The exceptions app should be passed as parameter on initialization
11
- # of ShowExceptions. Every time there is an exception, ShowExceptions will
12
- # store the exception in env["action_dispatch.exception"], rewrite the
13
- # PATH_INFO to the exception status code and call the Rack app.
9
+ # The exceptions app should be passed as a parameter on initialization of
10
+ # +ShowExceptions+. Every time there is an exception, +ShowExceptions+ will
11
+ # store the exception in <tt>env["action_dispatch.exception"]</tt>, rewrite
12
+ # the +PATH_INFO+ to the exception status code and call the Rack app.
14
13
  #
15
- # If the application returns a "X-Cascade" pass response, this middleware
16
- # will send an empty response as result with the correct status code.
17
- # If any exception happens inside the exceptions app, this middleware
18
- # catches the exceptions and returns a FAILSAFE_RESPONSE.
14
+ # In \Rails applications, the exceptions app can be configured with
15
+ # +config.exceptions_app+, which defaults to ActionDispatch::PublicExceptions.
16
+ #
17
+ # If the application returns an <tt>"X-Cascade" => "pass"</tt> response, this
18
+ # middleware will send an empty response as a result with the correct status
19
+ # code. If any exception happens inside the exceptions app, this middleware
20
+ # catches the exceptions and returns a failsafe response.
19
21
  class ShowExceptions
20
- FAILSAFE_RESPONSE = [500, { "Content-Type" => "text/plain" },
21
- ["500 Internal Server Error\n" \
22
- "If you are the administrator of this website, then please read this web " \
23
- "application's log file and/or the web server's log file to find out what " \
24
- "went wrong."]]
25
-
26
22
  def initialize(app, exceptions_app)
27
23
  @app = app
28
24
  @exceptions_app = exceptions_app
@@ -54,7 +50,12 @@ module ActionDispatch
54
50
  response[1]["X-Cascade"] == "pass" ? pass_response(status) : response
55
51
  rescue Exception => failsafe_error
56
52
  $stderr.puts "Error during failsafe response: #{failsafe_error}\n #{failsafe_error.backtrace * "\n "}"
57
- FAILSAFE_RESPONSE
53
+
54
+ [500, { "Content-Type" => "text/plain" },
55
+ ["500 Internal Server Error\n" \
56
+ "If you are the administrator of this website, then please read this web " \
57
+ "application's log file and/or the web server's log file to find out what " \
58
+ "went wrong."]]
58
59
  end
59
60
 
60
61
  def fallback_to_html_format_if_invalid_mime_type(request)
@@ -72,8 +72,8 @@ module ActionDispatch
72
72
  yield(self) if block_given?
73
73
  end
74
74
 
75
- def each
76
- @middlewares.each { |x| yield x }
75
+ def each(&block)
76
+ @middlewares.each(&block)
77
77
  end
78
78
 
79
79
  def size
@@ -91,7 +91,7 @@ module ActionDispatch
91
91
  def unshift(klass, *args, &block)
92
92
  middlewares.unshift(build_middleware(klass, args, block))
93
93
  end
94
- ruby2_keywords(:unshift) if respond_to?(:ruby2_keywords, true)
94
+ ruby2_keywords(:unshift)
95
95
 
96
96
  def initialize_copy(other)
97
97
  self.middlewares = other.middlewares.dup
@@ -101,7 +101,7 @@ module ActionDispatch
101
101
  index = assert_index(index, :before)
102
102
  middlewares.insert(index, build_middleware(klass, args, block))
103
103
  end
104
- ruby2_keywords(:insert) if respond_to?(:ruby2_keywords, true)
104
+ ruby2_keywords(:insert)
105
105
 
106
106
  alias_method :insert_before, :insert
107
107
 
@@ -109,17 +109,29 @@ module ActionDispatch
109
109
  index = assert_index(index, :after)
110
110
  insert(index + 1, *args, &block)
111
111
  end
112
- ruby2_keywords(:insert_after) if respond_to?(:ruby2_keywords, true)
112
+ ruby2_keywords(:insert_after)
113
113
 
114
114
  def swap(target, *args, &block)
115
115
  index = assert_index(target, :before)
116
116
  insert(index, *args, &block)
117
117
  middlewares.delete_at(index + 1)
118
118
  end
119
- ruby2_keywords(:swap) if respond_to?(:ruby2_keywords, true)
119
+ ruby2_keywords(:swap)
120
120
 
121
+ # Deletes a middleware from the middleware stack.
122
+ #
123
+ # Returns the array of middlewares not including the deleted item, or
124
+ # returns nil if the target is not found.
121
125
  def delete(target)
122
- middlewares.delete_if { |m| m.klass == target }
126
+ middlewares.reject! { |m| m.name == target.name }
127
+ end
128
+
129
+ # Deletes a middleware from the middleware stack.
130
+ #
131
+ # Returns the array of middlewares not including the deleted item, or
132
+ # raises +RuntimeError+ if the target is not found.
133
+ def delete!(target)
134
+ delete(target) || (raise "No such middleware to remove: #{target.inspect}")
123
135
  end
124
136
 
125
137
  def move(target, source)
@@ -143,7 +155,7 @@ module ActionDispatch
143
155
  def use(klass, *args, &block)
144
156
  middlewares.push(build_middleware(klass, args, block))
145
157
  end
146
- ruby2_keywords(:use) if respond_to?(:ruby2_keywords, true)
158
+ ruby2_keywords(:use)
147
159
 
148
160
  def build(app = nil, &block)
149
161
  instrumenting = ActiveSupport::Notifications.notifier.listening?(InstrumentationProxy::EVENT_NAME)
@@ -158,7 +170,7 @@ module ActionDispatch
158
170
 
159
171
  private
160
172
  def assert_index(index, where)
161
- i = index.is_a?(Integer) ? index : middlewares.index { |m| m.klass == index }
173
+ i = index.is_a?(Integer) ? index : index_of(index)
162
174
  raise "No such middleware to insert #{where}: #{index.inspect}" unless i
163
175
  i
164
176
  end
@@ -166,5 +178,11 @@ module ActionDispatch
166
178
  def build_middleware(klass, args, block)
167
179
  Middleware.new(klass, args, block)
168
180
  end
181
+
182
+ def index_of(klass)
183
+ middlewares.index do |m|
184
+ m.name == klass.name
185
+ end
186
+ end
169
187
  end
170
188
  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.
@@ -25,7 +24,7 @@ module ActionDispatch
25
24
  end
26
25
  end
27
26
 
28
- # This endpoint serves static files from disk using Rack::File.
27
+ # This endpoint serves static files from disk using +Rack::File+.
29
28
  #
30
29
  # URL paths are matched with static files according to expected
31
30
  # conventions: +path+, +path+.html, +path+/index.html.
@@ -34,13 +33,13 @@ module ActionDispatch
34
33
  # and gzip (.gz) files are supported. If +path+.br exists, this
35
34
  # endpoint returns that file with a <tt>Content-Encoding: br</tt> header.
36
35
  #
37
- # If no matching file is found, this endpoint responds 404 Not Found.
36
+ # If no matching file is found, this endpoint responds <tt>404 Not Found</tt>.
38
37
  #
39
38
  # Pass the +root+ directory to search for matching files, an optional
40
39
  # <tt>index: "index"</tt> to change the default +path+/index.html, and optional
41
40
  # additional response headers.
42
41
  class FileHandler
43
- # Accept-Encoding value -> file extension
42
+ # +Accept-Encoding+ value -> file extension
44
43
  PRECOMPRESSED = {
45
44
  "br" => ".br",
46
45
  "gzip" => ".gz",
@@ -137,11 +136,8 @@ module ActionDispatch
137
136
  end
138
137
 
139
138
  def file_readable?(path)
140
- file_stat = File.stat(File.join(@root, path.b))
141
- rescue SystemCallError
142
- false
143
- else
144
- file_stat.file? && file_stat.readable?
139
+ file_path = File.join(@root, path.b)
140
+ File.file?(file_path) && File.readable?(file_path)
145
141
  end
146
142
 
147
143
  def compressible?(content_type)