actionpack 5.2.4.4 → 6.1.1

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 (155) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +264 -322
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +4 -3
  5. data/lib/abstract_controller.rb +1 -0
  6. data/lib/abstract_controller/base.rb +38 -4
  7. data/lib/abstract_controller/caching.rb +1 -1
  8. data/lib/abstract_controller/caching/fragments.rb +6 -22
  9. data/lib/abstract_controller/callbacks.rb +14 -2
  10. data/lib/abstract_controller/collector.rb +1 -2
  11. data/lib/abstract_controller/helpers.rb +106 -90
  12. data/lib/abstract_controller/railties/routes_helpers.rb +1 -1
  13. data/lib/abstract_controller/rendering.rb +9 -9
  14. data/lib/abstract_controller/translation.rb +11 -5
  15. data/lib/action_controller.rb +7 -4
  16. data/lib/action_controller/api.rb +4 -3
  17. data/lib/action_controller/base.rb +6 -9
  18. data/lib/action_controller/caching.rb +1 -3
  19. data/lib/action_controller/log_subscriber.rb +10 -7
  20. data/lib/action_controller/metal.rb +10 -8
  21. data/lib/action_controller/metal/basic_implicit_render.rb +1 -1
  22. data/lib/action_controller/metal/conditional_get.rb +19 -5
  23. data/lib/action_controller/metal/content_security_policy.rb +1 -2
  24. data/lib/action_controller/metal/cookies.rb +3 -1
  25. data/lib/action_controller/metal/data_streaming.rb +6 -7
  26. data/lib/action_controller/metal/default_headers.rb +17 -0
  27. data/lib/action_controller/metal/etag_with_template_digest.rb +3 -5
  28. data/lib/action_controller/metal/exceptions.rb +56 -2
  29. data/lib/action_controller/metal/flash.rb +5 -5
  30. data/lib/action_controller/metal/head.rb +7 -4
  31. data/lib/action_controller/metal/helpers.rb +14 -5
  32. data/lib/action_controller/metal/http_authentication.rb +24 -23
  33. data/lib/action_controller/metal/implicit_render.rb +5 -15
  34. data/lib/action_controller/metal/instrumentation.rb +13 -14
  35. data/lib/action_controller/metal/live.rb +30 -32
  36. data/lib/action_controller/metal/logging.rb +20 -0
  37. data/lib/action_controller/metal/mime_responds.rb +19 -4
  38. data/lib/action_controller/metal/parameter_encoding.rb +35 -4
  39. data/lib/action_controller/metal/params_wrapper.rb +31 -22
  40. data/lib/action_controller/metal/permissions_policy.rb +46 -0
  41. data/lib/action_controller/metal/redirecting.rb +6 -6
  42. data/lib/action_controller/metal/renderers.rb +4 -4
  43. data/lib/action_controller/metal/rendering.rb +8 -3
  44. data/lib/action_controller/metal/request_forgery_protection.rb +62 -34
  45. data/lib/action_controller/metal/rescue.rb +1 -1
  46. data/lib/action_controller/metal/streaming.rb +0 -1
  47. data/lib/action_controller/metal/strong_parameters.rb +167 -58
  48. data/lib/action_controller/metal/url_for.rb +1 -1
  49. data/lib/action_controller/railties/helpers.rb +1 -1
  50. data/lib/action_controller/renderer.rb +37 -13
  51. data/lib/action_controller/template_assertions.rb +1 -1
  52. data/lib/action_controller/test_case.rb +70 -65
  53. data/lib/action_dispatch.rb +9 -3
  54. data/lib/action_dispatch/http/cache.rb +26 -21
  55. data/lib/action_dispatch/http/content_disposition.rb +45 -0
  56. data/lib/action_dispatch/http/content_security_policy.rb +33 -19
  57. data/lib/action_dispatch/http/filter_parameters.rb +9 -8
  58. data/lib/action_dispatch/http/filter_redirect.rb +2 -3
  59. data/lib/action_dispatch/http/headers.rb +4 -4
  60. data/lib/action_dispatch/http/mime_negotiation.rb +26 -13
  61. data/lib/action_dispatch/http/mime_type.rb +42 -23
  62. data/lib/action_dispatch/http/parameters.rb +14 -23
  63. data/lib/action_dispatch/http/permissions_policy.rb +173 -0
  64. data/lib/action_dispatch/http/request.rb +45 -22
  65. data/lib/action_dispatch/http/response.rb +45 -25
  66. data/lib/action_dispatch/http/upload.rb +9 -1
  67. data/lib/action_dispatch/http/url.rb +82 -82
  68. data/lib/action_dispatch/journey.rb +0 -2
  69. data/lib/action_dispatch/journey/formatter.rb +54 -30
  70. data/lib/action_dispatch/journey/gtg/builder.rb +22 -37
  71. data/lib/action_dispatch/journey/gtg/simulator.rb +8 -7
  72. data/lib/action_dispatch/journey/gtg/transition_table.rb +6 -5
  73. data/lib/action_dispatch/journey/nfa/dot.rb +0 -11
  74. data/lib/action_dispatch/journey/nodes/node.rb +13 -11
  75. data/lib/action_dispatch/journey/parser.rb +13 -13
  76. data/lib/action_dispatch/journey/parser.y +1 -1
  77. data/lib/action_dispatch/journey/path/pattern.rb +19 -21
  78. data/lib/action_dispatch/journey/route.rb +10 -20
  79. data/lib/action_dispatch/journey/router.rb +26 -34
  80. data/lib/action_dispatch/journey/router/utils.rb +14 -12
  81. data/lib/action_dispatch/journey/routes.rb +0 -2
  82. data/lib/action_dispatch/journey/scanner.rb +10 -4
  83. data/lib/action_dispatch/journey/visitors.rb +1 -4
  84. data/lib/action_dispatch/middleware/actionable_exceptions.rb +46 -0
  85. data/lib/action_dispatch/middleware/callbacks.rb +2 -4
  86. data/lib/action_dispatch/middleware/cookies.rb +128 -109
  87. data/lib/action_dispatch/middleware/debug_exceptions.rb +43 -66
  88. data/lib/action_dispatch/middleware/debug_locks.rb +5 -5
  89. data/lib/action_dispatch/middleware/debug_view.rb +66 -0
  90. data/lib/action_dispatch/middleware/exception_wrapper.rb +75 -30
  91. data/lib/action_dispatch/middleware/flash.rb +1 -1
  92. data/lib/action_dispatch/middleware/host_authorization.rb +121 -0
  93. data/lib/action_dispatch/middleware/public_exceptions.rb +6 -3
  94. data/lib/action_dispatch/middleware/remote_ip.rb +14 -16
  95. data/lib/action_dispatch/middleware/request_id.rb +5 -6
  96. data/lib/action_dispatch/middleware/session/abstract_store.rb +2 -3
  97. data/lib/action_dispatch/middleware/session/cookie_store.rb +3 -9
  98. data/lib/action_dispatch/middleware/show_exceptions.rb +3 -2
  99. data/lib/action_dispatch/middleware/ssl.rb +20 -15
  100. data/lib/action_dispatch/middleware/stack.rb +56 -2
  101. data/lib/action_dispatch/middleware/static.rb +153 -93
  102. data/lib/action_dispatch/middleware/templates/rescues/_actions.html.erb +13 -0
  103. data/lib/action_dispatch/middleware/templates/rescues/_actions.text.erb +0 -0
  104. data/lib/action_dispatch/middleware/templates/rescues/_message_and_suggestions.html.erb +22 -0
  105. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb +3 -1
  106. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.text.erb +1 -1
  107. data/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +4 -2
  108. data/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +45 -35
  109. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb +7 -0
  110. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.text.erb +5 -0
  111. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +23 -4
  112. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb +1 -1
  113. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +6 -3
  114. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +3 -1
  115. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +104 -8
  116. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb +19 -0
  117. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.text.erb +3 -0
  118. data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +2 -2
  119. data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +1 -1
  120. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +2 -2
  121. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +1 -1
  122. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +24 -1
  123. data/lib/action_dispatch/railtie.rb +8 -2
  124. data/lib/action_dispatch/request/session.rb +10 -9
  125. data/lib/action_dispatch/request/utils.rb +26 -2
  126. data/lib/action_dispatch/routing.rb +21 -20
  127. data/lib/action_dispatch/routing/inspector.rb +100 -52
  128. data/lib/action_dispatch/routing/mapper.rb +155 -103
  129. data/lib/action_dispatch/routing/polymorphic_routes.rb +13 -15
  130. data/lib/action_dispatch/routing/redirection.rb +3 -3
  131. data/lib/action_dispatch/routing/route_set.rb +71 -69
  132. data/lib/action_dispatch/routing/url_for.rb +2 -2
  133. data/lib/action_dispatch/system_test_case.rb +54 -11
  134. data/lib/action_dispatch/system_testing/browser.rb +53 -16
  135. data/lib/action_dispatch/system_testing/driver.rb +11 -3
  136. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +49 -7
  137. data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +8 -10
  138. data/lib/action_dispatch/testing/assertion_response.rb +0 -1
  139. data/lib/action_dispatch/testing/assertions.rb +1 -1
  140. data/lib/action_dispatch/testing/assertions/response.rb +4 -7
  141. data/lib/action_dispatch/testing/assertions/routing.rb +20 -8
  142. data/lib/action_dispatch/testing/integration.rb +61 -28
  143. data/lib/action_dispatch/testing/request_encoder.rb +2 -2
  144. data/lib/action_dispatch/testing/test_process.rb +29 -4
  145. data/lib/action_dispatch/testing/test_request.rb +3 -3
  146. data/lib/action_dispatch/testing/test_response.rb +4 -32
  147. data/lib/action_pack.rb +1 -1
  148. data/lib/action_pack/gem_version.rb +4 -4
  149. metadata +38 -26
  150. data/lib/action_controller/metal/force_ssl.rb +0 -99
  151. data/lib/action_dispatch/http/parameter_filter.rb +0 -86
  152. data/lib/action_dispatch/journey/nfa/builder.rb +0 -78
  153. data/lib/action_dispatch/journey/nfa/simulator.rb +0 -49
  154. data/lib/action_dispatch/journey/nfa/transition_table.rb +0 -120
  155. data/lib/action_dispatch/system_testing/test_helpers/undef_methods.rb +0 -26
