actionpack 5.2.1 → 7.0.2.4

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 (167) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +264 -220
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +6 -6
  5. data/lib/abstract_controller/asset_paths.rb +1 -1
  6. data/lib/abstract_controller/base.rb +24 -4
  7. data/lib/abstract_controller/caching/fragments.rb +8 -24
  8. data/lib/abstract_controller/caching.rb +2 -2
  9. data/lib/abstract_controller/callbacks.rb +34 -8
  10. data/lib/abstract_controller/collector.rb +5 -4
  11. data/lib/abstract_controller/error.rb +1 -1
  12. data/lib/abstract_controller/helpers.rb +107 -90
  13. data/lib/abstract_controller/logger.rb +1 -1
  14. data/lib/abstract_controller/railties/routes_helpers.rb +19 -1
  15. data/lib/abstract_controller/rendering.rb +9 -9
  16. data/lib/abstract_controller/translation.rb +12 -5
  17. data/lib/abstract_controller/url_for.rb +4 -6
  18. data/lib/abstract_controller.rb +2 -0
  19. data/lib/action_controller/api.rb +5 -4
  20. data/lib/action_controller/base.rb +6 -9
  21. data/lib/action_controller/caching.rb +1 -3
  22. data/lib/action_controller/log_subscriber.rb +13 -9
  23. data/lib/action_controller/metal/basic_implicit_render.rb +1 -1
  24. data/lib/action_controller/metal/conditional_get.rb +57 -6
  25. data/lib/action_controller/metal/content_security_policy.rb +2 -3
  26. data/lib/action_controller/metal/cookies.rb +4 -2
  27. data/lib/action_controller/metal/data_streaming.rb +9 -18
  28. data/lib/action_controller/metal/default_headers.rb +17 -0
  29. data/lib/action_controller/metal/etag_with_template_digest.rb +4 -6
  30. data/lib/action_controller/metal/exceptions.rb +55 -12
  31. data/lib/action_controller/metal/flash.rb +10 -6
  32. data/lib/action_controller/metal/head.rb +7 -4
  33. data/lib/action_controller/metal/helpers.rb +15 -6
  34. data/lib/action_controller/metal/http_authentication.rb +41 -39
  35. data/lib/action_controller/metal/implicit_render.rb +5 -15
  36. data/lib/action_controller/metal/instrumentation.rb +59 -55
  37. data/lib/action_controller/metal/live.rb +80 -33
  38. data/lib/action_controller/metal/logging.rb +20 -0
  39. data/lib/action_controller/metal/mime_responds.rb +22 -7
  40. data/lib/action_controller/metal/parameter_encoding.rb +35 -4
  41. data/lib/action_controller/metal/params_wrapper.rb +50 -31
  42. data/lib/action_controller/metal/permissions_policy.rb +46 -0
  43. data/lib/action_controller/metal/redirecting.rb +93 -23
  44. data/lib/action_controller/metal/renderers.rb +4 -4
  45. data/lib/action_controller/metal/rendering.rb +14 -9
  46. data/lib/action_controller/metal/request_forgery_protection.rb +160 -58
  47. data/lib/action_controller/metal/rescue.rb +2 -2
  48. data/lib/action_controller/metal/streaming.rb +1 -4
  49. data/lib/action_controller/metal/strong_parameters.rb +236 -88
  50. data/lib/action_controller/metal/testing.rb +9 -2
  51. data/lib/action_controller/metal/url_for.rb +1 -1
  52. data/lib/action_controller/metal.rb +16 -17
  53. data/lib/action_controller/railtie.rb +49 -6
  54. data/lib/action_controller/railties/helpers.rb +1 -1
  55. data/lib/action_controller/renderer.rb +37 -13
  56. data/lib/action_controller/template_assertions.rb +1 -1
  57. data/lib/action_controller/test_case.rb +98 -68
  58. data/lib/action_controller.rb +4 -5
  59. data/lib/action_dispatch/http/cache.rb +45 -32
  60. data/lib/action_dispatch/http/content_disposition.rb +45 -0
  61. data/lib/action_dispatch/http/content_security_policy.rb +69 -56
  62. data/lib/action_dispatch/http/filter_parameters.rb +14 -8
  63. data/lib/action_dispatch/http/filter_redirect.rb +2 -3
  64. data/lib/action_dispatch/http/headers.rb +4 -4
  65. data/lib/action_dispatch/http/mime_negotiation.rb +44 -16
  66. data/lib/action_dispatch/http/mime_type.rb +47 -30
  67. data/lib/action_dispatch/http/parameters.rb +18 -27
  68. data/lib/action_dispatch/http/permissions_policy.rb +173 -0
  69. data/lib/action_dispatch/http/request.rb +49 -35
  70. data/lib/action_dispatch/http/response.rb +34 -26
  71. data/lib/action_dispatch/http/upload.rb +9 -1
  72. data/lib/action_dispatch/http/url.rb +86 -94
  73. data/lib/action_dispatch/journey/formatter.rb +55 -31
  74. data/lib/action_dispatch/journey/gtg/builder.rb +30 -46
  75. data/lib/action_dispatch/journey/gtg/simulator.rb +15 -8
  76. data/lib/action_dispatch/journey/gtg/transition_table.rb +78 -21
  77. data/lib/action_dispatch/journey/nfa/dot.rb +0 -11
  78. data/lib/action_dispatch/journey/nodes/node.rb +83 -16
  79. data/lib/action_dispatch/journey/parser.rb +13 -13
  80. data/lib/action_dispatch/journey/parser.y +1 -1
  81. data/lib/action_dispatch/journey/path/pattern.rb +42 -34
  82. data/lib/action_dispatch/journey/route.rb +14 -31
  83. data/lib/action_dispatch/journey/router/utils.rb +16 -14
  84. data/lib/action_dispatch/journey/router.rb +27 -35
  85. data/lib/action_dispatch/journey/routes.rb +3 -5
  86. data/lib/action_dispatch/journey/scanner.rb +10 -4
  87. data/lib/action_dispatch/journey/visitors.rb +1 -4
  88. data/lib/action_dispatch/journey/visualizer/fsm.js +49 -24
  89. data/lib/action_dispatch/journey/visualizer/index.html.erb +1 -1
  90. data/lib/action_dispatch/journey.rb +0 -2
  91. data/lib/action_dispatch/middleware/actionable_exceptions.rb +45 -0
  92. data/lib/action_dispatch/middleware/callbacks.rb +2 -4
  93. data/lib/action_dispatch/middleware/cookies.rb +136 -113
  94. data/lib/action_dispatch/middleware/debug_exceptions.rb +47 -68
  95. data/lib/action_dispatch/middleware/debug_locks.rb +8 -8
  96. data/lib/action_dispatch/middleware/debug_view.rb +66 -0
  97. data/lib/action_dispatch/middleware/exception_wrapper.rb +79 -30
  98. data/lib/action_dispatch/middleware/executor.rb +4 -1
  99. data/lib/action_dispatch/middleware/flash.rb +10 -12
  100. data/lib/action_dispatch/middleware/host_authorization.rb +159 -0
  101. data/lib/action_dispatch/middleware/public_exceptions.rb +6 -3
  102. data/lib/action_dispatch/middleware/remote_ip.rb +30 -20
  103. data/lib/action_dispatch/middleware/request_id.rb +5 -6
  104. data/lib/action_dispatch/middleware/server_timing.rb +33 -0
  105. data/lib/action_dispatch/middleware/session/abstract_store.rb +16 -3
  106. data/lib/action_dispatch/middleware/session/cache_store.rb +11 -6
  107. data/lib/action_dispatch/middleware/session/cookie_store.rb +24 -19
  108. data/lib/action_dispatch/middleware/show_exceptions.rb +20 -11
  109. data/lib/action_dispatch/middleware/ssl.rb +20 -15
  110. data/lib/action_dispatch/middleware/stack.rb +79 -7
  111. data/lib/action_dispatch/middleware/static.rb +150 -94
  112. data/lib/action_dispatch/middleware/templates/rescues/_actions.html.erb +13 -0
  113. data/lib/action_dispatch/middleware/templates/rescues/_actions.text.erb +0 -0
  114. data/lib/action_dispatch/middleware/templates/rescues/_message_and_suggestions.html.erb +22 -0
  115. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb +6 -11
  116. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.text.erb +1 -1
  117. data/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +4 -2
  118. data/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +46 -36
  119. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb +8 -0
  120. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.text.erb +7 -0
  121. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +25 -6
  122. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb +1 -1
  123. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +9 -6
  124. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +4 -1
  125. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +121 -15
  126. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb +19 -0
  127. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.text.erb +3 -0
  128. data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +5 -5
  129. data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +4 -4
  130. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +5 -5
  131. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +4 -4
  132. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +16 -2
  133. data/lib/action_dispatch/railtie.rb +16 -4
  134. data/lib/action_dispatch/request/session.rb +59 -22
  135. data/lib/action_dispatch/request/utils.rb +28 -2
  136. data/lib/action_dispatch/routing/inspector.rb +102 -54
  137. data/lib/action_dispatch/routing/mapper.rb +184 -156
  138. data/lib/action_dispatch/routing/polymorphic_routes.rb +21 -19
  139. data/lib/action_dispatch/routing/redirection.rb +4 -6
  140. data/lib/action_dispatch/routing/route_set.rb +83 -73
  141. data/lib/action_dispatch/routing/routes_proxy.rb +1 -1
  142. data/lib/action_dispatch/routing/url_for.rb +2 -3
  143. data/lib/action_dispatch/routing.rb +23 -22
  144. data/lib/action_dispatch/system_test_case.rb +65 -16
  145. data/lib/action_dispatch/system_testing/browser.rb +43 -16
  146. data/lib/action_dispatch/system_testing/driver.rb +42 -10
  147. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +58 -12
  148. data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +3 -10
  149. data/lib/action_dispatch/testing/assertion_response.rb +0 -1
  150. data/lib/action_dispatch/testing/assertions/response.rb +4 -7
  151. data/lib/action_dispatch/testing/assertions/routing.rb +20 -8
  152. data/lib/action_dispatch/testing/assertions.rb +3 -6
  153. data/lib/action_dispatch/testing/integration.rb +61 -30
  154. data/lib/action_dispatch/testing/request_encoder.rb +2 -2
  155. data/lib/action_dispatch/testing/test_process.rb +8 -6
  156. data/lib/action_dispatch/testing/test_request.rb +3 -3
  157. data/lib/action_dispatch/testing/test_response.rb +4 -32
  158. data/lib/action_dispatch.rb +15 -7
  159. data/lib/action_pack/gem_version.rb +4 -4
  160. data/lib/action_pack.rb +1 -1
  161. metadata +44 -25
  162. data/lib/action_controller/metal/force_ssl.rb +0 -99
  163. data/lib/action_dispatch/http/parameter_filter.rb +0 -86
  164. data/lib/action_dispatch/journey/nfa/builder.rb +0 -78
  165. data/lib/action_dispatch/journey/nfa/simulator.rb +0 -49
  166. data/lib/action_dispatch/journey/nfa/transition_table.rb +0 -120
  167. data/lib/action_dispatch/system_testing/test_helpers/undef_methods.rb +0 -26
