actionpack 4.2.11.1 → 6.0.3

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 (182) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +212 -526
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +9 -9
  5. data/lib/abstract_controller/asset_paths.rb +2 -0
  6. data/lib/abstract_controller/base.rb +47 -50
  7. data/lib/{action_controller → abstract_controller}/caching/fragments.rb +64 -17
  8. data/lib/abstract_controller/caching.rb +66 -0
  9. data/lib/abstract_controller/callbacks.rb +59 -31
  10. data/lib/abstract_controller/collector.rb +9 -13
  11. data/lib/abstract_controller/error.rb +6 -0
  12. data/lib/abstract_controller/helpers.rb +31 -30
  13. data/lib/abstract_controller/logger.rb +2 -0
  14. data/lib/abstract_controller/railties/routes_helpers.rb +5 -3
  15. data/lib/abstract_controller/rendering.rb +42 -41
  16. data/lib/abstract_controller/translation.rb +12 -9
  17. data/lib/abstract_controller/url_for.rb +2 -0
  18. data/lib/abstract_controller.rb +12 -5
  19. data/lib/action_controller/api/api_rendering.rb +16 -0
  20. data/lib/action_controller/api.rb +150 -0
  21. data/lib/action_controller/base.rb +25 -22
  22. data/lib/action_controller/caching.rb +13 -57
  23. data/lib/action_controller/form_builder.rb +50 -0
  24. data/lib/action_controller/log_subscriber.rb +15 -17
  25. data/lib/action_controller/metal/basic_implicit_render.rb +13 -0
  26. data/lib/action_controller/metal/conditional_get.rb +124 -44
  27. data/lib/action_controller/metal/content_security_policy.rb +51 -0
  28. data/lib/action_controller/metal/cookies.rb +3 -3
  29. data/lib/action_controller/metal/data_streaming.rb +29 -49
  30. data/lib/action_controller/metal/default_headers.rb +17 -0
  31. data/lib/action_controller/metal/etag_with_flash.rb +18 -0
  32. data/lib/action_controller/metal/etag_with_template_digest.rb +20 -13
  33. data/lib/action_controller/metal/exceptions.rb +30 -15
  34. data/lib/action_controller/metal/flash.rb +9 -8
  35. data/lib/action_controller/metal/force_ssl.rb +23 -62
  36. data/lib/action_controller/metal/head.rb +22 -20
  37. data/lib/action_controller/metal/helpers.rb +26 -17
  38. data/lib/action_controller/metal/http_authentication.rb +76 -70
  39. data/lib/action_controller/metal/implicit_render.rb +53 -9
  40. data/lib/action_controller/metal/instrumentation.rb +22 -27
  41. data/lib/action_controller/metal/live.rb +101 -119
  42. data/lib/action_controller/metal/mime_responds.rb +44 -46
  43. data/lib/action_controller/metal/parameter_encoding.rb +51 -0
  44. data/lib/action_controller/metal/params_wrapper.rb +74 -63
  45. data/lib/action_controller/metal/redirecting.rb +53 -32
  46. data/lib/action_controller/metal/renderers.rb +87 -44
  47. data/lib/action_controller/metal/rendering.rb +72 -51
  48. data/lib/action_controller/metal/request_forgery_protection.rb +217 -97
  49. data/lib/action_controller/metal/rescue.rb +9 -16
  50. data/lib/action_controller/metal/streaming.rb +12 -11
  51. data/lib/action_controller/metal/strong_parameters.rb +619 -183
  52. data/lib/action_controller/metal/testing.rb +2 -17
  53. data/lib/action_controller/metal/url_for.rb +19 -10
  54. data/lib/action_controller/metal.rb +104 -87
  55. data/lib/action_controller/railtie.rb +28 -10
  56. data/lib/action_controller/railties/helpers.rb +3 -1
  57. data/lib/action_controller/renderer.rb +130 -0
  58. data/lib/action_controller/template_assertions.rb +11 -0
  59. data/lib/action_controller/test_case.rb +286 -418
  60. data/lib/action_controller.rb +33 -21
  61. data/lib/action_dispatch/http/cache.rb +100 -51
  62. data/lib/action_dispatch/http/content_disposition.rb +45 -0
  63. data/lib/action_dispatch/http/content_security_policy.rb +282 -0
  64. data/lib/action_dispatch/http/filter_parameters.rb +31 -24
  65. data/lib/action_dispatch/http/filter_redirect.rb +10 -12
  66. data/lib/action_dispatch/http/headers.rb +54 -22
  67. data/lib/action_dispatch/http/mime_negotiation.rb +61 -45
  68. data/lib/action_dispatch/http/mime_type.rb +141 -122
  69. data/lib/action_dispatch/http/mime_types.rb +20 -6
  70. data/lib/action_dispatch/http/parameter_filter.rb +8 -68
  71. data/lib/action_dispatch/http/parameters.rb +107 -39
  72. data/lib/action_dispatch/http/rack_cache.rb +2 -0
  73. data/lib/action_dispatch/http/request.rb +204 -117
  74. data/lib/action_dispatch/http/response.rb +248 -114
  75. data/lib/action_dispatch/http/upload.rb +21 -7
  76. data/lib/action_dispatch/http/url.rb +181 -100
  77. data/lib/action_dispatch/journey/formatter.rb +56 -34
  78. data/lib/action_dispatch/journey/gtg/builder.rb +7 -6
  79. data/lib/action_dispatch/journey/gtg/simulator.rb +3 -9
  80. data/lib/action_dispatch/journey/gtg/transition_table.rb +17 -17
  81. data/lib/action_dispatch/journey/nfa/builder.rb +5 -3
  82. data/lib/action_dispatch/journey/nfa/dot.rb +13 -13
  83. data/lib/action_dispatch/journey/nfa/simulator.rb +3 -3
  84. data/lib/action_dispatch/journey/nfa/transition_table.rb +5 -49
  85. data/lib/action_dispatch/journey/nodes/node.rb +25 -12
  86. data/lib/action_dispatch/journey/parser.rb +23 -22
  87. data/lib/action_dispatch/journey/parser.y +3 -2
  88. data/lib/action_dispatch/journey/parser_extras.rb +12 -4
  89. data/lib/action_dispatch/journey/path/pattern.rb +55 -46
  90. data/lib/action_dispatch/journey/route.rb +107 -28
  91. data/lib/action_dispatch/journey/router/utils.rb +25 -16
  92. data/lib/action_dispatch/journey/router.rb +35 -27
  93. data/lib/action_dispatch/journey/routes.rb +17 -17
  94. data/lib/action_dispatch/journey/scanner.rb +26 -17
  95. data/lib/action_dispatch/journey/visitors.rb +98 -54
  96. data/lib/action_dispatch/journey.rb +7 -5
  97. data/lib/action_dispatch/middleware/actionable_exceptions.rb +39 -0
  98. data/lib/action_dispatch/middleware/callbacks.rb +3 -6
  99. data/lib/action_dispatch/middleware/cookies.rb +292 -203
  100. data/lib/action_dispatch/middleware/debug_exceptions.rb +142 -63
  101. data/lib/action_dispatch/middleware/debug_locks.rb +124 -0
  102. data/lib/action_dispatch/middleware/debug_view.rb +66 -0
  103. data/lib/action_dispatch/middleware/exception_wrapper.rb +102 -70
  104. data/lib/action_dispatch/middleware/executor.rb +21 -0
  105. data/lib/action_dispatch/middleware/flash.rb +78 -54
  106. data/lib/action_dispatch/middleware/host_authorization.rb +101 -0
  107. data/lib/action_dispatch/middleware/public_exceptions.rb +32 -27
  108. data/lib/action_dispatch/middleware/reloader.rb +5 -91
  109. data/lib/action_dispatch/middleware/remote_ip.rb +48 -41
  110. data/lib/action_dispatch/middleware/request_id.rb +17 -9
  111. data/lib/action_dispatch/middleware/session/abstract_store.rb +41 -26
  112. data/lib/action_dispatch/middleware/session/cache_store.rb +24 -14
  113. data/lib/action_dispatch/middleware/session/cookie_store.rb +72 -73
  114. data/lib/action_dispatch/middleware/session/mem_cache_store.rb +8 -2
  115. data/lib/action_dispatch/middleware/show_exceptions.rb +26 -23
  116. data/lib/action_dispatch/middleware/ssl.rb +113 -35
  117. data/lib/action_dispatch/middleware/stack.rb +64 -41
  118. data/lib/action_dispatch/middleware/static.rb +57 -51
  119. data/lib/action_dispatch/middleware/templates/rescues/_actions.html.erb +13 -0
  120. data/lib/action_dispatch/middleware/templates/rescues/_actions.text.erb +0 -0
  121. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb +4 -14
  122. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.text.erb +1 -1
  123. data/lib/action_dispatch/middleware/templates/rescues/{_source.erb → _source.html.erb} +4 -2
  124. data/lib/action_dispatch/middleware/templates/rescues/_source.text.erb +8 -0
  125. data/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +45 -35
  126. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb +7 -0
  127. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.text.erb +5 -0
  128. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +26 -4
  129. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb +1 -1
  130. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +24 -0
  131. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +15 -0
  132. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +5 -0
  133. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb +19 -0
  134. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.text.erb +3 -0
  135. data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +2 -2
  136. data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +1 -1
  137. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +3 -3
  138. data/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb +1 -1
  139. data/lib/action_dispatch/middleware/templates/routes/_route.html.erb +4 -4
  140. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +67 -64
  141. data/lib/action_dispatch/railtie.rb +26 -13
  142. data/lib/action_dispatch/request/session.rb +114 -60
  143. data/lib/action_dispatch/request/utils.rb +67 -24
  144. data/lib/action_dispatch/routing/endpoint.rb +9 -2
  145. data/lib/action_dispatch/routing/inspector.rb +140 -102
  146. data/lib/action_dispatch/routing/mapper.rb +762 -455
  147. data/lib/action_dispatch/routing/polymorphic_routes.rb +161 -142
  148. data/lib/action_dispatch/routing/redirection.rb +36 -26
  149. data/lib/action_dispatch/routing/route_set.rb +322 -298
  150. data/lib/action_dispatch/routing/routes_proxy.rb +32 -5
  151. data/lib/action_dispatch/routing/url_for.rb +65 -26
  152. data/lib/action_dispatch/routing.rb +36 -36
  153. data/lib/action_dispatch/system_test_case.rb +185 -0
  154. data/lib/action_dispatch/system_testing/browser.rb +80 -0
  155. data/lib/action_dispatch/system_testing/driver.rb +68 -0
  156. data/lib/action_dispatch/system_testing/server.rb +31 -0
  157. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +97 -0
  158. data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +32 -0
  159. data/lib/action_dispatch/testing/assertion_response.rb +46 -0
  160. data/lib/action_dispatch/testing/assertions/response.rb +44 -20
  161. data/lib/action_dispatch/testing/assertions/routing.rb +44 -28
  162. data/lib/action_dispatch/testing/assertions.rb +6 -4
  163. data/lib/action_dispatch/testing/integration.rb +375 -215
  164. data/lib/action_dispatch/testing/request_encoder.rb +55 -0
  165. data/lib/action_dispatch/testing/test_process.rb +28 -22
  166. data/lib/action_dispatch/testing/test_request.rb +27 -34
  167. data/lib/action_dispatch/testing/test_response.rb +11 -11
  168. data/lib/action_dispatch.rb +33 -20
  169. data/lib/action_pack/gem_version.rb +6 -4
  170. data/lib/action_pack/version.rb +3 -1
  171. data/lib/action_pack.rb +4 -2
  172. metadata +71 -40
  173. data/lib/action_controller/metal/hide_actions.rb +0 -40
  174. data/lib/action_controller/metal/rack_delegation.rb +0 -32
  175. data/lib/action_controller/middleware.rb +0 -39
  176. data/lib/action_controller/model_naming.rb +0 -12
  177. data/lib/action_dispatch/journey/backwards.rb +0 -5
  178. data/lib/action_dispatch/journey/router/strexp.rb +0 -27
  179. data/lib/action_dispatch/middleware/params_parser.rb +0 -60
  180. data/lib/action_dispatch/testing/assertions/dom.rb +0 -3
  181. data/lib/action_dispatch/testing/assertions/selector.rb +0 -3
  182. data/lib/action_dispatch/testing/assertions/tag.rb +0 -3