@@ -21,14 +21,17 @@ module ActionDispatch
21
21
  def call(env)
22
22
  request = ActionDispatch::Request.new(env)
23
23
  status = request.path_info[1..-1].to_i
24
- content_type = request.formats.first
25
- body = { status: status, error: Rack::Utils::HTTP_STATUS_CODES.fetch(status, Rack::Utils::HTTP_STATUS_CODES[500]) }
24
+ begin
25
+ content_type = request.formats.first
26
+ rescue ActionDispatch::Http::MimeNegotiation::InvalidType
27
+ content_type = Mime[:text]
28
+ end
29
+ body = { status: status, error: Rack::Utils::HTTP_STATUS_CODES.fetch(status, Rack::Utils::HTTP_STATUS_CODES[500]) }
26
30
 
27
31
  render(status, content_type, body)
28
32
  end
29
33
 
30
34
  private
31
-
32
35
  def render(status, content_type, body)
33
36
  format = "to_#{content_type.to_sym}" if content_type
34
37
  if format && body.respond_to?(format)
@@ -8,13 +8,13 @@ module ActionDispatch
8
8
  # contain the address, and then picking the last-set address that is not
9
9
  # on the list of trusted IPs. This follows the precedent set by e.g.
10
10
  # {the Tomcat server}[https://issues.apache.org/bugzilla/show_bug.cgi?id=50453],