@@ -13,7 +13,7 @@ module ActionDispatch
13
13
  #
14
14
  # Requests can opt-out of redirection with +exclude+:
15
15
  #
16
- # config.ssl_options = { redirect: { exclude: -> request { request.path =~ /healthcheck/ } } }
16
+ # config.ssl_options = { redirect: { exclude: -> request { /healthcheck/.match?(request.path) } } }
17
17
  #
18
18
  # Cookies will not be flagged as secure for excluded requests.
19
19
  #
@@ -29,7 +29,7 @@ module ActionDispatch
29
29
  #
30
30
  # * +expires+: How long, in seconds, these settings will stick. The minimum
31
31
  # required to qualify for browser preload lists is 1 year. Defaults to
32
- # 1 year (recommended).
32
+ # 2 years (recommended).
33
33
  #
34
34
  # * +subdomains+: Set to +true+ to tell the browser to apply these settings
35
35
  # to all subdomains. This protects your cookies from interception by a
@@ -49,14 +49,16 @@ module ActionDispatch
49
49
  class SSL
50
50
  # :stopdoc:
51
51
 
52
- # Default to 1 year, the minimum for browser preload lists.
53
- HSTS_EXPIRES_IN = 31536000
52
+ # Default to 2 years as recommended on hstspreload.org.
53
+ HSTS_EXPIRES_IN = 63072000
54
+
55
+ PERMANENT_REDIRECT_REQUEST_METHODS = %w[GET HEAD] # :nodoc:
54
56
 
