1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
|
require 'rack/utils'
require 'active_support/core_ext/uri'
module ActionDispatch
# This middleware returns a file's contents from disk in the body response.
# When initialized, it can accept optional HTTP headers, which will be set
# when a response containing a file's contents is delivered.
#
# This middleware will render the file specified in `env["PATH_INFO"]`
# where the base path is in the +root+ directory. For example, if the +root+
# is set to `public/`, then a request with `env["PATH_INFO"]` of
# `assets/application.js` will return a response with the contents of a file
# located at `public/assets/application.js` if the file exists. If the file
# does not exist, a 404 "File not Found" response will be returned.
class FileHandler
def initialize(root, index: 'index', headers: {})
@root = root.chomp('/')
@file_server = ::Rack::File.new(@root, headers)
@index = index
end
# Takes a path to a file. If the file is found, has valid encoding, and has
# correct read permissions, the return value is a URI-escaped string
# representing the filename. Otherwise, false is returned.
#
# Used by the `Static` class to check the existence of a valid file
# in the server's `public/` directory (see Static#call).
def match?(path)
path = ::Rack::Utils.unescape_path path
return false unless valid_path?(path)
path = Rack::Utils.clean_path_info path
paths = [path, "#{path}#{ext}", "#{path}/#{@index}#{ext}"]
if match = paths.detect { |p|
path = File.join(@root, p.force_encoding('UTF-8'.freeze))
begin
File.file?(path) && File.readable?(path)
rescue SystemCallError
false
end
}
return ::Rack::Utils.escape_path(match)
end
end
def call(env)
serve ActionDispatch::Request.new env
end
def serve(request)
path = request.path_info
gzip_path = gzip_file_path(path)
if gzip_path && gzip_encoding_accepted?(request)
request.path_info = gzip_path
status, headers, body = @file_server.call(request.env)
if status == 304
return [status, headers, body]
end
headers['Content-Encoding'] = 'gzip'
headers['Content-Type'] = content_type(path)
else
status, headers, body = @file_server.call(request.env)
end
headers['Vary'] = 'Accept-Encoding' if gzip_path
return [status, headers, body]
ensure
request.path_info = path
end
private
def ext
::ActionController::Base.default_static_extension
end
def content_type(path)
::Rack::Mime.mime_type(::File.extname(path), 'text/plain'.freeze)
end
def gzip_encoding_accepted?(request)
request.accept_encoding =~ /\bgzip\b/i
end
def gzip_file_path(path)
can_gzip_mime = content_type(path) =~ /\A(?:text\/|application\/javascript)/
gzip_path = "#{path}.gz"
if can_gzip_mime && File.exist?(File.join(@root, ::Rack::Utils.unescape_path(gzip_path)))
gzip_path
else
false
end
end
def valid_path?(path)
path.valid_encoding? && !path.include?("\0")
end
end
# This middleware will attempt to return the contents of a file's body from
# disk in the response. If a file is not found on disk, the request will be
# delegated to the application stack. This middleware is commonly initialized
# to serve assets from a server's `public/` directory.
#
# This middleware verifies the path to ensure that only files
# living in the root directory can be rendered. A request cannot
# produce a directory traversal using this middleware. Only 'GET' and 'HEAD'
# requests will result in a file being returned.
class Static
def initialize(app, path, deprecated_cache_control = :not_set, index: 'index', headers: {})
if deprecated_cache_control != :not_set
ActiveSupport::Deprecation.warn("The `cache_control` argument is deprecated," \
"replaced by `headers: { 'Cache-Control' => #{deprecated_cache_control} }`, " \
" and will be removed in Rails 5.1.")
headers['Cache-Control'.freeze] = deprecated_cache_control
end
@app = app
@file_handler = FileHandler.new(path, index: index, headers: headers)
end
def call(env)
req = ActionDispatch::Request.new env
if req.get? || req.head?
path = req.path_info.chomp('/'.freeze)
if match = @file_handler.match?(path)
req.path_info = match
return @file_handler.serve(req)
end
end
@app.call(req.env)
end
end
end
|