11
- # with {reasoning explained at length}[http://blog.gingerlime.com/2012/rails-ip-spoofing-vulnerabilities-and-protection]
11
+ # with {reasoning explained at length}[https://blog.gingerlime.com/2012/rails-ip-spoofing-vulnerabilities-and-protection]
12
12
  # by @gingerlime. A more detailed explanation of the algorithm is given
13
13
  # at GetIp#calculate_ip.
14
14
  #
15
15
  # Some Rack servers concatenate repeated headers, like {HTTP RFC 2616}[https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2]
16
16
  # requires. Some Rack servers simply drop preceding headers, and only report
17
- # the value that was {given in the last header}[http://andre.arko.net/2011/12/26/repeated-headers-and-ruby-web-servers].
17
+ # the value that was {given in the last header}[https://andre.arko.net/2011/12/26/repeated-headers-and-ruby-web-servers].
18
18
  # If you are behind multiple proxy servers (like NGINX to HAProxy to Unicorn)
19
19
  # then you should test your Rack server to make sure your data is good.
20
20
  #
@@ -33,7 +33,7 @@ module ActionDispatch
33
33
  # not be the ultimate client IP in production, and so are discarded. See
34
34
  # https://en.wikipedia.org/wiki/Private_network for details.
35
35
  TRUSTED_PROXIES = [
36
- "127.0.0.1", # localhost IPv4
36
+ "127.0.0.0/8", # localhost IPv4 range, per RFC-3330
37
37
  "::1", # localhost IPv6
38
38
  "fc00::/7", # private IPv6 range fc00::/7
39
39
  "10.0.0.0/8", # private IPv4 range 10.x.x.x
@@ -102,7 +102,7 @@ module ActionDispatch
102
102
  # proxies, that header may contain a list of IPs. Other proxy services
103
103
  # set the Client-Ip header instead, so we check that too.
104
104
  #
105
- # As discussed in {this post about Rails IP Spoofing}[http://blog.gingerlime.com/2012/rails-ip-spoofing-vulnerabilities-and-protection/],
105
+ # As discussed in {this post about Rails IP Spoofing}[https://blog.gingerlime.com/2012/rails-ip-spoofing-vulnerabilities-and-protection/],
106
106
  # while the first IP in the list is likely to be the "originating" IP,
107
107
  # it could also have been set by the client maliciously.
108
108
  #
@@ -143,10 +143,11 @@ module ActionDispatch
143
143
  # - X-Forwarded-For will be a list of IPs, one per proxy, or blank
144
144
  # - Client-Ip is propagated from the outermost proxy, or is blank
145
145
  # - REMOTE_ADDR will be the IP that made the request to Rack
146
- ips = [forwarded_ips, client_ips, remote_addr].flatten.compact
146
+ ips = [forwarded_ips, client_ips].flatten.compact
147
147
 
148
- # If every single IP option is in the trusted list, just return REMOTE_ADDR
149
- filter_proxies(ips).first || remote_addr
148
+ # If every single IP option is in the trusted list, return the IP
149
+ # that's furthest away
150
+ filter_proxies(ips + [remote_addr]).first || ips.last || remote_addr
150
151
  end
151
152
 
152
153
  # Memoizes the value returned by #calculate_ip and returns it for
@@ -156,20 +157,17 @@ module ActionDispatch
156
157
  end
157
158
 
158
159
  private
159
-
160
160
  def ips_from(header) # :doc:
161
161
  return [] unless header
162
162
  # Split the comma-separated list into an array of strings.
163
163
  ips = header.strip.split(/[,\s]+/)
164
164
  ips.select do |ip|
165
- begin
166
- # Only return IPs that are valid according to the IPAddr#new method.
167
- range = IPAddr.new(ip).to_range
168
- # We want to make sure nobody is sneaking a netmask in.
169
- range.begin == range.end
170
- rescue ArgumentError
171
- nil
172
- end
165
+ # Only return IPs that are valid according to the IPAddr#new method.
166
+ range = IPAddr.new(ip).to_range
167
+ # We want to make sure nobody is sneaking a netmask in.
168
+ range.begin == range.end
169
+ rescue ArgumentError
170
+ nil
173
171
  end
174
172
  end
175
173
 
@@ -15,22 +15,21 @@ module ActionDispatch
15
15
  # The unique request id can be used to trace a request end-to-end and would typically end up being part of log files
16
16
  # from multiple pieces of the stack.
17
17
  class RequestId
18
- X_REQUEST_ID = "X-Request-Id".freeze #:nodoc:
19
-
20
- def initialize(app)
18
+ def initialize(app, header:)
21
19
  @app = app
20
+ @header = header
22
21
  end
23
22
 
24
23
  def call(env)
25
24
  req = ActionDispatch::Request.new env
26
- req.request_id = make_request_id(req.x_request_id)
27
- @app.call(env).tap { |_status, headers, _body| headers[X_REQUEST_ID] = req.request_id }
25
+ req.request_id = make_request_id(req.headers[@header])
26
+ @app.call(env).tap { |_status, headers, _body| headers[@header] = req.request_id }
28
27
  end
29
28
 
30
29
  private
31
30
  def make_request_id(request_id)
32
31
  if request_id.presence
33
- request_id.gsub(/[^\w\-@]/, "".freeze).first(255)
32
+ request_id.gsub(/[^\w\-@]/, "").first(255)
34
33
  else
35
34
  internal_request_id
36
35
  end
@@ -30,7 +30,6 @@ module ActionDispatch
30
30
  end
31
31
 
32
32
  private
33
-
34
33
  def initialize_sid # :doc:
35
34
  @default_options.delete(:sidbits)
36
35
  @default_options.delete(:secure_random)
@@ -83,7 +82,7 @@ module ActionDispatch
83
82
  include SessionObject
84
83
 
85
84
  private
86
- def set_cookie(request, session_id, cookie)
85
+ def set_cookie(request, response, cookie)
87
86
  request.cookie_jar[key] = cookie
88
87
  end
89
88
  end
@@ -98,7 +97,7 @@ module ActionDispatch
98
97
  end
99
98
 
100
99
  private
101
- def set_cookie(request, session_id, cookie)
100
+ def set_cookie(request, response, cookie)
102
101
  request.cookie_jar[key] = cookie
103
102
  end
104
103
  end
@@ -10,22 +10,17 @@ module ActionDispatch
10
10
  # dramatically faster than the alternatives.
11
11
  #
12
12
  # Sessions typically contain at most a user_id and flash message; both fit
13
- # within the 4K cookie size limit. A CookieOverflow exception is raised if
14
- # you attempt to store more than 4K of data.
13
+ # within the 4096 bytes cookie size limit. A CookieOverflow exception is raised if
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
- # If you only have secret_token set, your cookies will be signed, but
20
- # not encrypted. This means a user cannot alter their +user_id+ without
21
- # knowing your app's secret key, but can easily read their +user_id+. This
22
- # was the default for Rails 3 apps.
23
- #
24
19
  # Your cookies will be encrypted using your apps secret_key_base. This
25
20
  # goes a step further than signed cookies in that encrypted cookies cannot
26
21
  # be altered or read by users. This is the default starting in Rails 4.
27
22
  #
28
- # Configure your session store in <tt>config/initializers/session_store.rb</tt>:
23
+ # Configure your session store in an initializer:
29
24
  #
30
25
  # Rails.application.config.session_store :cookie_store, key: '_your_app_session'
31
26
  #
@@ -81,7 +76,6 @@ module ActionDispatch
81
76
  end
82
77
 
83
78
  private
84
-
85
79
  def extract_session_id(req)
86
80
  stale_session_check! do
87
81
  sid = unpacked_cookie_data(req)["session_id"]
@@ -40,14 +40,15 @@ module ActionDispatch
40
40
  end
41
41
 
42
42
  private
43
-
44
43
  def render_exception(request, exception)
45
44
  backtrace_cleaner = request.get_header "action_dispatch.backtrace_cleaner"
46
45
  wrapper = ExceptionWrapper.new(backtrace_cleaner, exception)
47
46
  status = wrapper.status_code
48
- request.set_header "action_dispatch.exception", wrapper.exception
47
+ request.set_header "action_dispatch.exception", wrapper.unwrapped_exception
49
48
  request.set_header "action_dispatch.original_path", request.path_info
49
+ request.set_header "action_dispatch.original_request_method", request.raw_request_method
50
50
  request.path_info = "/#{status}"
51
+ request.request_method = "GET"
51
52
  response = @exceptions_app.call(request.env)
52
53
  response[1]["X-Cascade"] == "pass" ? pass_response(status) : response
53
54
  rescue Exception => failsafe_error
@@ -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
@@ -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) if respond_to?(:ruby2_keywords, true)
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) if respond_to?(:ruby2_keywords, true)
78
105
 