@@ -1,66 +1,119 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActionDispatch
4
+ # This middleware is added to the stack when <tt>config.force_ssl = true</tt>, and is passed
5
+ # the options set in +config.ssl_options+. It does three jobs to enforce secure HTTP
6
+ # requests:
7
+ #
8
+ # 1. <b>TLS redirect</b>: Permanently redirects +http://+ requests to +https://+
9
+ # with the same URL host, path, etc. Enabled by default. Set +config.ssl_options+
10
+ # to modify the destination URL
11
+ # (e.g. <tt>redirect: { host: "secure.widgets.com", port: 8080 }</tt>), or set
12
+ # <tt>redirect: false</tt> to disable this feature.
13
+ #
14
+ # Requests can opt-out of redirection with +exclude+:
15
+ #
16
+ # config.ssl_options = { redirect: { exclude: -> request { request.path =~ /healthcheck/ } } }
17
+ #
18
+ # Cookies will not be flagged as secure for excluded requests.
19
+ #
20
+ # 2. <b>Secure cookies</b>: Sets the +secure+ flag on cookies to tell browsers they
21
+ # must not be sent along with +http://+ requests. Enabled by default. Set
22
+ # +config.ssl_options+ with <tt>secure_cookies: false</tt> to disable this feature.
23
+ #
24
+ # 3. <b>HTTP Strict Transport Security (HSTS)</b>: Tells the browser to remember
25
+ # this site as TLS-only and automatically redirect non-TLS requests.
26
+ # Enabled by default. Configure +config.ssl_options+ with <tt>hsts: false</tt> to disable.
27
+ #
28
+ # Set +config.ssl_options+ with <tt>hsts: { ... }</tt> to configure HSTS:
29
+ #
30
+ # * +expires+: How long, in seconds, these settings will stick. The minimum
31
+ # required to qualify for browser preload lists is 1 year. Defaults to
32
+ # 1 year (recommended).
33
+ #
34
+ # * +subdomains+: Set to +true+ to tell the browser to apply these settings
35
+ # to all subdomains. This protects your cookies from interception by a
36
+ # vulnerable site on a subdomain. Defaults to +true+.
37
+ #
38
+ # * +preload+: Advertise that this site may be included in browsers'
39
+ # preloaded HSTS lists. HSTS protects your site on every visit <i>except the
40
+ # first visit</i> since it hasn't seen your HSTS header yet. To close this
41
+ # gap, browser vendors include a baked-in list of HSTS-enabled sites.
42
+ # Go to https://hstspreload.org to submit your site for inclusion.
43
+ # Defaults to +false+.
44
+ #
45
+ # To turn off HSTS, omitting the header is not enough. Browsers will remember the
46
+ # original HSTS directive until it expires. Instead, use the header to tell browsers to
47
+ # expire HSTS immediately. Setting <tt>hsts: false</tt> is a shortcut for
48
+ # <tt>hsts: { expires: 0 }</tt>.
2
49
  class SSL
