actionpack 6.0.3.7 → 6.1.3.2

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 (118) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +287 -226
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +1 -1
  5. data/lib/abstract_controller.rb +1 -0
  6. data/lib/abstract_controller/base.rb +35 -2
  7. data/lib/abstract_controller/callbacks.rb +2 -2
  8. data/lib/abstract_controller/helpers.rb +105 -90
  9. data/lib/abstract_controller/railties/routes_helpers.rb +17 -1
  10. data/lib/abstract_controller/rendering.rb +9 -9
  11. data/lib/abstract_controller/translation.rb +8 -2
  12. data/lib/action_controller.rb +2 -3
  13. data/lib/action_controller/api.rb +2 -2
  14. data/lib/action_controller/base.rb +4 -2
  15. data/lib/action_controller/caching.rb +0 -1
  16. data/lib/action_controller/log_subscriber.rb +3 -3
  17. data/lib/action_controller/metal.rb +2 -2
  18. data/lib/action_controller/metal/conditional_get.rb +11 -3
  19. data/lib/action_controller/metal/content_security_policy.rb +1 -1
  20. data/lib/action_controller/metal/cookies.rb +3 -1
  21. data/lib/action_controller/metal/data_streaming.rb +1 -1
  22. data/lib/action_controller/metal/etag_with_template_digest.rb +2 -4
  23. data/lib/action_controller/metal/exceptions.rb +33 -0
  24. data/lib/action_controller/metal/head.rb +7 -4
  25. data/lib/action_controller/metal/helpers.rb +11 -1
  26. data/lib/action_controller/metal/http_authentication.rb +4 -2
  27. data/lib/action_controller/metal/implicit_render.rb +1 -1
  28. data/lib/action_controller/metal/instrumentation.rb +11 -9
  29. data/lib/action_controller/metal/live.rb +1 -1
  30. data/lib/action_controller/metal/logging.rb +20 -0
  31. data/lib/action_controller/metal/mime_responds.rb +6 -2
  32. data/lib/action_controller/metal/parameter_encoding.rb +35 -4
  33. data/lib/action_controller/metal/params_wrapper.rb +14 -8
  34. data/lib/action_controller/metal/permissions_policy.rb +46 -0
  35. data/lib/action_controller/metal/redirecting.rb +1 -1
  36. data/lib/action_controller/metal/rendering.rb +6 -0
  37. data/lib/action_controller/metal/request_forgery_protection.rb +48 -24
  38. data/lib/action_controller/metal/rescue.rb +1 -1
  39. data/lib/action_controller/metal/strong_parameters.rb +103 -15
  40. data/lib/action_controller/renderer.rb +24 -13
  41. data/lib/action_controller/test_case.rb +62 -56
  42. data/lib/action_dispatch.rb +3 -2
  43. data/lib/action_dispatch/http/cache.rb +12 -10
  44. data/lib/action_dispatch/http/content_disposition.rb +2 -2
  45. data/lib/action_dispatch/http/content_security_policy.rb +5 -1
  46. data/lib/action_dispatch/http/filter_parameters.rb +1 -1
  47. data/lib/action_dispatch/http/filter_redirect.rb +1 -1
  48. data/lib/action_dispatch/http/headers.rb +3 -2
  49. data/lib/action_dispatch/http/mime_negotiation.rb +20 -8
  50. data/lib/action_dispatch/http/mime_type.rb +29 -16
  51. data/lib/action_dispatch/http/parameters.rb +1 -19
  52. data/lib/action_dispatch/http/permissions_policy.rb +173 -0
  53. data/lib/action_dispatch/http/request.rb +26 -8
  54. data/lib/action_dispatch/http/response.rb +17 -16
  55. data/lib/action_dispatch/http/url.rb +3 -2
  56. data/lib/action_dispatch/journey.rb +0 -2
  57. data/lib/action_dispatch/journey/formatter.rb +53 -28
  58. data/lib/action_dispatch/journey/gtg/builder.rb +22 -36
  59. data/lib/action_dispatch/journey/gtg/simulator.rb +8 -7
  60. data/lib/action_dispatch/journey/gtg/transition_table.rb +6 -4
  61. data/lib/action_dispatch/journey/nfa/dot.rb +0 -11
  62. data/lib/action_dispatch/journey/nodes/node.rb +4 -3
  63. data/lib/action_dispatch/journey/parser.rb +13 -13
  64. data/lib/action_dispatch/journey/parser.y +1 -1
  65. data/lib/action_dispatch/journey/path/pattern.rb +13 -18
  66. data/lib/action_dispatch/journey/route.rb +7 -18
  67. data/lib/action_dispatch/journey/router.rb +26 -30
  68. data/lib/action_dispatch/journey/router/utils.rb +6 -4
  69. data/lib/action_dispatch/middleware/actionable_exceptions.rb +2 -2
  70. data/lib/action_dispatch/middleware/cookies.rb +74 -33
  71. data/lib/action_dispatch/middleware/debug_exceptions.rb +10 -17
  72. data/lib/action_dispatch/middleware/debug_view.rb +1 -1
  73. data/lib/action_dispatch/middleware/exception_wrapper.rb +29 -17
  74. data/lib/action_dispatch/middleware/host_authorization.rb +27 -7
  75. data/lib/action_dispatch/middleware/public_exceptions.rb +1 -1
  76. data/lib/action_dispatch/middleware/remote_ip.rb +5 -4
  77. data/lib/action_dispatch/middleware/request_id.rb +4 -5
  78. data/lib/action_dispatch/middleware/session/abstract_store.rb +2 -2
  79. data/lib/action_dispatch/middleware/session/cookie_store.rb +2 -2
  80. data/lib/action_dispatch/middleware/show_exceptions.rb +2 -0
  81. data/lib/action_dispatch/middleware/ssl.rb +12 -7
  82. data/lib/action_dispatch/middleware/stack.rb +19 -1
  83. data/lib/action_dispatch/middleware/static.rb +154 -93
  84. data/lib/action_dispatch/middleware/templates/rescues/_message_and_suggestions.html.erb +22 -0
  85. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +2 -5
  86. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +2 -2
  87. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +2 -2
  88. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +100 -8
  89. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +1 -1
  90. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +21 -1
  91. data/lib/action_dispatch/railtie.rb +3 -2
  92. data/lib/action_dispatch/request/session.rb +2 -8
  93. data/lib/action_dispatch/request/utils.rb +26 -2
  94. data/lib/action_dispatch/routing/inspector.rb +8 -7
  95. data/lib/action_dispatch/routing/mapper.rb +102 -71
  96. data/lib/action_dispatch/routing/polymorphic_routes.rb +12 -11
  97. data/lib/action_dispatch/routing/redirection.rb +4 -4
  98. data/lib/action_dispatch/routing/route_set.rb +49 -41
  99. data/lib/action_dispatch/routing/url_for.rb +1 -0
  100. data/lib/action_dispatch/system_test_case.rb +29 -24
  101. data/lib/action_dispatch/system_testing/browser.rb +33 -27
  102. data/lib/action_dispatch/system_testing/driver.rb +6 -7
  103. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +47 -6
  104. data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +4 -7
  105. data/lib/action_dispatch/testing/assertions.rb +1 -1
  106. data/lib/action_dispatch/testing/assertions/response.rb +2 -4
  107. data/lib/action_dispatch/testing/assertions/routing.rb +5 -5
  108. data/lib/action_dispatch/testing/integration.rb +40 -29
  109. data/lib/action_dispatch/testing/test_process.rb +29 -4
  110. data/lib/action_dispatch/testing/test_request.rb +3 -3
  111. data/lib/action_pack.rb +1 -1
  112. data/lib/action_pack/gem_version.rb +2 -2
  113. metadata +16 -17
  114. data/lib/action_controller/metal/force_ssl.rb +0 -58
  115. data/lib/action_dispatch/http/parameter_filter.rb +0 -12
  116. data/lib/action_dispatch/journey/nfa/builder.rb +0 -78
  117. data/lib/action_dispatch/journey/nfa/simulator.rb +0 -47
  118. data/lib/action_dispatch/journey/nfa/transition_table.rb +0 -119