79
106
  alias_method :insert_before, :insert
80
107
 
@@ -82,27 +109,54 @@ module ActionDispatch
82
109
  index = assert_index(index, :after)
83
110
  insert(index + 1, *args, &block)
84
111
  end
112
+ ruby2_keywords(:insert_after) if respond_to?(:ruby2_keywords, true)
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) if respond_to?(:ruby2_keywords, true)
91
120
 
92
121
  def delete(target)
93
122
  middlewares.delete_if { |m| m.klass == target }
94
123
  end
95
124
 
125
+ def move(target, source)
126
+ source_index = assert_index(source, :before)
127
+ source_middleware = middlewares.delete_at(source_index)
128
+
129
+ target_index = assert_index(target, :before)
130
+ middlewares.insert(target_index, source_middleware)
131
+ end
132
+
133
+ alias_method :move_before, :move
134
+
135
+ def move_after(target, source)
136
+ source_index = assert_index(source, :after)
137
+ source_middleware = middlewares.delete_at(source_index)
138
+
139
+ target_index = assert_index(target, :after)
140
+ middlewares.insert(target_index + 1, source_middleware)
141
+ end
142
+
96
143
  def use(klass, *args, &block)
97
144
  middlewares.push(build_middleware(klass, args, block))
98
145
  end
