rack 2.2.18 → 3.2.3
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +561 -75
- data/CONTRIBUTING.md +63 -55
- data/MIT-LICENSE +1 -1
- data/README.md +384 -0
- data/SPEC.rdoc +243 -277
- data/lib/rack/auth/abstract/handler.rb +3 -1
- data/lib/rack/auth/abstract/request.rb +5 -1
- data/lib/rack/auth/basic.rb +1 -3
- data/lib/rack/bad_request.rb +8 -0
- data/lib/rack/body_proxy.rb +21 -3
- data/lib/rack/builder.rb +108 -69
- data/lib/rack/cascade.rb +2 -3
- data/lib/rack/common_logger.rb +22 -17
- data/lib/rack/conditional_get.rb +20 -16
- data/lib/rack/constants.rb +68 -0
- data/lib/rack/content_length.rb +12 -16
- data/lib/rack/content_type.rb +8 -5
- data/lib/rack/deflater.rb +40 -26
- data/lib/rack/directory.rb +9 -3
- data/lib/rack/etag.rb +17 -23
- data/lib/rack/events.rb +25 -6
- data/lib/rack/files.rb +15 -17
- data/lib/rack/head.rb +8 -8
- data/lib/rack/headers.rb +238 -0
- data/lib/rack/lint.rb +817 -648
- data/lib/rack/lock.rb +2 -5
- data/lib/rack/media_type.rb +6 -7
- data/lib/rack/method_override.rb +5 -1
- data/lib/rack/mime.rb +14 -5
- data/lib/rack/mock.rb +1 -300
- data/lib/rack/mock_request.rb +161 -0
- data/lib/rack/mock_response.rb +147 -0
- data/lib/rack/multipart/generator.rb +7 -5
- data/lib/rack/multipart/parser.rb +291 -95
- data/lib/rack/multipart/uploaded_file.rb +45 -4
- data/lib/rack/multipart.rb +53 -40
- data/lib/rack/null_logger.rb +9 -0
- data/lib/rack/query_parser.rb +118 -121
- data/lib/rack/recursive.rb +2 -0
- data/lib/rack/reloader.rb +0 -2
- data/lib/rack/request.rb +272 -141
- data/lib/rack/response.rb +151 -66
- data/lib/rack/rewindable_input.rb +27 -5
- data/lib/rack/runtime.rb +7 -6
- data/lib/rack/sendfile.rb +68 -33
- data/lib/rack/show_exceptions.rb +25 -6
- data/lib/rack/show_status.rb +17 -9
- data/lib/rack/static.rb +8 -8
- data/lib/rack/tempfile_reaper.rb +15 -4
- data/lib/rack/urlmap.rb +3 -1
- data/lib/rack/utils.rb +228 -238
- data/lib/rack/version.rb +3 -15
- data/lib/rack.rb +13 -90
- metadata +14 -40
- data/README.rdoc +0 -347
- data/Rakefile +0 -130
- 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 -53
- data/lib/rack/auth/digest/params.rb +0 -54
- data/lib/rack/auth/digest/request.rb +0 -43
- data/lib/rack/chunked.rb +0 -117
- data/lib/rack/core_ext/regexp.rb +0 -14
- data/lib/rack/file.rb +0 -7
- data/lib/rack/handler/cgi.rb +0 -59
- data/lib/rack/handler/fastcgi.rb +0 -100
- data/lib/rack/handler/lsws.rb +0 -61
- data/lib/rack/handler/scgi.rb +0 -71
- data/lib/rack/handler/thin.rb +0 -34
- data/lib/rack/handler/webrick.rb +0 -129
- data/lib/rack/handler.rb +0 -104
- data/lib/rack/lobster.rb +0 -70
- data/lib/rack/logger.rb +0 -20
- data/lib/rack/server.rb +0 -466
- data/lib/rack/session/abstract/id.rb +0 -523
- data/lib/rack/session/cookie.rb +0 -203
- data/lib/rack/session/memcache.rb +0 -10
- data/lib/rack/session/pool.rb +0 -90
- data/rack.gemspec +0 -46
data/lib/rack/response.rb
CHANGED
@@ -2,6 +2,11 @@
|
|
2
2
|
|
3
3
|
require 'time'
|
4
4
|
|
5
|
+
require_relative 'constants'
|
6
|
+
require_relative 'utils'
|
7
|
+
require_relative 'media_type'
|
8
|
+
require_relative 'headers'
|
9
|
+
|
5
10
|
module Rack
|
6
11
|
# Rack::Response provides a convenient interface to create a Rack
|
7
12
|
# response.
|
@@ -26,22 +31,38 @@ module Rack
|
|
26
31
|
attr_accessor :length, :status, :body
|
27
32
|
attr_reader :headers
|
28
33
|
|
29
|
-
#
|
30
|
-
|
31
|
-
|
32
|
-
#
|
33
|
-
#
|
34
|
+
# Initialize the response object with the specified +body+, +status+
|
35
|
+
# and +headers+.
|
36
|
+
#
|
37
|
+
# If the +body+ is +nil+, construct an empty response object with internal
|
38
|
+
# buffering.
|
39
|
+
#
|
40
|
+
# If the +body+ responds to +to_str+, assume it's a string-like object and
|
41
|
+
# construct a buffered response object containing using that string as the
|
42
|
+
# initial contents of the buffer.
|
43
|
+
#
|
44
|
+
# Otherwise it is expected +body+ conforms to the normal requirements of a
|
45
|
+
# Rack response body, typically implementing one of +each+ (enumerable
|
46
|
+
# body) or +call+ (streaming body).
|
34
47
|
#
|
35
|
-
#
|
36
|
-
#
|
37
|
-
# HTTP protocol RFCs.
|
38
|
-
# @param headers [#each] a list of key-value header pairs which
|
39
|
-
# conform to the HTTP protocol RFCs.
|
48
|
+
# The +status+ defaults to +200+ which is the "OK" HTTP status code. You
|
49
|
+
# can provide any other valid status code.
|
40
50
|
#
|
41
|
-
#
|
51
|
+
# The +headers+ must be a +Hash+ of key-value header pairs which conform to
|
52
|
+
# the Rack specification for response headers. The key must be a +String+
|
53
|
+
# instance and the value can be either a +String+ or +Array+ instance.
|
42
54
|
def initialize(body = nil, status = 200, headers = {})
|
43
55
|
@status = status.to_i
|
44
|
-
|
56
|
+
|
57
|
+
unless headers.is_a?(Hash)
|
58
|
+
raise ArgumentError, "Headers must be a Hash!"
|
59
|
+
end
|
60
|
+
|
61
|
+
@headers = Headers.new
|
62
|
+
# Convert headers input to a plain hash with lowercase keys.
|
63
|
+
headers.each do |k, v|
|
64
|
+
@headers[k] = v
|
65
|
+
end
|
45
66
|
|
46
67
|
@writer = self.method(:append)
|
47
68
|
|
@@ -51,15 +72,16 @@ module Rack
|
|
51
72
|
if body.nil?
|
52
73
|
@body = []
|
53
74
|
@buffered = true
|
54
|
-
|
75
|
+
# Body is unspecified - it may be a buffered response, or it may be a HEAD response.
|
76
|
+
@length = nil
|
55
77
|
elsif body.respond_to?(:to_str)
|
56
78
|
@body = [body]
|
57
79
|
@buffered = true
|
58
80
|
@length = body.to_str.bytesize
|
59
81
|
else
|
60
82
|
@body = body
|
61
|
-
@buffered =
|
62
|
-
@length =
|
83
|
+
@buffered = nil # undetermined as of yet.
|
84
|
+
@length = nil
|
63
85
|
end
|
64
86
|
|
65
87
|
yield self if block_given?
|
@@ -74,20 +96,30 @@ module Rack
|
|
74
96
|
CHUNKED == get_header(TRANSFER_ENCODING)
|
75
97
|
end
|
76
98
|
|
99
|
+
def no_entity_body?
|
100
|
+
# The response body is an enumerable body and it is not allowed to have an entity body.
|
101
|
+
@body.respond_to?(:each) && STATUS_WITH_NO_ENTITY_BODY[@status]
|
102
|
+
end
|
103
|
+
|
77
104
|
# Generate a response array consistent with the requirements of the SPEC.
|
78
105
|
# @return [Array] a 3-tuple suitable of `[status, headers, body]`
|
79
106
|
# which is suitable to be returned from the middleware `#call(env)` method.
|
80
107
|
def finish(&block)
|
81
|
-
if
|
108
|
+
if no_entity_body?
|
82
109
|
delete_header CONTENT_TYPE
|
83
110
|
delete_header CONTENT_LENGTH
|
84
111
|
close
|
85
112
|
return [@status, @headers, []]
|
86
113
|
else
|
87
114
|
if block_given?
|
115
|
+
# We don't add the content-length here as the user has provided a block that can #write additional chunks to the body.
|
88
116
|
@block = block
|
89
117
|
return [@status, @headers, self]
|
90
118
|
else
|
119
|
+
# If we know the length of the body, set the content-length header... except if we are chunked? which is a legacy special case where the body might already be encoded and thus the actual encoded body length and the content-length are likely to be different.
|
120
|
+
if @length && !chunked?
|
121
|
+
@headers[CONTENT_LENGTH] = @length.to_s
|
122
|
+
end
|
91
123
|
return [@status, @headers, @body]
|
92
124
|
end
|
93
125
|
end
|
@@ -105,7 +137,9 @@ module Rack
|
|
105
137
|
end
|
106
138
|
end
|
107
139
|
|
108
|
-
# Append to
|
140
|
+
# Append a chunk to the response body.
|
141
|
+
#
|
142
|
+
# Converts the response into a buffered response if it wasn't already.
|
109
143
|
#
|
110
144
|
# NOTE: Do not mix #write and direct #body access!
|
111
145
|
#
|
@@ -123,10 +157,22 @@ module Rack
|
|
123
157
|
@block == nil && @body.empty?
|
124
158
|
end
|
125
159
|
|
126
|
-
def has_header?(key)
|
127
|
-
|
128
|
-
|
129
|
-
|
160
|
+
def has_header?(key)
|
161
|
+
raise ArgumentError unless key.is_a?(String)
|
162
|
+
@headers.key?(key)
|
163
|
+
end
|
164
|
+
def get_header(key)
|
165
|
+
raise ArgumentError unless key.is_a?(String)
|
166
|
+
@headers[key]
|
167
|
+
end
|
168
|
+
def set_header(key, value)
|
169
|
+
raise ArgumentError unless key.is_a?(String)
|
170
|
+
@headers[key] = value
|
171
|
+
end
|
172
|
+
def delete_header(key)
|
173
|
+
raise ArgumentError unless key.is_a?(String)
|
174
|
+
@headers.delete key
|
175
|
+
end
|
130
176
|
|
131
177
|
alias :[] :get_header
|
132
178
|
alias :[]= :set_header
|
@@ -150,31 +196,43 @@ module Rack
|
|
150
196
|
def forbidden?; status == 403; end
|
151
197
|
def not_found?; status == 404; end
|
152
198
|
def method_not_allowed?; status == 405; end
|
199
|
+
def not_acceptable?; status == 406; end
|
200
|
+
def request_timeout?; status == 408; end
|
153
201
|
def precondition_failed?; status == 412; end
|
154
202
|
def unprocessable?; status == 422; end
|
155
203
|
|
156
204
|
def redirect?; [301, 302, 303, 307, 308].include? status; end
|
157
205
|
|
158
206
|
def include?(header)
|
159
|
-
has_header?
|
207
|
+
has_header?(header)
|
160
208
|
end
|
161
209
|
|
162
210
|
# Add a header that may have multiple values.
|
163
211
|
#
|
164
212
|
# Example:
|
165
|
-
# response.add_header '
|
166
|
-
# response.add_header '
|
213
|
+
# response.add_header 'vary', 'accept-encoding'
|
214
|
+
# response.add_header 'vary', 'cookie'
|
167
215
|
#
|
168
|
-
# assert_equal '
|
216
|
+
# assert_equal 'accept-encoding,cookie', response.get_header('vary')
|
169
217
|
#
|
170
218
|
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2
|
171
|
-
def add_header(key,
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
219
|
+
def add_header(key, value)
|
220
|
+
raise ArgumentError unless key.is_a?(String)
|
221
|
+
|
222
|
+
if value.nil?
|
223
|
+
return get_header(key)
|
224
|
+
end
|
225
|
+
|
226
|
+
value = value.to_s
|
227
|
+
|
228
|
+
if header = get_header(key)
|
229
|
+
if header.is_a?(Array)
|
230
|
+
header << value
|
231
|
+
else
|
232
|
+
set_header(key, [header, value])
|
233
|
+
end
|
176
234
|
else
|
177
|
-
set_header
|
235
|
+
set_header(key, value)
|
178
236
|
end
|
179
237
|
end
|
180
238
|
|
@@ -202,36 +260,39 @@ module Rack
|
|
202
260
|
end
|
203
261
|
|
204
262
|
def location
|
205
|
-
get_header "
|
263
|
+
get_header "location"
|
206
264
|
end
|
207
265
|
|
208
266
|
def location=(location)
|
209
|
-
set_header "
|
267
|
+
set_header "location", location
|
210
268
|
end
|
211
269
|
|
212
270
|
def set_cookie(key, value)
|
213
|
-
|
214
|
-
set_header SET_COOKIE, ::Rack::Utils.add_cookie_to_header(cookie_header, key, value)
|
271
|
+
add_header SET_COOKIE, Utils.set_cookie_header(key, value)
|
215
272
|
end
|
216
273
|
|
217
274
|
def delete_cookie(key, value = {})
|
218
|
-
set_header
|
275
|
+
set_header(SET_COOKIE,
|
276
|
+
Utils.delete_set_cookie_header!(
|
277
|
+
get_header(SET_COOKIE), key, value
|
278
|
+
)
|
279
|
+
)
|
219
280
|
end
|
220
281
|
|
221
282
|
def set_cookie_header
|
222
283
|
get_header SET_COOKIE
|
223
284
|
end
|
224
285
|
|
225
|
-
def set_cookie_header=(
|
226
|
-
set_header SET_COOKIE,
|
286
|
+
def set_cookie_header=(value)
|
287
|
+
set_header SET_COOKIE, value
|
227
288
|
end
|
228
289
|
|
229
290
|
def cache_control
|
230
291
|
get_header CACHE_CONTROL
|
231
292
|
end
|
232
293
|
|
233
|
-
def cache_control=(
|
234
|
-
set_header CACHE_CONTROL,
|
294
|
+
def cache_control=(value)
|
295
|
+
set_header CACHE_CONTROL, value
|
235
296
|
end
|
236
297
|
|
237
298
|
# Specifies that the content shouldn't be cached. Overrides `cache!` if already called.
|
@@ -254,42 +315,55 @@ module Rack
|
|
254
315
|
get_header ETAG
|
255
316
|
end
|
256
317
|
|
257
|
-
def etag=(
|
258
|
-
set_header ETAG,
|
318
|
+
def etag=(value)
|
319
|
+
set_header ETAG, value
|
259
320
|
end
|
260
321
|
|
261
322
|
protected
|
262
323
|
|
324
|
+
# Convert the body of this response into an internally buffered Array if possible.
|
325
|
+
#
|
326
|
+
# `@buffered` is a ternary value which indicates whether the body is buffered. It can be:
|
327
|
+
# * `nil` - The body has not been buffered yet.
|
328
|
+
# * `true` - The body is buffered as an Array instance.
|
329
|
+
# * `false` - The body is not buffered and cannot be buffered.
|
330
|
+
#
|
331
|
+
# @return [Boolean] whether the body is buffered as an Array instance.
|
263
332
|
def buffered_body!
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
333
|
+
if @buffered.nil?
|
334
|
+
if @body.is_a?(Array)
|
335
|
+
# The user supplied body was an array:
|
336
|
+
@body = @body.compact
|
337
|
+
@length = @body.sum{|part| part.bytesize}
|
338
|
+
@buffered = true
|
339
|
+
elsif @body.respond_to?(:each)
|
340
|
+
# Turn the user supplied body into a buffered array:
|
341
|
+
body = @body
|
342
|
+
@body = Array.new
|
343
|
+
@buffered = true
|
344
|
+
|
345
|
+
body.each do |part|
|
346
|
+
@writer.call(part.to_s)
|
347
|
+
end
|
348
|
+
|
349
|
+
body.close if body.respond_to?(:close)
|
350
|
+
else
|
351
|
+
# We don't know how to buffer the user-supplied body:
|
352
|
+
@buffered = false
|
279
353
|
end
|
280
|
-
|
281
|
-
body.close if body.respond_to?(:close)
|
282
354
|
end
|
283
355
|
|
284
|
-
@buffered
|
356
|
+
return @buffered
|
285
357
|
end
|
286
358
|
|
287
359
|
def append(chunk)
|
360
|
+
chunk = chunk.dup unless chunk.frozen?
|
288
361
|
@body << chunk
|
289
362
|
|
290
|
-
|
363
|
+
if @length
|
291
364
|
@length += chunk.bytesize
|
292
|
-
|
365
|
+
elsif @buffered
|
366
|
+
@length = chunk.bytesize
|
293
367
|
end
|
294
368
|
|
295
369
|
return chunk
|
@@ -309,10 +383,21 @@ module Rack
|
|
309
383
|
@headers = headers
|
310
384
|
end
|
311
385
|
|
312
|
-
def has_header?(key)
|
313
|
-
|
314
|
-
|
315
|
-
|
386
|
+
def has_header?(key)
|
387
|
+
headers.key?(key)
|
388
|
+
end
|
389
|
+
|
390
|
+
def get_header(key)
|
391
|
+
headers[key]
|
392
|
+
end
|
393
|
+
|
394
|
+
def set_header(key, value)
|
395
|
+
headers[key] = value
|
396
|
+
end
|
397
|
+
|
398
|
+
def delete_header(key)
|
399
|
+
headers.delete(key)
|
400
|
+
end
|
316
401
|
end
|
317
402
|
end
|
318
403
|
end
|
@@ -3,17 +3,32 @@
|
|
3
3
|
|
4
4
|
require 'tempfile'
|
5
5
|
|
6
|
+
require_relative 'constants'
|
7
|
+
|
6
8
|
module Rack
|
7
9
|
# Class which can make any IO object rewindable, including non-rewindable ones. It does
|
8
10
|
# this by buffering the data into a tempfile, which is rewindable.
|
9
11
|
#
|
10
|
-
# rack.input is required to be rewindable, so if your input stream IO is non-rewindable
|
11
|
-
# by nature (e.g. a pipe or a socket) then you can wrap it in an object of this class
|
12
|
-
# to easily make it rewindable.
|
13
|
-
#
|
14
12
|
# Don't forget to call #close when you're done. This frees up temporary resources that
|
15
13
|
# RewindableInput uses, though it does *not* close the original IO object.
|
16
14
|
class RewindableInput
|
15
|
+
# Makes rack.input rewindable, for compatibility with applications and middleware
|
16
|
+
# designed for earlier versions of Rack (where rack.input was required to be
|
17
|
+
# rewindable).
|
18
|
+
class Middleware
|
19
|
+
def initialize(app)
|
20
|
+
@app = app
|
21
|
+
end
|
22
|
+
|
23
|
+
def call(env)
|
24
|
+
if (input = env[RACK_INPUT])
|
25
|
+
env[RACK_INPUT] = RewindableInput.new(input)
|
26
|
+
end
|
27
|
+
|
28
|
+
@app.call(env)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
17
32
|
def initialize(io)
|
18
33
|
@io = io
|
19
34
|
@rewindable_io = nil
|
@@ -40,6 +55,11 @@ module Rack
|
|
40
55
|
@rewindable_io.rewind
|
41
56
|
end
|
42
57
|
|
58
|
+
def size
|
59
|
+
make_rewindable unless @rewindable_io
|
60
|
+
@rewindable_io.size
|
61
|
+
end
|
62
|
+
|
43
63
|
# Closes this RewindableInput object without closing the originally
|
44
64
|
# wrapped IO object. Cleans up any temporary resources that this RewindableInput
|
45
65
|
# has created.
|
@@ -66,12 +86,14 @@ module Rack
|
|
66
86
|
# access it because we have the file handle open.
|
67
87
|
@rewindable_io = Tempfile.new('RackRewindableInput')
|
68
88
|
@rewindable_io.chmod(0000)
|
69
|
-
@rewindable_io.set_encoding(Encoding::BINARY)
|
89
|
+
@rewindable_io.set_encoding(Encoding::BINARY)
|
70
90
|
@rewindable_io.binmode
|
91
|
+
# :nocov:
|
71
92
|
if filesystem_has_posix_semantics?
|
72
93
|
raise 'Unlink failed. IO closed.' if @rewindable_io.closed?
|
73
94
|
@unlinked = true
|
74
95
|
end
|
96
|
+
# :nocov:
|
75
97
|
|
76
98
|
buffer = "".dup
|
77
99
|
while @io.read(1024 * 4, buffer)
|
data/lib/rack/runtime.rb
CHANGED
@@ -1,7 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative 'utils'
|
4
|
+
|
3
5
|
module Rack
|
4
|
-
# Sets an "
|
6
|
+
# Sets an "x-runtime" response header, indicating the response
|
5
7
|
# time of the request, in seconds
|
6
8
|
#
|
7
9
|
# You can put it right before the application to see the processing
|
@@ -9,18 +11,17 @@ module Rack
|
|
9
11
|
# too.
|
10
12
|
class Runtime
|
11
13
|
FORMAT_STRING = "%0.6f" # :nodoc:
|
12
|
-
HEADER_NAME = "
|
14
|
+
HEADER_NAME = "x-runtime" # :nodoc:
|
13
15
|
|
14
16
|
def initialize(app, name = nil)
|
15
17
|
@app = app
|
16
18
|
@header_name = HEADER_NAME
|
17
|
-
@header_name += "-#{name}" if name
|
19
|
+
@header_name += "-#{name.to_s.downcase}" if name
|
18
20
|
end
|
19
21
|
|
20
22
|
def call(env)
|
21
23
|
start_time = Utils.clock_time
|
22
|
-
|
23
|
-
headers = Utils::HeaderHash[headers]
|
24
|
+
_, headers, _ = response = @app.call(env)
|
24
25
|
|
25
26
|
request_time = Utils.clock_time - start_time
|
26
27
|
|
@@ -28,7 +29,7 @@ module Rack
|
|
28
29
|
headers[@header_name] = FORMAT_STRING % request_time
|
29
30
|
end
|
30
31
|
|
31
|
-
|
32
|
+
response
|
32
33
|
end
|
33
34
|
end
|
34
35
|
end
|
data/lib/rack/sendfile.rb
CHANGED
@@ -1,32 +1,36 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative 'constants'
|
4
|
+
require_relative 'utils'
|
5
|
+
require_relative 'body_proxy'
|
6
|
+
|
3
7
|
module Rack
|
4
8
|
|
5
9
|
# = Sendfile
|
6
10
|
#
|
7
11
|
# The Sendfile middleware intercepts responses whose body is being
|
8
|
-
# served from a file and replaces it with a server specific
|
12
|
+
# served from a file and replaces it with a server specific x-sendfile
|
9
13
|
# header. The web server is then responsible for writing the file contents
|
10
14
|
# to the client. This can dramatically reduce the amount of work required
|
11
15
|
# by the Ruby backend and takes advantage of the web server's optimized file
|
12
16
|
# delivery code.
|
13
17
|
#
|
14
18
|
# In order to take advantage of this middleware, the response body must
|
15
|
-
# respond to +to_path+ and the request must include an
|
19
|
+
# respond to +to_path+ and the request must include an `x-sendfile-type`
|
16
20
|
# header. Rack::Files and other components implement +to_path+ so there's
|
17
|
-
# rarely anything you need to do in your application. The
|
21
|
+
# rarely anything you need to do in your application. The `x-sendfile-type`
|
18
22
|
# header is typically set in your web servers configuration. The following
|
19
23
|
# sections attempt to document
|
20
24
|
#
|
21
25
|
# === Nginx
|
22
26
|
#
|
23
|
-
# Nginx supports the
|
27
|
+
# Nginx supports the `x-accel-redirect` header. This is similar to `x-sendfile`
|
24
28
|
# but requires parts of the filesystem to be mapped into a private URL
|
25
29
|
# hierarchy.
|
26
30
|
#
|
27
31
|
# The following example shows the Nginx configuration required to create
|
28
|
-
# a private "/files/" area, enable
|
29
|
-
#
|
32
|
+
# a private "/files/" area, enable `x-accel-redirect`, and pass the special
|
33
|
+
# `x-accel-mapping` header to the backend:
|
30
34
|
#
|
31
35
|
# location ~ /files/(.*) {
|
32
36
|
# internal;
|
@@ -40,24 +44,29 @@ module Rack
|
|
40
44
|
# proxy_set_header X-Real-IP $remote_addr;
|
41
45
|
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
42
46
|
#
|
43
|
-
# proxy_set_header
|
44
|
-
# proxy_set_header X-Accel-Mapping /var/www/=/files/;
|
47
|
+
# proxy_set_header x-accel-mapping /var/www/=/files/;
|
45
48
|
#
|
46
49
|
# proxy_pass http://127.0.0.1:8080/;
|
47
50
|
# }
|
48
51
|
#
|
49
|
-
#
|
50
|
-
# The X-Accel-Mapping header should specify the location on the file system,
|
52
|
+
# The `x-accel-mapping` header should specify the location on the file system,
|
51
53
|
# followed by an equals sign (=), followed name of the private URL pattern
|
52
54
|
# that it maps to. The middleware performs a simple substitution on the
|
53
55
|
# resulting path.
|
54
56
|
#
|
57
|
+
# To enable `x-accel-redirect`, you must configure the middleware explicitly:
|
58
|
+
#
|
59
|
+
# use Rack::Sendfile, "x-accel-redirect"
|
60
|
+
#
|
61
|
+
# For security reasons, the `x-sendfile-type` header from requests is ignored.
|
62
|
+
# The sendfile variation must be set via the middleware constructor.
|
63
|
+
#
|
55
64
|
# See Also: https://www.nginx.com/resources/wiki/start/topics/examples/xsendfile
|
56
65
|
#
|
57
66
|
# === lighttpd
|
58
67
|
#
|
59
|
-
# Lighttpd has supported some variation of the
|
60
|
-
# time, although only recent version support
|
68
|
+
# Lighttpd has supported some variation of the `x-sendfile` header for some
|
69
|
+
# time, although only recent version support `x-sendfile` in a reverse proxy
|
61
70
|
# configuration.
|
62
71
|
#
|
63
72
|
# $HTTP["host"] == "example.com" {
|
@@ -71,7 +80,7 @@ module Rack
|
|
71
80
|
#
|
72
81
|
# proxy-core.allow-x-sendfile = "enable"
|
73
82
|
# proxy-core.rewrite-request = (
|
74
|
-
# "
|
83
|
+
# "x-sendfile-type" => (".*" => "x-sendfile")
|
75
84
|
# )
|
76
85
|
# }
|
77
86
|
#
|
@@ -79,56 +88,69 @@ module Rack
|
|
79
88
|
#
|
80
89
|
# === Apache
|
81
90
|
#
|
82
|
-
#
|
91
|
+
# `x-sendfile` is supported under Apache 2.x using a separate module:
|
83
92
|
#
|
84
93
|
# https://tn123.org/mod_xsendfile/
|
85
94
|
#
|
86
95
|
# Once the module is compiled and installed, you can enable it using
|
87
96
|
# XSendFile config directive:
|
88
97
|
#
|
89
|
-
# RequestHeader Set
|
98
|
+
# RequestHeader Set x-sendfile-type x-sendfile
|
90
99
|
# ProxyPassReverse / http://localhost:8001/
|
91
100
|
# XSendFile on
|
92
101
|
#
|
93
102
|
# === Mapping parameter
|
94
103
|
#
|
95
104
|
# The third parameter allows for an overriding extension of the
|
96
|
-
#
|
105
|
+
# `x-accel-mapping` header. Mappings should be provided in tuples of internal to
|
97
106
|
# external. The internal values may contain regular expression syntax, they
|
98
107
|
# will be matched with case indifference.
|
108
|
+
#
|
109
|
+
# When `x-accel-redirect` is explicitly enabled via the variation parameter,
|
110
|
+
# and no application-level mappings are provided, the middleware will read
|
111
|
+
# the `x-accel-mapping` header from the proxy. This allows nginx to control
|
112
|
+
# the path mapping without requiring application-level configuration.
|
113
|
+
#
|
114
|
+
# === Security
|
115
|
+
#
|
116
|
+
# For security reasons, the `x-sendfile-type` header from HTTP requests is
|
117
|
+
# ignored. The sendfile variation must be explicitly configured via the
|
118
|
+
# middleware constructor to prevent information disclosure vulnerabilities
|
119
|
+
# where attackers could bypass proxy restrictions.
|
99
120
|
|
100
121
|
class Sendfile
|
101
122
|
def initialize(app, variation = nil, mappings = [])
|
102
123
|
@app = app
|
103
124
|
@variation = variation
|
104
125
|
@mappings = mappings.map do |internal, external|
|
105
|
-
[
|
126
|
+
[/\A#{internal}/i, external]
|
106
127
|
end
|
107
128
|
end
|
108
129
|
|
109
130
|
def call(env)
|
110
|
-
|
131
|
+
_, headers, body = response = @app.call(env)
|
132
|
+
|
111
133
|
if body.respond_to?(:to_path)
|
112
134
|
case type = variation(env)
|
113
|
-
when
|
135
|
+
when /x-accel-redirect/i
|
114
136
|
path = ::File.expand_path(body.to_path)
|
115
137
|
if url = map_accel_path(env, path)
|
116
138
|
headers[CONTENT_LENGTH] = '0'
|
117
139
|
# '?' must be percent-encoded because it is not query string but a part of path
|
118
|
-
headers[type] = ::Rack::Utils.escape_path(url).gsub('?', '%3F')
|
140
|
+
headers[type.downcase] = ::Rack::Utils.escape_path(url).gsub('?', '%3F')
|
119
141
|
obody = body
|
120
|
-
|
142
|
+
response[2] = Rack::BodyProxy.new([]) do
|
121
143
|
obody.close if obody.respond_to?(:close)
|
122
144
|
end
|
123
145
|
else
|
124
|
-
env[RACK_ERRORS].puts "
|
146
|
+
env[RACK_ERRORS].puts "x-accel-mapping header missing"
|
125
147
|
end
|
126
|
-
when
|
148
|
+
when /x-sendfile|x-lighttpd-send-file/i
|
127
149
|
path = ::File.expand_path(body.to_path)
|
128
150
|
headers[CONTENT_LENGTH] = '0'
|
129
|
-
headers[type] = path
|
151
|
+
headers[type.downcase] = path
|
130
152
|
obody = body
|
131
|
-
|
153
|
+
response[2] = Rack::BodyProxy.new([]) do
|
132
154
|
obody.close if obody.respond_to?(:close)
|
133
155
|
end
|
134
156
|
when '', nil
|
@@ -136,26 +158,39 @@ module Rack
|
|
136
158
|
env[RACK_ERRORS].puts "Unknown x-sendfile variation: #{type.inspect}"
|
137
159
|
end
|
138
160
|
end
|
139
|
-
|
161
|
+
response
|
140
162
|
end
|
141
163
|
|
142
164
|
private
|
165
|
+
|
143
166
|
def variation(env)
|
144
|
-
|
145
|
-
|
146
|
-
|
167
|
+
# Note: HTTP_X_SENDFILE_TYPE is intentionally NOT read for security reasons.
|
168
|
+
# Attackers could use this header to enable x-accel-redirect and bypass proxy restrictions.
|
169
|
+
@variation || env['sendfile.type']
|
170
|
+
end
|
171
|
+
|
172
|
+
def x_accel_mapping(env)
|
173
|
+
# Only allow header when:
|
174
|
+
# 1. `x-accel-redirect` is explicitly enabled via constructor.
|
175
|
+
# 2. No application-level mappings are configured.
|
176
|
+
return nil unless @variation =~ /x-accel-redirect/i
|
177
|
+
return nil if @mappings.any?
|
178
|
+
|
179
|
+
env['HTTP_X_ACCEL_MAPPING']
|
147
180
|
end
|
148
181
|
|
149
182
|
def map_accel_path(env, path)
|
150
183
|
if mapping = @mappings.find { |internal, _| internal =~ path }
|
151
|
-
path.sub(*mapping)
|
152
|
-
elsif mapping = env
|
184
|
+
return path.sub(*mapping)
|
185
|
+
elsif mapping = x_accel_mapping(env)
|
186
|
+
# Safe to use header: explicit config + no app mappings:
|
153
187
|
mapping.split(',').map(&:strip).each do |m|
|
154
188
|
internal, external = m.split('=', 2).map(&:strip)
|
155
|
-
new_path = path.sub(
|
189
|
+
new_path = path.sub(/\A#{internal}/i, external)
|
156
190
|
return new_path unless path == new_path
|
157
191
|
end
|
158
|
-
|
192
|
+
|
193
|
+
return path
|
159
194
|
end
|
160
195
|
end
|
161
196
|
end
|