55
57
  def self.default_hsts_options
56
58
  { expires: HSTS_EXPIRES_IN, subdomains: true, preload: false }
57
59
  end
58
60
 
59
- def initialize(app, redirect: {}, hsts: {}, secure_cookies: true)
61
+ def initialize(app, redirect: {}, hsts: {}, secure_cookies: true, ssl_default_redirect_status: nil)
60
62
  @app = app
61
63
 
62
64
  @redirect = redirect
@@ -65,6 +67,7 @@ module ActionDispatch
65
67
  @secure_cookies = secure_cookies
66
68
 
67
69
  @hsts_header = build_hsts_header(normalize_hsts_options(hsts))
70
+ @ssl_default_redirect_status = ssl_default_redirect_status
68
71
  end
69
72
 
70
73
  def call(env)
@@ -83,7 +86,7 @@ module ActionDispatch
83
86
 
84
87
  private
85
88
  def set_hsts_header!(headers)
86
- headers["Strict-Transport-Security".freeze] ||= @hsts_header
89
+ headers["Strict-Transport-Security"] ||= @hsts_header
87
90
  end
88
91
 
89
92
  def normalize_hsts_options(options)
@@ -102,23 +105,23 @@ module ActionDispatch
102
105
 
103
106
  # https://tools.ietf.org/html/rfc6797#section-6.1