146
+ ruby2_keywords(:use) if respond_to?(:ruby2_keywords, true)
99
147
 
100
148
  def build(app = nil, &block)
101
- middlewares.freeze.reverse.inject(app || block) { |a, e| e.build(a) }
149
+ instrumenting = ActiveSupport::Notifications.notifier.listening?(InstrumentationProxy::EVENT_NAME)
150
+ middlewares.freeze.reverse.inject(app || block) do |a, e|
151
+ if instrumenting
152
+ e.build_instrumented(a)
153
+ else
154
+ e.build(a)
155
+ end
156
+ end
102
157
  end
103
158
 
104
159
  private
105
-
106
160
  def assert_index(index, where)
107
161
  i = index.is_a?(Integer) ? index : middlewares.index { |m| m.klass == index }
108
162
  raise "No such middleware to insert #{where}: #{index.inspect}" unless i
@@ -4,127 +4,187 @@ require "rack/utils"
4
4
  require "active_support/core_ext/uri"
5
5
 
6
6
  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.
7
+ # This middleware serves static files from disk, if available.
8
+ # If no file is found, it hands off to the main app.
10
9
  #
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
10
+ # In Rails apps, this middleware is configured to serve assets from
11
+ # the +public/+ directory.
12
+ #
13
+ # Only GET and HEAD requests are served. POST and other HTTP methods
14
+ # are handed off to the main app.
15
+ #
16
+ # Only files in the root directory are served; path traversal is denied.
17
+ class Static
18
+ def initialize(app, path, index: "index", headers: {})
19
+ @app = app
20
+ @file_handler = FileHandler.new(path, index: index, headers: headers)
22
21
  end