3
- YEAR = 31536000
50
+ # :stopdoc:
51
+
52
+ # Default to 1 year, the minimum for browser preload lists.
53
+ HSTS_EXPIRES_IN = 31536000
4
54
 
5
55
  def self.default_hsts_options
6
- { :expires => YEAR, :subdomains => false }
56
+ { expires: HSTS_EXPIRES_IN, subdomains: true, preload: false }
7
57
  end
8
58
 
9
- def initialize(app, options = {})
59
+ def initialize(app, redirect: {}, hsts: {}, secure_cookies: true)
10
60
  @app = app
11
61
 
12
- @hsts = options.fetch(:hsts, {})
13
- @hsts = {} if @hsts == true
14
- @hsts = self.class.default_hsts_options.merge(@hsts) if @hsts
62
+ @redirect = redirect
63
+
64
+ @exclude = @redirect && @redirect[:exclude] || proc { !@redirect }
65
+ @secure_cookies = secure_cookies
15
66
 
16
- @host = options[:host]
17
- @port = options[:port]
67
+ @hsts_header = build_hsts_header(normalize_hsts_options(hsts))
18
68
  end
19
69
 
20
70
  def call(env)
21
- request = Request.new(env)
71
+ request = Request.new env
22
72
 
23
73
  if request.ssl?