104
107
  def build_hsts_header(hsts)
105
- value = "max-age=#{hsts[:expires].to_i}".dup
108
+ value = +"max-age=#{hsts[:expires].to_i}"
106
109
  value << "; includeSubDomains" if hsts[:subdomains]
107
110
  value << "; preload" if hsts[:preload]
108
111
  value
109
112
  end
110
113
 
111
114
  def flag_cookies_as_secure!(headers)
112
- if cookies = headers["Set-Cookie".freeze]
113
- cookies = cookies.split("\n".freeze)
115
+ if cookies = headers["Set-Cookie"]
116
+ cookies = cookies.split("\n")
114
117
 
115
- headers["Set-Cookie".freeze] = cookies.map { |cookie|
116
- if cookie !~ /;\s*secure\s*(;|$)/i
118
+ headers["Set-Cookie"] = cookies.map { |cookie|
119
+ if !/;\s*secure\s*(;|$)/i.match?(cookie)
117
120
  "#{cookie}; secure"
118
121
  else
119
122
  cookie
120
123
  end
121
- }.join("\n".freeze)
124
+ }.join("\n")
122
125
  end
123
126
  end
124
127
 
@@ -126,12 +129,14 @@ module ActionDispatch
126
129
  [ @redirect.fetch(:status, redirection_status(request)),
127
130
  { "Content-Type" => "text/html",
128
131
  "Location" => https_location_for(request) },
129
- @redirect.fetch(:body, []) ]
132
+ (@redirect[:body] || []) ]
130
133
  end
131
134
 
132
135
  def redirection_status(request)
133
- if request.get? || request.head?
136
+ if PERMANENT_REDIRECT_REQUEST_METHODS.include?(request.raw_request_method)
134
137
  301 # Issue a permanent redirect via a GET request.
138
+ elsif @ssl_default_redirect_status
139
+ @ssl_default_redirect_status
135
140
  else
136
141
  307 # Issue a fresh request redirect to preserve the HTTP method.
137
142
  end
@@ -141,7 +146,7 @@ module ActionDispatch
141
146
  host = @redirect[:host] || request.host
142
147
  port = @redirect[:port] || request.port
143
148
 
144
- location = "https://#{host}".dup
149
+ location = +"https://#{host}"
145
150
  location << ":#{port}" if port != 80 && port != 443
146
151
  location << request.fullpath
147
152
  location
@@ -36,6 +36,31 @@ module ActionDispatch
36
36
  def build(app)
37
37
  klass.new(app, *args, &block)
38
38
  end
39
+
40
+ def build_instrumented(app)
41
+ InstrumentationProxy.new(build(app), inspect)
42
+ end
43
+ end
44
+
45
+ # This class is used to instrument the execution of a single middleware.
46
+ # It proxies the +call+ method transparently and instruments the method
47
+ # call.
48
+ class InstrumentationProxy
49
+ EVENT_NAME = "process_middleware.action_dispatch"
50
+
51
+ def initialize(middleware, class_name)
52
+ @middleware = middleware
53
+
54
+ @payload = {
55
+ middleware: class_name,
56
+ }
57
+ end
58
+
59
+ def call(env)
60
+ ActiveSupport::Notifications.instrument(EVENT_NAME, @payload) do
61
+ @middleware.call(env)
62
+ end
63
+ end
39
64
  end
40
65
 
41
66
  include Enumerable
@@ -47,8 +72,8 @@ module ActionDispatch
47
72
  yield(self) if block_given?
48
73
  end
49
74
 
50
- def each
51
- @middlewares.each { |x| yield x }
75
+ def each(&block)
76
+ @middlewares.each(&block)
52
77
  end
53
78
 
54
79
  def size
@@ -66,6 +91,7 @@ module ActionDispatch
66
91
  def unshift(klass, *args, &block)
67
92
  middlewares.unshift(build_middleware(klass, args, block))
68
93
  end
94
+ ruby2_keywords(:unshift)
69
95
 
70
96
  def initialize_copy(other)
71
97
  self.middlewares = other.middlewares.dup
@@ -75,6 +101,7 @@ module ActionDispatch
75
101
  index = assert_index(index, :before)
76
102
  middlewares.insert(index, build_middleware(klass, args, block))
77
103
  end
104
+ ruby2_keywords(:insert)
78
105
 