@@ -23,7 +23,7 @@ module ActionDispatch
23
23
  status = request.path_info[1..-1].to_i
24
24
  begin
25
25
  content_type = request.formats.first
26
- rescue Mime::Type::InvalidMimeType
26
+ rescue ActionDispatch::Http::MimeNegotiation::InvalidType
27
27
  content_type = Mime[:text]
28
28
  end
29
29
  body = { status: status, error: Rack::Utils::HTTP_STATUS_CODES.fetch(status, Rack::Utils::HTTP_STATUS_CODES[500]) }
@@ -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
@@ -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
@@ -15,16 +15,15 @@ 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" #: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
@@ -82,7 +82,7 @@ module ActionDispatch
82
82
  include SessionObject
83
83
 
84
84
  private
85
- def set_cookie(request, session_id, cookie)
85
+ def set_cookie(request, response, cookie)
86
86
  request.cookie_jar[key] = cookie
87
87
  end
88
88
  end
@@ -97,7 +97,7 @@ module ActionDispatch
97
97
  end
98
98
 
99
99
  private
100
- def set_cookie(request, session_id, cookie)
100
+ def set_cookie(request, response, cookie)
101
101
  request.cookie_jar[key] = cookie
102
102
  end
103
103
  end
@@ -10,8 +10,8 @@ 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.
@@ -46,7 +46,9 @@ module ActionDispatch
46
46
  status = wrapper.status_code