24
- status, headers, body = @app.call(env)
25
- headers.reverse_merge!(hsts_headers)
26
- flag_cookies_as_secure!(headers)
27
- [status, headers, body]
74
+ @app.call(env).tap do |status, headers, body|
75
+ set_hsts_header! headers
76
+ flag_cookies_as_secure! headers if @secure_cookies && !@exclude.call(request)
77
+ end
28
78
  else
29
- redirect_to_https(request)
79
+ return redirect_to_https request unless @exclude.call(request)
80
+ @app.call(env)
30
81
  end
31
82
  end
32
83
 
33
84
  private
34
- def redirect_to_https(request)
35
- host = @host || request.host
36
- port = @port || request.port
37
-
38
- location = "https://#{host}"
39
- location << ":#{port}" if port != 80
40
- location << request.fullpath
41
-
42
- headers = { 'Content-Type' => 'text/html', 'Location' => location }
43
-
44
- [301, headers, []]
85
+ def set_hsts_header!(headers)
86
+ headers["Strict-Transport-Security"] ||= @hsts_header
45
87
  end
46
88
 
47
- # http://tools.ietf.org/html/draft-hodges-strict-transport-sec-02
48
- def hsts_headers
49
- if @hsts
50
- value = "max-age=#{@hsts[:expires].to_i}"
51
- value += "; includeSubDomains" if @hsts[:subdomains]
52
- { 'Strict-Transport-Security' => value }
89
+ def normalize_hsts_options(options)
90
+ case options
91
+ # Explicitly disabling HSTS clears the existing setting from browsers
92
+ # by setting expiry to 0.
93
+ when false
94
+ self.class.default_hsts_options.merge(expires: 0)
95
+ # Default to enabled, with default options.
96
+ when nil, true
97
+ self.class.default_hsts_options
53
98
  else