79
106
  alias_method :insert_before, :insert
80
107
 
@@ -82,29 +109,68 @@ module ActionDispatch
82
109
  index = assert_index(index, :after)
83
110
  insert(index + 1, *args, &block)
84
111
  end
112
+ ruby2_keywords(:insert_after)
85
113
 
86
114
  def swap(target, *args, &block)
87
115
  index = assert_index(target, :before)
88
116
  insert(index, *args, &block)
89
117
  middlewares.delete_at(index + 1)
90
118
  end
119
+ ruby2_keywords(:swap)
91
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.
92
125
  def delete(target)
93
- 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}")
135
+ end
136
+
137
+ def move(target, source)
138
+ source_index = assert_index(source, :before)
139
+ source_middleware = middlewares.delete_at(source_index)
140
+
141
+ target_index = assert_index(target, :before)
142
+ middlewares.insert(target_index, source_middleware)
143
+ end
144
+
145
+ alias_method :move_before, :move
146
+
147
+ def move_after(target, source)
148
+ source_index = assert_index(source, :after)
149
+ source_middleware = middlewares.delete_at(source_index)
150
+
151
+ target_index = assert_index(target, :after)
152
+ middlewares.insert(target_index + 1, source_middleware)
94
153
  end
95
154
 
96
155
  def use(klass, *args, &block)
97
156
  middlewares.push(build_middleware(klass, args, block))
98
157
  end
158
+ ruby2_keywords(:use)
99
159
 
100
- def build(app = Proc.new)
101
- middlewares.freeze.reverse.inject(app) { |a, e| e.build(a) }
160
+ def build(app = nil, &block)
161
+ instrumenting = ActiveSupport::Notifications.notifier.listening?(InstrumentationProxy::EVENT_NAME)
162
+ middlewares.freeze.reverse.inject(app || block) do |a, e|
163
+ if instrumenting
164
+ e.build_instrumented(a)
165
+ else
166
+ e.build(a)
167
+ end
168
+ end
102
169
  end
103
170
 
104
171
  private
105
-
106
172
  def assert_index(index, where)
107
- i = index.is_a?(Integer) ? index : middlewares.index { |m| m.klass == index }
173
+ i = index.is_a?(Integer) ? index : index_of(index)
108
174
  raise "No such middleware to insert #{where}: #{index.inspect}" unless i
109
175
  i
110
176
  end
@@ -112,5 +178,11 @@ module ActionDispatch
112
178
  def build_middleware(klass, args, block)
113
179
  Middleware.new(klass, args, block)
114
180
  end
181
+
182
+ def index_of(klass)
183
+ middlewares.index do |m|
184
+ m.name == klass.name
185
+ end
186
+ end
115
187
  end
116
188
  end
@@ -1,130 +1,186 @@
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
- # This middleware returns a file's contents from disk in the body response.
8
- # When initialized, it can accept optional HTTP headers, which will be set
9
- # when a response containing a file's contents is delivered.
6
+ # This middleware serves static files from disk, if available.
7
+ # If no file is found, it hands off to the main app.
10
8
  #
11
- # This middleware will render the file specified in <tt>env["PATH_INFO"]</tt>
12
- # where the base path is in the +root+ directory. For example, if the +root+
13
- # is set to +public/+, then a request with <tt>env["PATH_INFO"]</tt> of
14
- # +assets/application.js+ will return a response with the contents of a file
15
- # located at +public/assets/application.js+ if the file exists. If the file
16
- # does not exist, a 404 "File not Found" response will be returned.
17
- class FileHandler
18
- def initialize(root, index: "index", headers: {})
19
- @root = root.chomp("/").b
20
- @file_server = ::Rack::File.new(@root, headers)
21
- @index = index
9
+ # In Rails apps, this middleware is configured to serve assets from
10
+ # the +public/+ directory.
11
+ #
12
+ # Only GET and HEAD requests are served. POST and other HTTP methods
13
+ # are handed off to the main app.
14
+ #
15
+ # Only files in the root directory are served; path traversal is denied.
16
+ class Static
17
+ def initialize(app, path, index: "index", headers: {})
18
+ @app = app
19
+ @file_handler = FileHandler.new(path, index: index, headers: headers)
22
20
  end
23
21
 