47
47
  request.set_header "action_dispatch.exception", wrapper.unwrapped_exception
48
48
  request.set_header "action_dispatch.original_path", request.path_info
49
+ request.set_header "action_dispatch.original_request_method", request.raw_request_method
49
50
  request.path_info = "/#{status}"
51
+ request.request_method = "GET"
50
52
  response = @exceptions_app.call(request.env)
51
53
  response[1]["X-Cascade"] == "pass" ? pass_response(status) : response
52
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)
@@ -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
@@ -43,7 +43,7 @@ module ActionDispatch
43
43
  end
44
44
 
45
45
  # This class is used to instrument the execution of a single middleware.
46
- # It proxies the `call` method transparently and instruments the method
46
+ # It proxies the +call+ method transparently and instruments the method
47
47
  # call.
48
48
  class InstrumentationProxy
49
49
  EVENT_NAME = "process_middleware.action_dispatch"
@@ -122,6 +122,24 @@ module ActionDispatch
122
122
  middlewares.delete_if { |m| m.klass == target }
123
123
  end
124
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
+
125
143
  def use(klass, *args, &block)
126
144
  middlewares.push(build_middleware(klass, args, block))
127
145
  end
@@ -4,126 +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
44
- }
45
- ::Rack::Utils.escape_path(match).b
46
- end
23
+ def call(env)
24
+ @file_handler.attempt(env) || @app.call(env)
25
+ end
26
+ end
27
+
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 <tt>Content-Encoding: br</tt> 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
+ # <tt>index: "index"</tt> 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)
47
58
  end
48
59
 
49
60
  def call(env)
50
- serve(Rack::Request.new(env))
61
+ attempt(env) || @file_server.call(env)
51
62
  end
52
63
 
53
- def serve(request)
54
- path = request.path_info
55
- gzip_path = gzip_file_path(path)
64
+ def attempt(env)
65
+ request = Rack::Request.new env
56
66
 
57
- if gzip_path && gzip_encoding_accepted?(request)
58
- request.path_info = gzip_path
59
- status, headers, body = @file_server.call(request.env)
60
- if status == 304
61
- 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
62
70
  end
63
- headers["Content-Encoding"] = "gzip"
64
- headers["Content-Type"] = content_type(path)
65
- else
66
- status, headers, body = @file_server.call(request.env)
67
71
  end
68
-
69
- headers["Vary"] = "Accept-Encoding" if gzip_path
70
-
71
- [status, headers, body]
72
- ensure
73
- request.path_info = path
74
72
  end
75
73
 
76
74
  private
77
- def ext
78
- ::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
79
87
  end
80
88
 
81
- def content_type(path)
82
- ::Rack::Mime.mime_type(::File.extname(path), "text/plain")
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
83
105
  end
84
106
 
85
- def gzip_encoding_accepted?(request)
86
- 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
87
115
  end
88
116
 
89
- def gzip_file_path(path)
90
- can_gzip_mime = content_type(path) =~ /\A(?:text\/|application\/javascript)/
91
- gzip_path = "#{path}.gz"
92
- if can_gzip_mime && File.exist?(File.join(@root, ::Rack::Utils.unescape_path(gzip_path)))
93
- gzip_path
94
- else
95
- 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
96
136
  end
97
137
  end
98
- end
99
138
 
100
- # This middleware will attempt to return the contents of a file's body from
101
- # disk in the response. If a file is not found on disk, the request will be
102
- # delegated to the application stack. This middleware is commonly initialized
103
- # to serve assets from a server's +public/+ directory.
104
- #
105
- # This middleware verifies the path to ensure that only files
106
- # living in the root directory can be rendered. A request cannot
107
- # produce a directory traversal using this middleware. Only 'GET' and 'HEAD'
108
- # requests will result in a file being returned.
109
- class Static
110
- def initialize(app, path, index: "index", headers: {})
111
- @app = app
112
- @file_handler = FileHandler.new(path, index: index, headers: headers)
113
- 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
114
146
 
115
- def call(env)
116
- req = Rack::Request.new env
147
+ def compressible?(content_type)
148
+ @compressible_content_types.match?(content_type)
149
+ end
117
150
 
118
- if req.get? || req.head?
119
- path = req.path_info.chomp("/")
120
- if match = @file_handler.match?(path)
121
- req.path_info = match
122
- 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}"
123
155
  end
156
+
157
+ nil
124
158
  end
125
159
 
126
- @app.call(req.env)
127
- 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
128
189
  end
129
190
  end