54
- {}
99
+ self.class.default_hsts_options.merge(options)
55
100
  end
56
101
  end
57
102
 
103
+ # https://tools.ietf.org/html/rfc6797#section-6.1
104
+ def build_hsts_header(hsts)
105
+ value = +"max-age=#{hsts[:expires].to_i}"
106
+ value << "; includeSubDomains" if hsts[:subdomains]
107
+ value << "; preload" if hsts[:preload]
108
+ value
109
+ end
110
+
58
111
  def flag_cookies_as_secure!(headers)
59
- if cookies = headers['Set-Cookie']
112
+ if cookies = headers["Set-Cookie"]
60
113
  cookies = cookies.split("\n")
61
114
 
62
- headers['Set-Cookie'] = cookies.map { |cookie|
63
- if cookie !~ /;\s*secure\s*(;|$)/i
115
+ headers["Set-Cookie"] = cookies.map { |cookie|
116
+ if !/;\s*secure\s*(;|$)/i.match?(cookie)
64
117
  "#{cookie}; secure"
65
118
  else
66
119
  cookie
@@ -68,5 +121,30 @@ module ActionDispatch
68
121
  }.join("\n")
69
122
  end
70
123
  end
124
+
125
+ def redirect_to_https(request)
126
+ [ @redirect.fetch(:status, redirection_status(request)),
127
+ { "Content-Type" => "text/html",
128
+ "Location" => https_location_for(request) },
129
+ @redirect.fetch(:body, []) ]
130
+ end
131
+
132
+ def redirection_status(request)
133
+ if request.get? || request.head?
134
+ 301 # Issue a permanent redirect via a GET request.
135
+ else
136
+ 307 # Issue a fresh request redirect to preserve the HTTP method.
137
+ end
138
+ end
139
+
140
+ def https_location_for(request)
141
+ host = @redirect[:host] || request.host
142
+ port = @redirect[:port] || request.port
143
+
144
+ location = +"https://#{host}"
145
+ location << ":#{port}" if port != 80 && port != 443
146
+ location << request.fullpath
147
+ location
148
+ end
71
149
  end
72
150
  end
@@ -1,28 +1,20 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "active_support/inflector/methods"
2
4
  require "active_support/dependencies"
3
5
 
4
6
  module ActionDispatch
5
7
  class MiddlewareStack
6
8
  class Middleware
7
- attr_reader :args, :block, :name, :classcache
8
-
9
- def initialize(klass_or_name, *args, &block)
10
- @klass = nil
11
-
12
- if klass_or_name.respond_to?(:name)
13
- @klass = klass_or_name
14
- @name = @klass.name
15
- else
16
- @name = klass_or_name.to_s
17
- end
9
+ attr_reader :args, :block, :klass
18
10
 
19
- @classcache = ActiveSupport::Dependencies::Reference
20
- @args, @block = args, block
11
+ def initialize(klass, args, block)
12
+ @klass = klass
13
+ @args = args
14
+ @block = block
21
15
  end
22
16
 
23
- def klass
24
- @klass || classcache[@name]
25
- end
17
+ def name; klass.name; end
26
18
 
27
19
  def ==(middleware)
28
20
  case middleware
@@ -30,23 +22,44 @@ module ActionDispatch
30
22
  klass == middleware.klass
31
23
  when Class
32
24
  klass == middleware
33
- else
34
- normalize(@name) == normalize(middleware)
35
25
  end
36
26
  end
37
27
 
38
28
  def inspect
39
- klass.to_s
29
+ if klass.is_a?(Class)
30
+ klass.to_s
31
+ else
32
+ klass.class.to_s
33
+ end
40
34
  end
41
35
 
42
36
  def build(app)
43
37
  klass.new(app, *args, &block)
44
38
  end
45
39
 
46
- private
40
+ def build_instrumented(app)
41
+ InstrumentationProxy.new(build(app), inspect)
42
+ end
43
+ end
47
44
 
48
- def normalize(object)
49
- object.to_s.strip.sub(/^::/, '')
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
50
63
  end
51
64
  end
52
65
 
@@ -75,20 +88,20 @@ module ActionDispatch
75
88
  middlewares[i]
76
89
  end
77
90
 
78
- def unshift(*args, &block)
79
- middleware = self.class::Middleware.new(*args, &block)
80
- middlewares.unshift(middleware)
91
+ def unshift(klass, *args, &block)
92
+ middlewares.unshift(build_middleware(klass, args, block))
81
93
  end
94
+ ruby2_keywords(:unshift) if respond_to?(:ruby2_keywords, true)
82
95
 
83
96
  def initialize_copy(other)
84
97
  self.middlewares = other.middlewares.dup
85
98
  end
86
99
 
87
- def insert(index, *args, &block)
100
+ def insert(index, klass, *args, &block)
88
101
  index = assert_index(index, :before)
89
- middleware = self.class::Middleware.new(*args, &block)
90
- middlewares.insert(index, middleware)
102
+ middlewares.insert(index, build_middleware(klass, args, block))
91
103
  end
104
+ ruby2_keywords(:insert) if respond_to?(:ruby2_keywords, true)
92
105
 
93
106
  alias_method :insert_before, :insert
94
107
 
@@ -96,34 +109,44 @@ module ActionDispatch
96
109
  index = assert_index(index, :after)
97
110
  insert(index + 1, *args, &block)
98
111
  end
112
+ ruby2_keywords(:insert_after) if respond_to?(:ruby2_keywords, true)
99
113
 
100
114
  def swap(target, *args, &block)
101
115
  index = assert_index(target, :before)
102
116
  insert(index, *args, &block)
103
117
  middlewares.delete_at(index + 1)
104
118
  end
119
+ ruby2_keywords(:swap) if respond_to?(:ruby2_keywords, true)
105
120
 
106
121
  def delete(target)
107
- middlewares.delete target
122
+ middlewares.delete_if { |m| m.klass == target }
108
123
  end
109
124
 
110
- def use(*args, &block)
111
- middleware = self.class::Middleware.new(*args, &block)
112
- middlewares.push(middleware)
125
+ def use(klass, *args, &block)
126
+ middlewares.push(build_middleware(klass, args, block))
113
127
  end
128
+ ruby2_keywords(:use) if respond_to?(:ruby2_keywords, true)
114
129
 
115
130
  def build(app = nil, &block)
116
- app ||= block
117
- raise "MiddlewareStack#build requires an app" unless app
118
- middlewares.freeze.reverse.inject(app) { |a, e| e.build(a) }
131
+ instrumenting = ActiveSupport::Notifications.notifier.listening?(InstrumentationProxy::EVENT_NAME)
132
+ middlewares.freeze.reverse.inject(app || block) do |a, e|
133
+ if instrumenting
134
+ e.build_instrumented(a)
135
+ else
136
+ e.build(a)
137
+ end
138
+ end
119
139
  end
120
140
 
121
- protected
141
+ private
142
+ def assert_index(index, where)
143
+ i = index.is_a?(Integer) ? index : middlewares.index { |m| m.klass == index }
144
+ raise "No such middleware to insert #{where}: #{index.inspect}" unless i
145
+ i
146
+ end
122
147
 
123
- def assert_index(index, where)
124
- i = index.is_a?(Integer) ? index : middlewares.index(index)
125
- raise "No such middleware to insert #{where}: #{index.inspect}" unless i
126
- i
127
- end
148
+ def build_middleware(klass, args, block)
149
+ Middleware.new(klass, args, block)
150
+ end
128
151
  end
129
152
  end
@@ -1,67 +1,76 @@
1
- require 'rack/utils'
2
- require 'active_support/core_ext/uri'
1
+ # frozen_string_literal: true
2
+
3
+ require "rack/utils"
4
+ require "active_support/core_ext/uri"
3
5
 
4
6
  module ActionDispatch
5
7
  # This middleware returns a file's contents from disk in the body response.
6
- # When initialized it can accept an optional 'Cache-Control' header which
7
- # will be set when a response containing a file's contents is delivered.
8
+ # When initialized, it can accept optional HTTP headers, which will be set
9
+ # when a response containing a file's contents is delivered.
8
10
  #
9
- # This middleware will render the file specified in `env["PATH_INFO"]`
10
- # where the base path is in the +root+ directory. For example if the +root+
11
- # is set to `public/` then a request with `env["PATH_INFO"]` of
12
- # `assets/application.js` will return a response with contents of a file
13
- # located at `public/assets/application.js` if the file exists. If the file
14
- # does not exist a 404 "File not Found" response will be returned.
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.
15
17
  class FileHandler
16
- def initialize(root, cache_control)
17
- @root = root.chomp('/')
18
- @compiled_root = /^#{Regexp.escape(root)}/
19
- headers = cache_control && { 'Cache-Control' => cache_control }
20
- @file_server = ::Rack::File.new(@root, headers)
18
+ def initialize(root, index: "index", headers: {})
19
+ @root = root.chomp("/").b
20
+ @file_server = ::Rack::File.new(@root, headers)
21
+ @index = index
21
22
  end
22
23
 
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).
23
30
  def match?(path)