23
22
 
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
23
+ def call(env)
24
+ @file_handler.attempt(env) || @app.call(env)
25
+ end
26
+ end
44
27
 
45
- }
46
- return ::Rack::Utils.escape_path(match).b
47
- end
28
+ # This endpoint serves static files from disk using Rack::File.
29
+ #
30
+ # URL paths are matched with static files according to expected
31
+ # conventions: +path+, +path+.html, +path+/index.html.
32
+ #
33
+ # Precompressed versions of these files are checked first. Brotli (.br)
34
+ # and gzip (.gz) files are supported. If +path+.br exists, this
35
+ # endpoint returns that file with a +Content-Encoding: br+ header.
36
+ #
37
+ # If no matching file is found, this endpoint responds 404 Not Found.
38
+ #
39
+ # Pass the +root+ directory to search for matching files, an optional
40
+ # +index: "index"+ to change the default +path+/index.html, and optional
41
+ # additional response headers.
42
+ class FileHandler
43
+ # Accept-Encoding value -> file extension
44
+ PRECOMPRESSED = {
45
+ "br" => ".br",
46
+ "gzip" => ".gz",
47
+ "identity" => nil
48
+ }
49
+
50
+ def initialize(root, index: "index", headers: {}, precompressed: %i[ br gzip ], compressible_content_types: /\A(?:text\/|application\/javascript)/)
51
+ @root = root.chomp("/").b
52
+ @index = index
53
+
54
+ @precompressed = Array(precompressed).map(&:to_s) | %w[ identity ]
55
+ @compressible_content_types = compressible_content_types
56
+
57
+ @file_server = ::Rack::File.new(@root, headers)
48
58
  end