24
- # Takes a path to a file. If the file is found, has valid encoding, and has
25
- # correct read permissions, the return value is a URI-escaped string
26
- # representing the filename. Otherwise, false is returned.
27
- #
28
- # Used by the +Static+ class to check the existence of a valid file
29
- # in the server's +public/+ directory (see Static#call).
30
- def match?(path)
31
- path = ::Rack::Utils.unescape_path path
32
- return false unless ::Rack::Utils.valid_path? path
33
- path = ::Rack::Utils.clean_path_info path
34
-
35
- paths = [path, "#{path}#{ext}", "#{path}/#{@index}#{ext}"]
36
-
37
- if match = paths.detect { |p|
38
- path = File.join(@root, p.b)
39
- begin
40
- File.file?(path) && File.readable?(path)
41
- rescue SystemCallError
42
- false
43
- end
22
+ def call(env)
23
+ @file_handler.attempt(env) || @app.call(env)
24
+ end
25
+ end
44
26
 
45
- }
46
- return ::Rack::Utils.escape_path(match).b
47
- end
27
+ # This endpoint serves static files from disk using Rack::File.
28
+ #
29
+ # URL paths are matched with static files according to expected
30
+ # conventions: +path+, +path+.html, +path+/index.html.
31
+ #
32
+ # Precompressed versions of these files are checked first. Brotli (.br)
33
+ # and gzip (.gz) files are supported. If +path+.br exists, this
34
+ # endpoint returns that file with a <tt>Content-Encoding: br</tt> header.
35
+ #
36
+ # If no matching file is found, this endpoint responds 404 Not Found.
37
+ #
38
+ # Pass the +root+ directory to search for matching files, an optional
39
+ # <tt>index: "index"</tt> to change the default +path+/index.html, and optional
40
+ # additional response headers.
41
+ class FileHandler
42
+ # Accept-Encoding value -> file extension
43
+ PRECOMPRESSED = {
44
+ "br" => ".br",
45
+ "gzip" => ".gz",
46
+ "identity" => nil
47
+ }
48
+
49
+ def initialize(root, index: "index", headers: {}, precompressed: %i[ br gzip ], compressible_content_types: /\A(?:text\/|application\/javascript)/)
50
+ @root = root.chomp("/").b
51
+ @index = index
52
+
53
+ @precompressed = Array(precompressed).map(&:to_s) | %w[ identity ]
54
+ @compressible_content_types = compressible_content_types
55
+
56
+ @file_server = ::Rack::File.new(@root, headers)
48
57
  end
49
58
 
50
59
  def call(env)
51
- serve(Rack::Request.new(env))
60
+ attempt(env) || @file_server.call(env)
52
61
  end
53
62
 
54
- def serve(request)
55
- path = request.path_info
56
- gzip_path = gzip_file_path(path)
63
+ def attempt(env)
64
+ request = Rack::Request.new env
57
65
 
58
- if gzip_path && gzip_encoding_accepted?(request)
59
- request.path_info = gzip_path
60
- status, headers, body = @file_server.call(request.env)
61
- if status == 304
62
- return [status, headers, body]
66
+ if request.get? || request.head?
67
+ if found = find_file(request.path_info, accept_encoding: request.accept_encoding)
68
+ serve request, *found
63
69
  end
64
- headers["Content-Encoding"] = "gzip"
65
- headers["Content-Type"] = content_type(path)
66
- else
67
- status, headers, body = @file_server.call(request.env)
68
70
  end
69
-
70
- headers["Vary"] = "Accept-Encoding" if gzip_path
71
-
72
- return [status, headers, body]
73
- ensure
74
- request.path_info = path
75
71
  end
76
72
 
77
73
  private
78
- def ext
79
- ::ActionController::Base.default_static_extension
74
+ def serve(request, filepath, content_headers)
75
+ original, request.path_info =
76
+ request.path_info, ::Rack::Utils.escape_path(filepath).b
77
+
78
+ @file_server.call(request.env).tap do |status, headers, body|
79
+ # Omit Content-Encoding/Type/etc headers for 304 Not Modified
80
+ if status != 304
81
+ headers.update(content_headers)
82
+ end
83
+ end
84
+ ensure
85
+ request.path_info = original
80
86
  end
81
87
 
82
- def content_type(path)
83
- ::Rack::Mime.mime_type(::File.extname(path), "text/plain".freeze)
88
+ # Match a URI path to a static file to be served.
89
+ #
90
+ # Used by the +Static+ class to negotiate a servable file in the
91
+ # +public/+ directory (see Static#call).
92
+ #
93
+ # Checks for +path+, +path+.html, and +path+/index.html files,
94
+ # in that order, including .br and .gzip compressed extensions.
95
+ #
96
+ # If a matching file is found, the path and necessary response headers
97
+ # (Content-Type, Content-Encoding) are returned.
98
+ def find_file(path_info, accept_encoding:)
99
+ each_candidate_filepath(path_info) do |filepath, content_type|
100
+ if response = try_files(filepath, content_type, accept_encoding: accept_encoding)
101
+ return response
102
+ end
103
+ end
84
104
  end