24
- path = URI.parser.unescape(path)
25
- return false unless valid_path?(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
26
34
 
27
- paths = [path, "#{path}#{ext}", "#{path}/index#{ext}"].map { |v|
28
- Rack::Utils.clean_path_info v
29
- }
35
+ paths = [path, "#{path}#{ext}", "#{path}/#{@index}#{ext}"]
30
36
 
31
37
  if match = paths.detect { |p|
32
- path = File.join(@root, p.force_encoding('UTF-8'))
38
+ path = File.join(@root, p.b)
33
39
  begin
34
40
  File.file?(path) && File.readable?(path)
35
41
  rescue SystemCallError
36
42
  false
37
43
  end
38
-
39
44
  }
40
- return ::Rack::Utils.escape(match)
45
+ ::Rack::Utils.escape_path(match).b
41
46
  end
42
47
  end
43
48
 
44
49
  def call(env)
45
- path = env['PATH_INFO']
50
+ serve(Rack::Request.new(env))
51
+ end
52
+
53
+ def serve(request)
54
+ path = request.path_info
46
55
  gzip_path = gzip_file_path(path)
47
56
 
48
- if gzip_path && gzip_encoding_accepted?(env)
49
- env['PATH_INFO'] = gzip_path
50
- status, headers, body = @file_server.call(env)
57
+ if gzip_path && gzip_encoding_accepted?(request)
58
+ request.path_info = gzip_path
59
+ status, headers, body = @file_server.call(request.env)
51
60
  if status == 304
52
61
  return [status, headers, body]
53
62
  end
54
- headers['Content-Encoding'] = 'gzip'
55
- headers['Content-Type'] = content_type(path)
63
+ headers["Content-Encoding"] = "gzip"
64
+ headers["Content-Type"] = content_type(path)
56
65
  else
57
- status, headers, body = @file_server.call(env)
66
+ status, headers, body = @file_server.call(request.env)
58
67
  end
59
68
 
60
- headers['Vary'] = 'Accept-Encoding' if gzip_path
69
+ headers["Vary"] = "Accept-Encoding" if gzip_path
61
70
 
62
- return [status, headers, body]
71
+ [status, headers, body]
63
72
  ensure
64
- env['PATH_INFO'] = path
73
+ request.path_info = path
65
74
  end
66
75
 
67
76
  private
@@ -70,54 +79,51 @@ module ActionDispatch
70
79
  end
71
80
 
72
81
  def content_type(path)
73
- ::Rack::Mime.mime_type(::File.extname(path), 'text/plain')
82
+ ::Rack::Mime.mime_type(::File.extname(path), "text/plain")
74
83
  end
75
84
 
76
- def gzip_encoding_accepted?(env)
77
- env['HTTP_ACCEPT_ENCODING'] =~ /\bgzip\b/i
85
+ def gzip_encoding_accepted?(request)
86
+ request.accept_encoding.any? { |enc, quality| enc =~ /\bgzip\b/i }
78
87
  end
79
88
 
80
89
  def gzip_file_path(path)
81
90
  can_gzip_mime = content_type(path) =~ /\A(?:text\/|application\/javascript)/
82
91
  gzip_path = "#{path}.gz"
83
- if can_gzip_mime && File.exist?(File.join(@root, ::Rack::Utils.unescape(gzip_path)))
92
+ if can_gzip_mime && File.exist?(File.join(@root, ::Rack::Utils.unescape_path(gzip_path)))
84
93
  gzip_path
85
94
  else
86
95
  false
87
96
  end
88
97
  end
89
-
90
- def valid_path?(path)
91
- path.valid_encoding? && !path.include?("\0")
92
- end
93
98
  end
94
99
 
95
100
  # This middleware will attempt to return the contents of a file's body from
96
- # disk in the response. If a file is not found on disk, the request will be
101
+ # disk in the response. If a file is not found on disk, the request will be
97
102
  # delegated to the application stack. This middleware is commonly initialized
98
- # to serve assets from a server's `public/` directory.
103
+ # to serve assets from a server's +public/+ directory.
99
104
  #
100
105
  # This middleware verifies the path to ensure that only files
101
106
  # living in the root directory can be rendered. A request cannot
102
107
  # produce a directory traversal using this middleware. Only 'GET' and 'HEAD'
103
108
  # requests will result in a file being returned.
104
109
  class Static
105
- def initialize(app, path, cache_control=nil)
110
+ def initialize(app, path, index: "index", headers: {})
106
111
  @app = app
107
- @file_handler = FileHandler.new(path, cache_control)
112
+ @file_handler = FileHandler.new(path, index: index, headers: headers)
108
113
  end
109
114
 
110
115
  def call(env)
111
- case env['REQUEST_METHOD']
112
- when 'GET', 'HEAD'
113
- path = env['PATH_INFO'].chomp('/')
116
+ req = Rack::Request.new env
117
+
118
+ if req.get? || req.head?
119
+ path = req.path_info.chomp("/")
114
120
  if match = @file_handler.match?(path)
115
- env["PATH_INFO"] = match
116
- return @file_handler.call(env)
121
+ req.path_info = match
122
+ return @file_handler.serve(req)
117
123
  end
118
124
  end
119
125
 
120
- @app.call(env)
126
+ @app.call(req.env)
121
127
  end
122
128
  end
123
129
  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 %>
@@ -5,20 +5,10 @@
5
5
  <pre id="blame_trace" <%='style="display:none"' if hide %>><code><%= @exception.describe_blame %></code></pre>
6
6
  <% end %>
7
7
 
8
- <%
9
- clean_params = @request.filtered_parameters.clone
10
- clean_params.delete("action")
11
- clean_params.delete("controller")
12
-
13
- request_dump = clean_params.empty? ? 'None' : clean_params.inspect.gsub(',', ",\n")
14
-
15
- def debug_hash(object)
16
- object.to_hash.sort_by { |k, _| k.to_s }.map { |k, v| "#{k}: #{v.inspect rescue $!.message}" }.join("\n")
17
- end unless self.class.method_defined?(:debug_hash)
18
- %>
19
-
20
8
  <h2 style="margin-top: 30px">Request</h2>
21
- <p><b>Parameters</b>:</p> <pre><%= request_dump %></pre>
9
+ <% if params_valid? %>
10
+ <p><b>Parameters</b>:</p> <pre><%= debug_params(@request.filtered_parameters) %></pre>
11
+ <% end %>
22
12
 
23
13
  <div class="details">
24
14
  <div class="summary"><a href="#" onclick="return toggleSessionDump()">Toggle session dump</a></div>
@@ -31,4 +21,4 @@
31
21
  </div>
32
22
 
33
23
  <h2 style="margin-top: 30px">Response</h2>
34
- <p><b>Headers</b>:</p> <pre><%= defined?(@response) ? @response.headers.inspect.gsub(',', ",\n") : 'None' %></pre>
24
+ <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>
@@ -0,0 +1,8 @@
1
+ <% @source_extracts.first(3).each do |source_extract| %>
2
+ <% if source_extract[:code] %>
3
+ Extracted source (around line #<%= source_extract[:line_number] %>):
4
+
5
+ <% source_extract[:code].each do |line, source| -%>
6
+ <%= line == source_extract[:line_number] ? "*#{line}" : "##{line}" -%> <%= source -%><% end -%>
7
+ <% end %>
8
+ <% end %>