49
59
 
50
60
  def call(env)
51
- serve(Rack::Request.new(env))
61
+ attempt(env) || @file_server.call(env)
52
62
  end
53
63
 
54
- def serve(request)
55
- path = request.path_info
56
- gzip_path = gzip_file_path(path)
64
+ def attempt(env)
65
+ request = Rack::Request.new env
57
66
 
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]
67
+ if request.get? || request.head?
68
+ if found = find_file(request.path_info, accept_encoding: request.accept_encoding)
69
+ serve request, *found
63
70
  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
71
  end
69
-
70
- headers["Vary"] = "Accept-Encoding" if gzip_path
71
-
72
- return [status, headers, body]
73
- ensure
74
- request.path_info = path
75
72
  end
76
73
 
77
74
  private
78
- def ext
79
- ::ActionController::Base.default_static_extension
75
+ def serve(request, filepath, content_headers)
76
+ original, request.path_info =
77
+ request.path_info, ::Rack::Utils.escape_path(filepath).b
78
+
79
+ @file_server.call(request.env).tap do |status, headers, body|
80
+ # Omit Content-Encoding/Type/etc headers for 304 Not Modified
81
+ if status != 304
82
+ headers.update(content_headers)
83
+ end
84
+ end
85
+ ensure
86
+ request.path_info = original
80
87
  end
81
88
 
