rack 2.1.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of rack might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/CHANGELOG.md +377 -16
- data/CONTRIBUTING.md +144 -0
- data/MIT-LICENSE +1 -1
- data/README.md +328 -0
- data/SPEC.rdoc +365 -0
- data/lib/rack/auth/abstract/handler.rb +3 -1
- data/lib/rack/auth/abstract/request.rb +2 -2
- data/lib/rack/auth/basic.rb +4 -7
- data/lib/rack/bad_request.rb +8 -0
- data/lib/rack/body_proxy.rb +34 -12
- data/lib/rack/builder.rb +162 -59
- data/lib/rack/cascade.rb +24 -10
- data/lib/rack/common_logger.rb +43 -28
- data/lib/rack/conditional_get.rb +30 -25
- data/lib/rack/constants.rb +66 -0
- data/lib/rack/content_length.rb +10 -16
- data/lib/rack/content_type.rb +9 -7
- data/lib/rack/deflater.rb +78 -50
- data/lib/rack/directory.rb +86 -63
- data/lib/rack/etag.rb +14 -22
- data/lib/rack/events.rb +18 -17
- data/lib/rack/files.rb +99 -61
- data/lib/rack/head.rb +8 -9
- data/lib/rack/headers.rb +238 -0
- data/lib/rack/lint.rb +868 -642
- data/lib/rack/lock.rb +2 -6
- data/lib/rack/logger.rb +3 -0
- data/lib/rack/media_type.rb +9 -4
- data/lib/rack/method_override.rb +6 -2
- data/lib/rack/mime.rb +14 -5
- data/lib/rack/mock.rb +1 -253
- data/lib/rack/mock_request.rb +171 -0
- data/lib/rack/mock_response.rb +124 -0
- data/lib/rack/multipart/generator.rb +15 -8
- data/lib/rack/multipart/parser.rb +238 -107
- data/lib/rack/multipart/uploaded_file.rb +17 -7
- data/lib/rack/multipart.rb +54 -42
- data/lib/rack/null_logger.rb +9 -0
- data/lib/rack/query_parser.rb +87 -105
- data/lib/rack/recursive.rb +3 -1
- data/lib/rack/reloader.rb +0 -4
- data/lib/rack/request.rb +366 -135
- data/lib/rack/response.rb +186 -68
- data/lib/rack/rewindable_input.rb +24 -6
- data/lib/rack/runtime.rb +8 -7
- data/lib/rack/sendfile.rb +29 -27
- data/lib/rack/show_exceptions.rb +27 -12
- data/lib/rack/show_status.rb +21 -13
- data/lib/rack/static.rb +19 -12
- data/lib/rack/tempfile_reaper.rb +14 -5
- data/lib/rack/urlmap.rb +5 -6
- data/lib/rack/utils.rb +274 -260
- data/lib/rack/version.rb +21 -0
- data/lib/rack.rb +18 -103
- metadata +25 -52
- data/README.rdoc +0 -262
- data/Rakefile +0 -123
- data/SPEC +0 -263
- data/bin/rackup +0 -5
- data/contrib/rack.png +0 -0
- data/contrib/rack.svg +0 -150
- data/contrib/rack_logo.svg +0 -164
- data/contrib/rdoc.css +0 -412
- data/example/lobster.ru +0 -6
- data/example/protectedlobster.rb +0 -16
- data/example/protectedlobster.ru +0 -10
- data/lib/rack/auth/digest/md5.rb +0 -131
- data/lib/rack/auth/digest/nonce.rb +0 -54
- data/lib/rack/auth/digest/params.rb +0 -54
- data/lib/rack/auth/digest/request.rb +0 -43
- data/lib/rack/chunked.rb +0 -92
- data/lib/rack/core_ext/regexp.rb +0 -14
- data/lib/rack/file.rb +0 -8
- data/lib/rack/handler/cgi.rb +0 -62
- data/lib/rack/handler/fastcgi.rb +0 -102
- data/lib/rack/handler/lsws.rb +0 -63
- data/lib/rack/handler/scgi.rb +0 -73
- data/lib/rack/handler/thin.rb +0 -38
- data/lib/rack/handler/webrick.rb +0 -122
- data/lib/rack/handler.rb +0 -104
- data/lib/rack/lobster.rb +0 -72
- data/lib/rack/server.rb +0 -467
- data/lib/rack/session/abstract/id.rb +0 -528
- data/lib/rack/session/cookie.rb +0 -205
- data/lib/rack/session/memcache.rb +0 -10
- data/lib/rack/session/pool.rb +0 -85
- data/rack.gemspec +0 -44
data/lib/rack/directory.rb
CHANGED
@@ -1,9 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'time'
|
4
|
-
|
5
|
-
|
6
|
-
|
4
|
+
|
5
|
+
require_relative 'constants'
|
6
|
+
require_relative 'utils'
|
7
|
+
require_relative 'head'
|
8
|
+
require_relative 'mime'
|
9
|
+
require_relative 'files'
|
7
10
|
|
8
11
|
module Rack
|
9
12
|
# Rack::Directory serves entries below the +root+ given, according to the
|
@@ -14,8 +17,8 @@ module Rack
|
|
14
17
|
# If +app+ is not specified, a Rack::Files of the same +root+ will be used.
|
15
18
|
|
16
19
|
class Directory
|
17
|
-
DIR_FILE = "<tr><td class='name'><a href='%s'>%s</a></td><td class='size'>%s</td><td class='type'>%s</td><td class='mtime'>%s</td></tr
|
18
|
-
|
20
|
+
DIR_FILE = "<tr><td class='name'><a href='%s'>%s</a></td><td class='size'>%s</td><td class='type'>%s</td><td class='mtime'>%s</td></tr>\n"
|
21
|
+
DIR_PAGE_HEADER = <<-PAGE
|
19
22
|
<html><head>
|
20
23
|
<title>%s</title>
|
21
24
|
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
|
@@ -36,33 +39,51 @@ table { width:100%%; }
|
|
36
39
|
<th class='type'>Type</th>
|
37
40
|
<th class='mtime'>Last Modified</th>
|
38
41
|
</tr>
|
39
|
-
|
42
|
+
PAGE
|
43
|
+
DIR_PAGE_FOOTER = <<-PAGE
|
40
44
|
</table>
|
41
45
|
<hr />
|
42
46
|
</body></html>
|
43
47
|
PAGE
|
44
48
|
|
49
|
+
# Body class for directory entries, showing an index page with links
|
50
|
+
# to each file.
|
45
51
|
class DirectoryBody < Struct.new(:root, :path, :files)
|
52
|
+
# Yield strings for each part of the directory entry
|
46
53
|
def each
|
47
|
-
show_path =
|
48
|
-
|
49
|
-
|
50
|
-
|
54
|
+
show_path = Utils.escape_html(path.sub(/^#{root}/, ''))
|
55
|
+
yield(DIR_PAGE_HEADER % [ show_path, show_path ])
|
56
|
+
|
57
|
+
unless path.chomp('/') == root
|
58
|
+
yield(DIR_FILE % DIR_FILE_escape(files.call('..')))
|
59
|
+
end
|
60
|
+
|
61
|
+
Dir.foreach(path) do |basename|
|
62
|
+
next if basename.start_with?('.')
|
63
|
+
next unless f = files.call(basename)
|
64
|
+
yield(DIR_FILE % DIR_FILE_escape(f))
|
65
|
+
end
|
66
|
+
|
67
|
+
yield(DIR_PAGE_FOOTER)
|
51
68
|
end
|
52
69
|
|
53
70
|
private
|
54
|
-
|
55
|
-
|
56
|
-
|
71
|
+
|
72
|
+
# Escape each element in the array of html strings.
|
73
|
+
def DIR_FILE_escape(htmls)
|
74
|
+
htmls.map { |e| Utils.escape_html(e) }
|
57
75
|
end
|
58
76
|
end
|
59
77
|
|
60
|
-
|
78
|
+
# The root of the directory hierarchy. Only requests for files and
|
79
|
+
# directories inside of the root directory are supported.
|
80
|
+
attr_reader :root
|
61
81
|
|
82
|
+
# Set the root directory and application for serving files.
|
62
83
|
def initialize(root, app = nil)
|
63
84
|
@root = ::File.expand_path(root)
|
64
|
-
@app = app ||
|
65
|
-
@head =
|
85
|
+
@app = app || Files.new(@root)
|
86
|
+
@head = Head.new(method(:get))
|
66
87
|
end
|
67
88
|
|
68
89
|
def call(env)
|
@@ -70,100 +91,101 @@ table { width:100%%; }
|
|
70
91
|
@head.call env
|
71
92
|
end
|
72
93
|
|
94
|
+
# Internals of request handling. Similar to call but does
|
95
|
+
# not remove body for HEAD requests.
|
73
96
|
def get(env)
|
74
97
|
script_name = env[SCRIPT_NAME]
|
75
98
|
path_info = Utils.unescape_path(env[PATH_INFO])
|
76
99
|
|
77
|
-
if
|
78
|
-
|
79
|
-
elsif forbidden = check_forbidden(path_info)
|
80
|
-
forbidden
|
100
|
+
if client_error_response = check_bad_request(path_info) || check_forbidden(path_info)
|
101
|
+
client_error_response
|
81
102
|
else
|
82
103
|
path = ::File.join(@root, path_info)
|
83
104
|
list_path(env, path, path_info, script_name)
|
84
105
|
end
|
85
106
|
end
|
86
107
|
|
108
|
+
# Rack response to use for requests with invalid paths, or nil if path is valid.
|
87
109
|
def check_bad_request(path_info)
|
88
110
|
return if Utils.valid_path?(path_info)
|
89
111
|
|
90
112
|
body = "Bad Request\n"
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
"X-Cascade" => "pass" }, [body]]
|
113
|
+
[400, { CONTENT_TYPE => "text/plain",
|
114
|
+
CONTENT_LENGTH => body.bytesize.to_s,
|
115
|
+
"x-cascade" => "pass" }, [body]]
|
95
116
|
end
|
96
117
|
|
118
|
+
# Rack response to use for requests with paths outside the root, or nil if path is inside the root.
|
97
119
|
def check_forbidden(path_info)
|
98
120
|
return unless path_info.include? ".."
|
121
|
+
return if ::File.expand_path(::File.join(@root, path_info)).start_with?(@root)
|
99
122
|
|
100
123
|
body = "Forbidden\n"
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
"X-Cascade" => "pass" }, [body]]
|
124
|
+
[403, { CONTENT_TYPE => "text/plain",
|
125
|
+
CONTENT_LENGTH => body.bytesize.to_s,
|
126
|
+
"x-cascade" => "pass" }, [body]]
|
105
127
|
end
|
106
128
|
|
129
|
+
# Rack response to use for directories under the root.
|
107
130
|
def list_directory(path_info, path, script_name)
|
108
|
-
files = [['../', 'Parent Directory', '', '', '']]
|
109
|
-
glob = ::File.join(path, '*')
|
110
|
-
|
111
131
|
url_head = (script_name.split('/') + path_info.split('/')).map do |part|
|
112
|
-
|
132
|
+
Utils.escape_path part
|
113
133
|
end
|
114
134
|
|
115
|
-
|
116
|
-
|
135
|
+
# Globbing not safe as path could contain glob metacharacters
|
136
|
+
body = DirectoryBody.new(@root, path, ->(basename) do
|
137
|
+
stat = stat(::File.join(path, basename))
|
117
138
|
next unless stat
|
118
|
-
basename = ::File.basename(node)
|
119
|
-
ext = ::File.extname(node)
|
120
139
|
|
121
|
-
url = ::File.join(*url_head + [
|
122
|
-
size = stat.size
|
123
|
-
type = stat.directory? ? 'directory' : Mime.mime_type(ext)
|
124
|
-
size = stat.directory? ? '-' : filesize_format(size)
|
140
|
+
url = ::File.join(*url_head + [Utils.escape_path(basename)])
|
125
141
|
mtime = stat.mtime.httpdate
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
142
|
+
if stat.directory?
|
143
|
+
type = 'directory'
|
144
|
+
size = '-'
|
145
|
+
url << '/'
|
146
|
+
if basename == '..'
|
147
|
+
basename = 'Parent Directory'
|
148
|
+
else
|
149
|
+
basename << '/'
|
150
|
+
end
|
151
|
+
else
|
152
|
+
type = Mime.mime_type(::File.extname(basename))
|
153
|
+
size = filesize_format(stat.size)
|
154
|
+
end
|
155
|
+
|
156
|
+
[ url, basename, size, type, mtime ]
|
157
|
+
end)
|
158
|
+
|
159
|
+
[ 200, { CONTENT_TYPE => 'text/html; charset=utf-8' }, body ]
|
133
160
|
end
|
134
161
|
|
135
|
-
|
136
|
-
|
162
|
+
# File::Stat for the given path, but return nil for missing/bad entries.
|
163
|
+
def stat(path)
|
164
|
+
::File.stat(path)
|
137
165
|
rescue Errno::ENOENT, Errno::ELOOP
|
138
166
|
return nil
|
139
167
|
end
|
140
168
|
|
141
|
-
#
|
142
|
-
#
|
169
|
+
# Rack response to use for files and directories under the root.
|
170
|
+
# Unreadable and non-file, non-directory entries will get a 404 response.
|
143
171
|
def list_path(env, path, path_info, script_name)
|
144
|
-
stat =
|
145
|
-
|
146
|
-
if stat.readable?
|
172
|
+
if (stat = stat(path)) && stat.readable?
|
147
173
|
return @app.call(env) if stat.file?
|
148
174
|
return list_directory(path_info, path, script_name) if stat.directory?
|
149
|
-
else
|
150
|
-
raise Errno::ENOENT, 'No such file or directory'
|
151
175
|
end
|
152
176
|
|
153
|
-
|
154
|
-
return entity_not_found(path_info)
|
177
|
+
entity_not_found(path_info)
|
155
178
|
end
|
156
179
|
|
180
|
+
# Rack response to use for unreadable and non-file, non-directory entries.
|
157
181
|
def entity_not_found(path_info)
|
158
182
|
body = "Entity not found: #{path_info}\n"
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
"X-Cascade" => "pass" }, [body]]
|
183
|
+
[404, { CONTENT_TYPE => "text/plain",
|
184
|
+
CONTENT_LENGTH => body.bytesize.to_s,
|
185
|
+
"x-cascade" => "pass" }, [body]]
|
163
186
|
end
|
164
187
|
|
165
188
|
# Stolen from Ramaze
|
166
|
-
|
167
189
|
FILESIZE_FORMAT = [
|
168
190
|
['%.1fT', 1 << 40],
|
169
191
|
['%.1fG', 1 << 30],
|
@@ -171,6 +193,7 @@ table { width:100%%; }
|
|
171
193
|
['%.1fK', 1 << 10],
|
172
194
|
]
|
173
195
|
|
196
|
+
# Provide human readable file sizes
|
174
197
|
def filesize_format(int)
|
175
198
|
FILESIZE_FORMAT.each do |format, size|
|
176
199
|
return format % (int.to_f / size) if int >= size
|
data/lib/rack/etag.rb
CHANGED
@@ -1,17 +1,19 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'rack'
|
4
3
|
require 'digest/sha2'
|
5
4
|
|
5
|
+
require_relative 'constants'
|
6
|
+
require_relative 'utils'
|
7
|
+
|
6
8
|
module Rack
|
7
|
-
# Automatically sets the
|
9
|
+
# Automatically sets the etag header on all String bodies.
|
8
10
|
#
|
9
|
-
# The
|
11
|
+
# The etag header is skipped if etag or last-modified headers are sent or if
|
10
12
|
# a sendfile body (body.responds_to :to_path) is given (since such cases
|
11
13
|
# should be handled by apache/nginx).
|
12
14
|
#
|
13
|
-
# On initialization, you can pass two parameters: a
|
14
|
-
# used when
|
15
|
+
# On initialization, you can pass two parameters: a cache-control directive
|
16
|
+
# used when etag is absent and a directive when it is present. The first
|
15
17
|
# defaults to nil, while the second defaults to "max-age=0, private, must-revalidate"
|
16
18
|
class ETag
|
17
19
|
ETAG_STRING = Rack::ETAG
|
@@ -24,14 +26,11 @@ module Rack
|
|
24
26
|
end
|
25
27
|
|
26
28
|
def call(env)
|
27
|
-
status, headers, body = @app.call(env)
|
29
|
+
status, headers, body = response = @app.call(env)
|
28
30
|
|
29
|
-
if etag_status?(status) &&
|
30
|
-
|
31
|
-
digest
|
32
|
-
body = Rack::BodyProxy.new(new_body) do
|
33
|
-
original_body.close if original_body.respond_to?(:close)
|
34
|
-
end
|
31
|
+
if etag_status?(status) && body.respond_to?(:to_ary) && !skip_caching?(headers)
|
32
|
+
body = body.to_ary
|
33
|
+
digest = digest_body(body)
|
35
34
|
headers[ETAG_STRING] = %(W/"#{digest}") if digest
|
36
35
|
end
|
37
36
|
|
@@ -43,7 +42,7 @@ module Rack
|
|
43
42
|
end
|
44
43
|
end
|
45
44
|
|
46
|
-
|
45
|
+
response
|
47
46
|
end
|
48
47
|
|
49
48
|
private
|
@@ -52,25 +51,18 @@ module Rack
|
|
52
51
|
status == 200 || status == 201
|
53
52
|
end
|
54
53
|
|
55
|
-
def etag_body?(body)
|
56
|
-
!body.respond_to?(:to_path)
|
57
|
-
end
|
58
|
-
|
59
54
|
def skip_caching?(headers)
|
60
|
-
(
|
61
|
-
headers.key?(ETAG_STRING) || headers.key?('Last-Modified')
|
55
|
+
headers.key?(ETAG_STRING) || headers.key?('last-modified')
|
62
56
|
end
|
63
57
|
|
64
58
|
def digest_body(body)
|
65
|
-
parts = []
|
66
59
|
digest = nil
|
67
60
|
|
68
61
|
body.each do |part|
|
69
|
-
parts << part
|
70
62
|
(digest ||= Digest::SHA256.new) << part unless part.empty?
|
71
63
|
end
|
72
64
|
|
73
|
-
|
65
|
+
digest && digest.hexdigest.byteslice(0,32)
|
74
66
|
end
|
75
67
|
end
|
76
68
|
end
|
data/lib/rack/events.rb
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
3
|
+
require_relative 'body_proxy'
|
4
|
+
require_relative 'request'
|
5
|
+
require_relative 'response'
|
5
6
|
|
6
7
|
module Rack
|
7
8
|
### This middleware provides hooks to certain places in the request /
|
@@ -59,26 +60,26 @@ module Rack
|
|
59
60
|
|
60
61
|
class Events
|
61
62
|
module Abstract
|
62
|
-
def on_start
|
63
|
+
def on_start(req, res)
|
63
64
|
end
|
64
65
|
|
65
|
-
def on_commit
|
66
|
+
def on_commit(req, res)
|
66
67
|
end
|
67
68
|
|
68
|
-
def on_send
|
69
|
+
def on_send(req, res)
|
69
70
|
end
|
70
71
|
|
71
|
-
def on_finish
|
72
|
+
def on_finish(req, res)
|
72
73
|
end
|
73
74
|
|
74
|
-
def on_error
|
75
|
+
def on_error(req, res, e)
|
75
76
|
end
|
76
77
|
end
|
77
78
|
|
78
79
|
class EventedBodyProxy < Rack::BodyProxy # :nodoc:
|
79
80
|
attr_reader :request, :response
|
80
81
|
|
81
|
-
def initialize
|
82
|
+
def initialize(body, request, response, handlers, &block)
|
82
83
|
super(body, &block)
|
83
84
|
@request = request
|
84
85
|
@response = response
|
@@ -94,7 +95,7 @@ module Rack
|
|
94
95
|
class BufferedResponse < Rack::Response::Raw # :nodoc:
|
95
96
|
attr_reader :body
|
96
97
|
|
97
|
-
def initialize
|
98
|
+
def initialize(status, headers, body)
|
98
99
|
super(status, headers)
|
99
100
|
@body = body
|
100
101
|
end
|
@@ -102,12 +103,12 @@ module Rack
|
|
102
103
|
def to_a; [status, headers, body]; end
|
103
104
|
end
|
104
105
|
|
105
|
-
def initialize
|
106
|
+
def initialize(app, handlers)
|
106
107
|
@app = app
|
107
108
|
@handlers = handlers
|
108
109
|
end
|
109
110
|
|
110
|
-
def call
|
111
|
+
def call(env)
|
111
112
|
request = make_request env
|
112
113
|
on_start request, nil
|
113
114
|
|
@@ -129,27 +130,27 @@ module Rack
|
|
129
130
|
|
130
131
|
private
|
131
132
|
|
132
|
-
def on_error
|
133
|
+
def on_error(request, response, e)
|
133
134
|
@handlers.reverse_each { |handler| handler.on_error request, response, e }
|
134
135
|
end
|
135
136
|
|
136
|
-
def on_commit
|
137
|
+
def on_commit(request, response)
|
137
138
|
@handlers.reverse_each { |handler| handler.on_commit request, response }
|
138
139
|
end
|
139
140
|
|
140
|
-
def on_start
|
141
|
+
def on_start(request, response)
|
141
142
|
@handlers.each { |handler| handler.on_start request, nil }
|
142
143
|
end
|
143
144
|
|
144
|
-
def on_finish
|
145
|
+
def on_finish(request, response)
|
145
146
|
@handlers.reverse_each { |handler| handler.on_finish request, response }
|
146
147
|
end
|
147
148
|
|
148
|
-
def make_request
|
149
|
+
def make_request(env)
|
149
150
|
Rack::Request.new env
|
150
151
|
end
|
151
152
|
|
152
|
-
def make_response
|
153
|
+
def make_response(status, headers, body)
|
153
154
|
BufferedResponse.new status, headers, body
|
154
155
|
end
|
155
156
|
end
|
data/lib/rack/files.rb
CHANGED
@@ -1,10 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'time'
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
4
|
+
|
5
|
+
require_relative 'constants'
|
6
|
+
require_relative 'head'
|
7
|
+
require_relative 'utils'
|
8
|
+
require_relative 'request'
|
9
|
+
require_relative 'mime'
|
8
10
|
|
9
11
|
module Rack
|
10
12
|
# Rack::Files serves files below the +root+ directory given, according to the
|
@@ -18,11 +20,12 @@ module Rack
|
|
18
20
|
class Files
|
19
21
|
ALLOWED_VERBS = %w[GET HEAD OPTIONS]
|
20
22
|
ALLOW_HEADER = ALLOWED_VERBS.join(', ')
|
23
|
+
MULTIPART_BOUNDARY = 'AaB03x'
|
21
24
|
|
22
25
|
attr_reader :root
|
23
26
|
|
24
27
|
def initialize(root, headers = {}, default_mime = 'text/plain')
|
25
|
-
@root = ::File.expand_path root
|
28
|
+
@root = (::File.expand_path(root) if root)
|
26
29
|
@headers = headers
|
27
30
|
@default_mime = default_mime
|
28
31
|
@head = Rack::Head.new(lambda { |env| get env })
|
@@ -36,7 +39,7 @@ module Rack
|
|
36
39
|
def get(env)
|
37
40
|
request = Rack::Request.new env
|
38
41
|
unless ALLOWED_VERBS.include? request.request_method
|
39
|
-
return fail(405, "Method Not Allowed", { '
|
42
|
+
return fail(405, "Method Not Allowed", { 'allow' => ALLOW_HEADER })
|
40
43
|
end
|
41
44
|
|
42
45
|
path_info = Utils.unescape_path request.path_info
|
@@ -48,7 +51,11 @@ module Rack
|
|
48
51
|
available = begin
|
49
52
|
::File.file?(path) && ::File.readable?(path)
|
50
53
|
rescue SystemCallError
|
54
|
+
# Not sure in what conditions this exception can occur, but this
|
55
|
+
# is a safe way to handle such an error.
|
56
|
+
# :nocov:
|
51
57
|
false
|
58
|
+
# :nocov:
|
52
59
|
end
|
53
60
|
|
54
61
|
if available
|
@@ -60,85 +67,126 @@ module Rack
|
|
60
67
|
|
61
68
|
def serving(request, path)
|
62
69
|
if request.options?
|
63
|
-
return [200, { '
|
70
|
+
return [200, { 'allow' => ALLOW_HEADER, CONTENT_LENGTH => '0' }, []]
|
64
71
|
end
|
65
72
|
last_modified = ::File.mtime(path).httpdate
|
66
73
|
return [304, {}, []] if request.get_header('HTTP_IF_MODIFIED_SINCE') == last_modified
|
67
74
|
|
68
|
-
headers = { "
|
75
|
+
headers = { "last-modified" => last_modified }
|
69
76
|
mime_type = mime_type path, @default_mime
|
70
77
|
headers[CONTENT_TYPE] = mime_type if mime_type
|
71
78
|
|
72
79
|
# Set custom headers
|
73
|
-
|
74
|
-
|
75
|
-
response = [ 200, headers ]
|
80
|
+
headers.merge!(@headers) if @headers
|
76
81
|
|
82
|
+
status = 200
|
77
83
|
size = filesize path
|
78
84
|
|
79
|
-
range = nil
|
80
85
|
ranges = Rack::Utils.get_byte_ranges(request.get_header('HTTP_RANGE'), size)
|
81
|
-
if ranges.nil?
|
82
|
-
# No ranges
|
83
|
-
|
84
|
-
response[0] = 200
|
85
|
-
range = 0..size - 1
|
86
|
+
if ranges.nil?
|
87
|
+
# No ranges:
|
88
|
+
ranges = [0..size - 1]
|
86
89
|
elsif ranges.empty?
|
87
90
|
# Unsatisfiable. Return error, and file size:
|
88
91
|
response = fail(416, "Byte range unsatisfiable")
|
89
|
-
response[1]["
|
92
|
+
response[1]["content-range"] = "bytes */#{size}"
|
90
93
|
return response
|
91
94
|
else
|
92
|
-
# Partial content
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
95
|
+
# Partial content
|
96
|
+
partial_content = true
|
97
|
+
|
98
|
+
if ranges.size == 1
|
99
|
+
range = ranges[0]
|
100
|
+
headers["content-range"] = "bytes #{range.begin}-#{range.end}/#{size}"
|
101
|
+
else
|
102
|
+
headers[CONTENT_TYPE] = "multipart/byteranges; boundary=#{MULTIPART_BOUNDARY}"
|
103
|
+
end
|
104
|
+
|
105
|
+
status = 206
|
106
|
+
body = BaseIterator.new(path, ranges, mime_type: mime_type, size: size)
|
107
|
+
size = body.bytesize
|
97
108
|
end
|
98
109
|
|
99
|
-
|
110
|
+
headers[CONTENT_LENGTH] = size.to_s
|
100
111
|
|
101
|
-
|
102
|
-
|
103
|
-
|
112
|
+
if request.head?
|
113
|
+
body = []
|
114
|
+
elsif !partial_content
|
115
|
+
body = Iterator.new(path, ranges, mime_type: mime_type, size: size)
|
116
|
+
end
|
117
|
+
|
118
|
+
[status, headers, body]
|
104
119
|
end
|
105
120
|
|
106
|
-
class
|
107
|
-
attr_reader :path, :
|
108
|
-
alias :to_path :path
|
121
|
+
class BaseIterator
|
122
|
+
attr_reader :path, :ranges, :options
|
109
123
|
|
110
|
-
def initialize
|
111
|
-
@path
|
112
|
-
@
|
124
|
+
def initialize(path, ranges, options)
|
125
|
+
@path = path
|
126
|
+
@ranges = ranges
|
127
|
+
@options = options
|
113
128
|
end
|
114
129
|
|
115
130
|
def each
|
116
131
|
::File.open(path, "rb") do |file|
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
yield part
|
132
|
+
ranges.each do |range|
|
133
|
+
yield multipart_heading(range) if multipart?
|
134
|
+
|
135
|
+
each_range_part(file, range) do |part|
|
136
|
+
yield part
|
137
|
+
end
|
125
138
|
end
|
139
|
+
|
140
|
+
yield "\r\n--#{MULTIPART_BOUNDARY}--\r\n" if multipart?
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def bytesize
|
145
|
+
size = ranges.inject(0) do |sum, range|
|
146
|
+
sum += multipart_heading(range).bytesize if multipart?
|
147
|
+
sum += range.size
|
126
148
|
end
|
149
|
+
size += "\r\n--#{MULTIPART_BOUNDARY}--\r\n".bytesize if multipart?
|
150
|
+
size
|
127
151
|
end
|
128
152
|
|
129
153
|
def close; end
|
130
|
-
end
|
131
154
|
|
132
|
-
|
155
|
+
private
|
133
156
|
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
157
|
+
def multipart?
|
158
|
+
ranges.size > 1
|
159
|
+
end
|
160
|
+
|
161
|
+
def multipart_heading(range)
|
162
|
+
<<-EOF
|
163
|
+
\r
|
164
|
+
--#{MULTIPART_BOUNDARY}\r
|
165
|
+
content-type: #{options[:mime_type]}\r
|
166
|
+
content-range: bytes #{range.begin}-#{range.end}/#{options[:size]}\r
|
167
|
+
\r
|
168
|
+
EOF
|
169
|
+
end
|
170
|
+
|
171
|
+
def each_range_part(file, range)
|
172
|
+
file.seek(range.begin)
|
173
|
+
remaining_len = range.end - range.begin + 1
|
174
|
+
while remaining_len > 0
|
175
|
+
part = file.read([8192, remaining_len].min)
|
176
|
+
break unless part
|
177
|
+
remaining_len -= part.length
|
178
|
+
|
179
|
+
yield part
|
180
|
+
end
|
139
181
|
end
|
140
182
|
end
|
141
183
|
|
184
|
+
class Iterator < BaseIterator
|
185
|
+
alias :to_path :path
|
186
|
+
end
|
187
|
+
|
188
|
+
private
|
189
|
+
|
142
190
|
def fail(status, body, headers = {})
|
143
191
|
body += "\n"
|
144
192
|
|
@@ -147,32 +195,22 @@ module Rack
|
|
147
195
|
{
|
148
196
|
CONTENT_TYPE => "text/plain",
|
149
197
|
CONTENT_LENGTH => body.size.to_s,
|
150
|
-
"
|
198
|
+
"x-cascade" => "pass"
|
151
199
|
}.merge!(headers),
|
152
200
|
[body]
|
153
201
|
]
|
154
202
|
end
|
155
203
|
|
156
204
|
# The MIME type for the contents of the file located at @path
|
157
|
-
def mime_type
|
205
|
+
def mime_type(path, default_mime)
|
158
206
|
Mime.mime_type(::File.extname(path), default_mime)
|
159
207
|
end
|
160
208
|
|
161
|
-
def filesize
|
162
|
-
# If response_body is present, use its size.
|
163
|
-
return response_body.bytesize if response_body
|
164
|
-
|
209
|
+
def filesize(path)
|
165
210
|
# We check via File::size? whether this file provides size info
|
166
211
|
# via stat (e.g. /proc files often don't), otherwise we have to
|
167
212
|
# figure it out by reading the whole file into memory.
|
168
213
|
::File.size?(path) || ::File.read(path).bytesize
|
169
214
|
end
|
170
|
-
|
171
|
-
# By default, the response body for file requests is nil.
|
172
|
-
# In this case, the response body will be generated later
|
173
|
-
# from the file at @path
|
174
|
-
def response_body
|
175
|
-
nil
|
176
|
-
end
|
177
215
|
end
|
178
216
|
end
|