actionpack 6.1.7.5 → 7.0.8.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (128) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +323 -399
  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 +27 -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 +95 -22
  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)