82
- def content_type(path)
83
- ::Rack::Mime.mime_type(::File.extname(path), "text/plain".freeze)
89
+ # Match a URI path to a static file to be served.
90
+ #
91
+ # Used by the +Static+ class to negotiate a servable file in the
92
+ # +public/+ directory (see Static#call).
93
+ #
94
+ # Checks for +path+, +path+.html, and +path+/index.html files,
95
+ # in that order, including .br and .gzip compressed extensions.
96
+ #
97
+ # If a matching file is found, the path and necessary response headers
98
+ # (Content-Type, Content-Encoding) are returned.
99
+ def find_file(path_info, accept_encoding:)
100
+ each_candidate_filepath(path_info) do |filepath, content_type|
101
+ if response = try_files(filepath, content_type, accept_encoding: accept_encoding)
102
+ return response
103
+ end
104
+ end
84
105
  end
85
106
 
86
- def gzip_encoding_accepted?(request)
87
- request.accept_encoding.any? { |enc, quality| enc =~ /\bgzip\b/i }
107
+ def try_files(filepath, content_type, accept_encoding:)
108
+ headers = { "Content-Type" => content_type }
109
+
110
+ if compressible? content_type
111
+ try_precompressed_files filepath, headers, accept_encoding: accept_encoding
112
+ elsif file_readable? filepath
113
+ [ filepath, headers ]
114
+ end
88
115
  end
89
116
 
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
117
+ def try_precompressed_files(filepath, headers, accept_encoding:)
118
+ each_precompressed_filepath(filepath) do |content_encoding, precompressed_filepath|
119
+ if file_readable? precompressed_filepath
120
+ # Identity encoding is default, so we skip Accept-Encoding
121
+ # negotiation and needn't set Content-Encoding.
122
+ #
123
+ # Vary header is expected when we've found other available
124
+ # encodings that Accept-Encoding ruled out.
125
+ if content_encoding == "identity"
126
+ return precompressed_filepath, headers
127
+ else
128
+ headers["Vary"] = "Accept-Encoding"
129
+
130
+ if accept_encoding.any? { |enc, _| /\b#{content_encoding}\b/i.match?(enc) }
131
+ headers["Content-Encoding"] = content_encoding
132
+ return precompressed_filepath, headers
133
+ end
134
+ end
135
+ end
97
136
  end
98
137
  end
99
- end
100
138
 
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
139
+ 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?
145
+ end
115
146
 
116
- def call(env)
117
- req = Rack::Request.new env
147
+ def compressible?(content_type)
148
+ @compressible_content_types.match?(content_type)
149
+ end
118
150
 
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)
151
+ def each_precompressed_filepath(filepath)
152
+ @precompressed.each do |content_encoding|
153
+ precompressed_ext = PRECOMPRESSED.fetch(content_encoding)
154
+ yield content_encoding, "#{filepath}#{precompressed_ext}"
124
155
  end
156
+
157
+ nil
125
158
  end
126
159
 
127
- @app.call(req.env)
128
- end
160
+ def each_candidate_filepath(path_info)
161
+ return unless path = clean_path(path_info)
162
+
163
+ ext = ::File.extname(path)
164
+ content_type = ::Rack::Mime.mime_type(ext, nil)
165
+ yield path, content_type || "text/plain"
166
+
167
+ # Tack on .html and /index.html only for paths that don't have
168
+ # an explicit, resolvable file extension. No need to check
169
+ # for foo.js.html and foo.js/index.html.
170
+ unless content_type
171
+ default_ext = ::ActionController::Base.default_static_extension
172
+ if ext != default_ext
173
+ default_content_type = ::Rack::Mime.mime_type(default_ext, "text/plain")
174
+
175
+ yield "#{path}#{default_ext}", default_content_type
176
+ yield "#{path}/#{@index}#{default_ext}", default_content_type
177
+ end
178
+ end
179
+
180
+ nil
181
+ end
182
+
183
+ def clean_path(path_info)
184
+ path = ::Rack::Utils.unescape_path path_info.chomp("/")
185
+ if ::Rack::Utils.valid_path? path
186
+ ::Rack::Utils.clean_path_info path
187
+ end
188
+ end
129
189
  end
130
190
  end