85
105
 
86
- def gzip_encoding_accepted?(request)
87
- request.accept_encoding.any? { |enc, quality| enc =~ /\bgzip\b/i }
106
+ def try_files(filepath, content_type, accept_encoding:)
107
+ headers = { "Content-Type" => content_type }
108
+
109
+ if compressible? content_type
110
+ try_precompressed_files filepath, headers, accept_encoding: accept_encoding
111
+ elsif file_readable? filepath
112
+ [ filepath, headers ]
113
+ end
88
114
  end
89
115
 
90
- def gzip_file_path(path)
91
- can_gzip_mime = content_type(path) =~ /\A(?:text\/|application\/javascript)/
92
- gzip_path = "#{path}.gz"
93
- if can_gzip_mime && File.exist?(File.join(@root, ::Rack::Utils.unescape_path(gzip_path).b))
94
- gzip_path.b
95
- else
96
- false
116
+ def try_precompressed_files(filepath, headers, accept_encoding:)
117
+ each_precompressed_filepath(filepath) do |content_encoding, precompressed_filepath|
118
+ if file_readable? precompressed_filepath
119
+ # Identity encoding is default, so we skip Accept-Encoding
120
+ # negotiation and needn't set Content-Encoding.
121
+ #
122
+ # Vary header is expected when we've found other available
123
+ # encodings that Accept-Encoding ruled out.
124
+ if content_encoding == "identity"
125
+ return precompressed_filepath, headers
126
+ else
127
+ headers["Vary"] = "Accept-Encoding"
128
+
129
+ if accept_encoding.any? { |enc, _| /\b#{content_encoding}\b/i.match?(enc) }
130
+ headers["Content-Encoding"] = content_encoding
131
+ return precompressed_filepath, headers
132
+ end
133
+ end
134
+ end
97
135
  end
98
136
  end
99
- end
100
137
 
101
- # This middleware will attempt to return the contents of a file's body from
102
- # disk in the response. If a file is not found on disk, the request will be
103
- # delegated to the application stack. This middleware is commonly initialized
104
- # to serve assets from a server's +public/+ directory.
105
- #
106
- # This middleware verifies the path to ensure that only files
107
- # living in the root directory can be rendered. A request cannot
108
- # produce a directory traversal using this middleware. Only 'GET' and 'HEAD'
109
- # requests will result in a file being returned.
110
- class Static
111
- def initialize(app, path, index: "index", headers: {})
112
- @app = app
113
- @file_handler = FileHandler.new(path, index: index, headers: headers)
114
- end
138
+ def file_readable?(path)
139
+ file_path = File.join(@root, path.b)
140
+ File.file?(file_path) && File.readable?(file_path)
141
+ end
115
142
 
116
- def call(env)
117
- req = Rack::Request.new env
143
+ def compressible?(content_type)
144
+ @compressible_content_types.match?(content_type)
145
+ end
118
146
 
119
- if req.get? || req.head?
120
- path = req.path_info.chomp("/".freeze)
121
- if match = @file_handler.match?(path)
122
- req.path_info = match
123
- return @file_handler.serve(req)
147
+ def each_precompressed_filepath(filepath)
148
+ @precompressed.each do |content_encoding|
149
+ precompressed_ext = PRECOMPRESSED.fetch(content_encoding)
150
+ yield content_encoding, "#{filepath}#{precompressed_ext}"
124
151
  end
152
+
153
+ nil
125
154
  end
126
155
 
127
- @app.call(req.env)
128
- end
156
+ def each_candidate_filepath(path_info)
157
+ return unless path = clean_path(path_info)
158
+
159
+ ext = ::File.extname(path)
160
+ content_type = ::Rack::Mime.mime_type(ext, nil)
161
+ yield path, content_type || "text/plain"
162
+
163
+ # Tack on .html and /index.html only for paths that don't have
164
+ # an explicit, resolvable file extension. No need to check
165
+ # for foo.js.html and foo.js/index.html.
166
+ unless content_type
167
+ default_ext = ::ActionController::Base.default_static_extension
168
+ if ext != default_ext
169
+ default_content_type = ::Rack::Mime.mime_type(default_ext, "text/plain")
170
+
171
+ yield "#{path}#{default_ext}", default_content_type
172
+ yield "#{path}/#{@index}#{default_ext}", default_content_type
173
+ end
174
+ end
175
+
176
+ nil
177
+ end
178
+
179
+ def clean_path(path_info)
180
+ path = ::Rack::Utils.unescape_path path_info.chomp("/")
181
+ if ::Rack::Utils.valid_path? path
182
+ ::Rack::Utils.clean_path_info path
183
+ end
184
+ end
129
185
  end
130
186
  end
@@ -0,0 +1,13 @@
1
+ <% actions = ActiveSupport::ActionableError.actions(exception) %>
2
+
3
+ <% if actions.any? %>
4
+ <div class="actions">
5
+ <% actions.each do |action, _| %>
6
+ <%= button_to action, ActionDispatch::ActionableExceptions.endpoint, params: {
7
+ error: exception.class.name,
8
+ action: action,
9
+ location: request.path
10
+ } %>
11
+ <% end %>
12
+ </div>
13
+ <% end %>
@@ -0,0 +1,22 @@
1
+ <% if exception.respond_to?(:original_message) && exception.respond_to?(:corrections) %>
2
+ <div class="exception-message">
3
+ <%= simple_format h(exception.original_message), { class: "message" }, wrapper_tag: "div" %>
4
+ </div>
5
+ <%
6
+ # The 'did_you_mean' gem can raise exceptions when calling #corrections on
7
+ # the exception. If it does there are no corrections to show.
8
+ corrections = exception.corrections rescue []
9
+ %>
10
+ <% if corrections.any? %>
11
+ <b>Did you mean?</b>
12
+ <ul>
13
+ <% corrections.each do |correction| %>
14
+ <li class="correction"><%= h correction %></li>
15
+ <% end %>
16
+ </ul>
17
+ <% end %>
18
+ <% else %>
19
+ <div class="exception-message">
20
+ <%= simple_format h(exception.message), { class: "message" }, wrapper_tag: "div" %>
21
+ </div>
22
+ <% end %>
@@ -1,22 +1,17 @@
1
- <% unless @exception.blamed_files.blank? %>
2
- <% if (hide = @exception.blamed_files.length > 8) %>
3
- <a href="#" onclick="return toggleTrace()">Toggle blamed files</a>
4
- <% end %>
5
- <pre id="blame_trace" <%='style="display:none"' if hide %>><code><%= @exception.describe_blame %></code></pre>
1
+ <h2 class="request-heading">Request</h2>
2
+ <% if params_valid? %>
3
+ <p><b>Parameters</b>:</p> <pre><%= debug_params(@request.filtered_parameters) %></pre>
6
4
  <% end %>
7
5
 
8
- <h2 style="margin-top: 30px">Request</h2>
9
- <p><b>Parameters</b>:</p> <pre><%= debug_params(@request.filtered_parameters) %></pre>
10
-
11
6
  <div class="details">
12
7
  <div class="summary"><a href="#" onclick="return toggleSessionDump()">Toggle session dump</a></div>
13
- <div id="session_dump" style="display:none"><pre><%= debug_hash @request.session %></pre></div>
8
+ <div id="session_dump" class="hidden"><pre><%= debug_hash @request.session %></pre></div>
14
9
  </div>
15
10
 
16
11
  <div class="details">
17
12
  <div class="summary"><a href="#" onclick="return toggleEnvDump()">Toggle env dump</a></div>
18
- <div id="env_dump" style="display:none"><pre><%= debug_hash @request.env.slice(*@request.class::ENV_METHODS) %></pre></div>
13
+ <div id="env_dump" class="hidden"><pre><%= debug_hash @request.env.slice(*@request.class::ENV_METHODS) %></pre></div>
19
14
  </div>
20
15
 
21
- <h2 style="margin-top: 30px">Response</h2>
16
+ <h2 class="response-heading">Response</h2>
22
17
  <p><b>Headers</b>:</p> <pre><%= debug_headers(defined?(@response) ? @response.headers : {}) %></pre>
@@ -1,5 +1,5 @@
1
1
  <%
2
- clean_params = @request.filtered_parameters.clone
2
+ clean_params = params_valid? ? @request.filtered_parameters.clone : {}
3
3
  clean_params.delete("action")
4
4
  clean_params.delete("controller")
5
5
 
@@ -1,6 +1,8 @@
1
- <% @source_extracts.each_with_index do |source_extract, index| %>
1
+ <% error_index = local_assigns[:error_index] || 0 %>
2
+
3
+ <% source_extracts.each_with_index do |source_extract, index| %>
2
4
  <% if source_extract[:code] %>
3
- <div class="source <%="hidden" if @show_source_idx != index%>" id="frame-source-<%=index%>">
5
+ <div class="source <%= "hidden" if show_source_idx != index %>" id="frame-source-<%= error_index %>-<%= index %>">
4
6
  <div class="info">
5
7
  Extracted source (around line <strong>#<%= source_extract[:line_number] %></strong>):
6
8
  </div>