rack 3.0.0 → 3.2.7
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 +592 -1
- data/CONTRIBUTING.md +11 -9
- data/README.md +120 -29
- data/SPEC.rdoc +206 -288
- data/lib/rack/auth/abstract/request.rb +2 -0
- data/lib/rack/auth/basic.rb +1 -4
- data/lib/rack/bad_request.rb +8 -0
- data/lib/rack/body_proxy.rb +18 -2
- data/lib/rack/builder.rb +61 -40
- data/lib/rack/cascade.rb +0 -3
- data/lib/rack/common_logger.rb +3 -2
- data/lib/rack/conditional_get.rb +4 -3
- data/lib/rack/constants.rb +6 -1
- data/lib/rack/directory.rb +6 -3
- data/lib/rack/etag.rb +3 -0
- data/lib/rack/events.rb +21 -6
- data/lib/rack/files.rb +1 -1
- data/lib/rack/head.rb +2 -3
- data/lib/rack/headers.rb +98 -14
- data/lib/rack/lint.rb +491 -428
- data/lib/rack/media_type.rb +18 -9
- data/lib/rack/method_override.rb +2 -2
- data/lib/rack/mime.rb +6 -5
- data/lib/rack/mock_request.rb +10 -15
- data/lib/rack/mock_response.rb +50 -20
- data/lib/rack/multipart/parser.rb +279 -80
- data/lib/rack/multipart/uploaded_file.rb +42 -5
- data/lib/rack/multipart.rb +34 -1
- data/lib/rack/query_parser.rb +88 -80
- data/lib/rack/request.rb +98 -65
- data/lib/rack/response.rb +35 -20
- data/lib/rack/rewindable_input.rb +4 -1
- data/lib/rack/sendfile.rb +53 -23
- data/lib/rack/show_exceptions.rb +10 -4
- data/lib/rack/show_status.rb +0 -2
- data/lib/rack/static.rb +9 -4
- data/lib/rack/urlmap.rb +1 -1
- data/lib/rack/utils.rb +205 -133
- data/lib/rack/version.rb +3 -20
- data/lib/rack.rb +11 -17
- metadata +6 -15
- data/lib/rack/auth/digest/md5.rb +0 -1
- data/lib/rack/auth/digest/nonce.rb +0 -1
- data/lib/rack/auth/digest/params.rb +0 -1
- data/lib/rack/auth/digest/request.rb +0 -1
- data/lib/rack/auth/digest.rb +0 -256
- data/lib/rack/chunked.rb +0 -120
- data/lib/rack/file.rb +0 -9
- data/lib/rack/logger.rb +0 -22
|
@@ -3,51 +3,90 @@
|
|
|
3
3
|
require 'strscan'
|
|
4
4
|
|
|
5
5
|
require_relative '../utils'
|
|
6
|
+
require_relative '../bad_request'
|
|
6
7
|
|
|
7
8
|
module Rack
|
|
8
9
|
module Multipart
|
|
9
|
-
class MultipartPartLimitError < Errno::EMFILE
|
|
10
|
+
class MultipartPartLimitError < Errno::EMFILE
|
|
11
|
+
include BadRequest
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
class MultipartTotalPartLimitError < StandardError
|
|
15
|
+
include BadRequest
|
|
16
|
+
end
|
|
10
17
|
|
|
11
18
|
# Use specific error class when parsing multipart request
|
|
12
19
|
# that ends early.
|
|
13
|
-
class EmptyContentError < ::EOFError
|
|
20
|
+
class EmptyContentError < ::EOFError
|
|
21
|
+
include BadRequest
|
|
22
|
+
end
|
|
14
23
|
|
|
15
24
|
# Base class for multipart exceptions that do not subclass from
|
|
16
25
|
# other exception classes for backwards compatibility.
|
|
17
|
-
class
|
|
26
|
+
class BoundaryTooLongError < StandardError
|
|
27
|
+
include BadRequest
|
|
28
|
+
end
|
|
18
29
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
TOKEN = /[^\s()<>,;:\\"\/\[\]?=]+/
|
|
22
|
-
CONDISP = /Content-Disposition:\s*#{TOKEN}\s*/i
|
|
23
|
-
VALUE = /"(?:\\"|[^"])*"|#{TOKEN}/
|
|
24
|
-
BROKEN = /^#{CONDISP}.*;\s*filename=(#{VALUE})/i
|
|
25
|
-
MULTIPART_CONTENT_TYPE = /Content-Type: (.*)#{EOL}/ni
|
|
26
|
-
MULTIPART_CONTENT_DISPOSITION = /Content-Disposition:.*;\s*name=(#{VALUE})/ni
|
|
27
|
-
MULTIPART_CONTENT_ID = /Content-ID:\s*([^#{EOL}]*)/ni
|
|
28
|
-
# Updated definitions from RFC 2231
|
|
29
|
-
ATTRIBUTE_CHAR = %r{[^ \t\v\n\r)(><@,;:\\"/\[\]?='*%]}
|
|
30
|
-
ATTRIBUTE = /#{ATTRIBUTE_CHAR}+/
|
|
31
|
-
SECTION = /\*[0-9]+/
|
|
32
|
-
REGULAR_PARAMETER_NAME = /#{ATTRIBUTE}#{SECTION}?/
|
|
33
|
-
REGULAR_PARAMETER = /(#{REGULAR_PARAMETER_NAME})=(#{VALUE})/
|
|
34
|
-
EXTENDED_OTHER_NAME = /#{ATTRIBUTE}\*[1-9][0-9]*\*/
|
|
35
|
-
EXTENDED_OTHER_VALUE = /%[0-9a-fA-F]{2}|#{ATTRIBUTE_CHAR}/
|
|
36
|
-
EXTENDED_OTHER_PARAMETER = /(#{EXTENDED_OTHER_NAME})=(#{EXTENDED_OTHER_VALUE}*)/
|
|
37
|
-
EXTENDED_INITIAL_NAME = /#{ATTRIBUTE}(?:\*0)?\*/
|
|
38
|
-
EXTENDED_INITIAL_VALUE = /[a-zA-Z0-9\-]*'[a-zA-Z0-9\-]*'#{EXTENDED_OTHER_VALUE}*/
|
|
39
|
-
EXTENDED_INITIAL_PARAMETER = /(#{EXTENDED_INITIAL_NAME})=(#{EXTENDED_INITIAL_VALUE})/
|
|
40
|
-
EXTENDED_PARAMETER = /#{EXTENDED_INITIAL_PARAMETER}|#{EXTENDED_OTHER_PARAMETER}/
|
|
41
|
-
DISPPARM = /;\s*(?:#{REGULAR_PARAMETER}|#{EXTENDED_PARAMETER})\s*/
|
|
42
|
-
RFC2183 = /^#{CONDISP}(#{DISPPARM})+$/i
|
|
30
|
+
# Prefer to use the BoundaryTooLongError class or Rack::BadRequest.
|
|
31
|
+
Error = BoundaryTooLongError
|
|
43
32
|
|
|
33
|
+
EOL = "\r\n"
|
|
34
|
+
FWS = /[ \t]+(?:\r\n[ \t]+)?/ # whitespace with optional folding
|
|
35
|
+
HEADER_VALUE = "(?:[^\r\n]|\r\n[ \t])*" # anything but a non-folding CRLF
|
|
36
|
+
MULTIPART = %r|\Amultipart/.*?boundary(\s*)=\"?([^\";,]+)\"?|ni
|
|
37
|
+
MULTIPART_CONTENT_TYPE = /^Content-Type:#{FWS}?(#{HEADER_VALUE})/ni
|
|
38
|
+
MULTIPART_CONTENT_DISPOSITION = /^Content-Disposition:#{FWS}?(#{HEADER_VALUE})/ni
|
|
39
|
+
MULTIPART_CONTENT_ID = /^Content-ID:#{FWS}?(#{HEADER_VALUE})/ni
|
|
40
|
+
|
|
41
|
+
# Rack::Multipart::Parser handles parsing of multipart/form-data requests.
|
|
42
|
+
#
|
|
43
|
+
# File Parameter Contents
|
|
44
|
+
#
|
|
45
|
+
# When processing file uploads, the parser returns a hash containing
|
|
46
|
+
# information about uploaded files. For +file+ parameters, the hash includes:
|
|
47
|
+
#
|
|
48
|
+
# * +:filename+ - The original filename, already URL decoded by the parser
|
|
49
|
+
# * +:type+ - The content type of the uploaded file
|
|
50
|
+
# * +:name+ - The parameter name from the form
|
|
51
|
+
# * +:tempfile+ - A Tempfile object containing the uploaded data
|
|
52
|
+
# * +:head+ - The raw header content for this part
|
|
44
53
|
class Parser
|
|
45
54
|
BUFSIZE = 1_048_576
|
|
46
55
|
TEXT_PLAIN = "text/plain"
|
|
47
56
|
TEMPFILE_FACTORY = lambda { |filename, content_type|
|
|
48
|
-
|
|
57
|
+
extension = ::File.extname(filename.gsub("\0", '%00'))[0, 129]
|
|
58
|
+
|
|
59
|
+
Tempfile.new(["RackMultipart", extension])
|
|
49
60
|
}
|
|
50
61
|
|
|
62
|
+
BOUNDARY_START_LIMIT = 16 * 1024
|
|
63
|
+
private_constant :BOUNDARY_START_LIMIT
|
|
64
|
+
|
|
65
|
+
MIME_HEADER_BYTESIZE_LIMIT = 64 * 1024
|
|
66
|
+
private_constant :MIME_HEADER_BYTESIZE_LIMIT
|
|
67
|
+
|
|
68
|
+
env_int = lambda do |key, val|
|
|
69
|
+
if str_val = ENV[key]
|
|
70
|
+
begin
|
|
71
|
+
val = Integer(str_val, 10)
|
|
72
|
+
rescue ArgumentError
|
|
73
|
+
raise ArgumentError, "non-integer value provided for environment variable #{key}"
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
val
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
BUFFERED_UPLOAD_BYTESIZE_LIMIT = env_int.call("RACK_MULTIPART_BUFFERED_UPLOAD_BYTESIZE_LIMIT", 16 * 1024 * 1024)
|
|
81
|
+
private_constant :BUFFERED_UPLOAD_BYTESIZE_LIMIT
|
|
82
|
+
|
|
83
|
+
bytesize_limit = env_int.call("RACK_MULTIPART_PARSER_BYTESIZE_LIMIT", 10 * 1024 * 1024 * 1024)
|
|
84
|
+
PARSER_BYTESIZE_LIMIT = bytesize_limit > 0 ? bytesize_limit : nil
|
|
85
|
+
private_constant :PARSER_BYTESIZE_LIMIT
|
|
86
|
+
|
|
87
|
+
CONTENT_DISPOSITION_QUOTED_ESCAPES_LIMIT = env_int.call("RACK_MULTIPART_CONTENT_DISPOSITION_QUOTED_ESCAPES_LIMIT", 8 * 1024)
|
|
88
|
+
private_constant :CONTENT_DISPOSITION_QUOTED_ESCAPES_LIMIT
|
|
89
|
+
|
|
51
90
|
class BoundedIO # :nodoc:
|
|
52
91
|
def initialize(io, content_length)
|
|
53
92
|
@io = io
|
|
@@ -84,7 +123,15 @@ module Rack
|
|
|
84
123
|
return unless content_type
|
|
85
124
|
data = content_type.match(MULTIPART)
|
|
86
125
|
return unless data
|
|
87
|
-
|
|
126
|
+
|
|
127
|
+
unless data[1].empty?
|
|
128
|
+
raise Error, "whitespace between boundary parameter name and equal sign"
|
|
129
|
+
end
|
|
130
|
+
if data.post_match.match?(/boundary\s*=/i)
|
|
131
|
+
raise BoundaryTooLongError, "multiple boundary parameters found in multipart content type"
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
data[2]
|
|
88
135
|
end
|
|
89
136
|
|
|
90
137
|
def self.parse(io, content_length, content_type, tmpfile, bufsize, qp)
|
|
@@ -93,10 +140,14 @@ module Rack
|
|
|
93
140
|
boundary = parse_boundary content_type
|
|
94
141
|
return EMPTY unless boundary
|
|
95
142
|
|
|
143
|
+
if PARSER_BYTESIZE_LIMIT && content_length && content_length > PARSER_BYTESIZE_LIMIT
|
|
144
|
+
raise Error, "multipart Content-Length #{content_length} exceeds limit of #{PARSER_BYTESIZE_LIMIT} bytes"
|
|
145
|
+
end
|
|
146
|
+
|
|
96
147
|
if boundary.length > 70
|
|
97
148
|
# RFC 1521 Section 7.2.1 imposes a 70 character maximum for the boundary.
|
|
98
149
|
# Most clients use no more than 55 characters.
|
|
99
|
-
raise
|
|
150
|
+
raise BoundaryTooLongError, "multipart boundary size too large (#{boundary.length} characters)"
|
|
100
151
|
end
|
|
101
152
|
|
|
102
153
|
io = BoundedIO.new(io, content_length) if content_length
|
|
@@ -166,7 +217,7 @@ module Rack
|
|
|
166
217
|
|
|
167
218
|
@mime_parts[mime_index] = klass.new(body, head, filename, content_type, name)
|
|
168
219
|
|
|
169
|
-
|
|
220
|
+
check_part_limits
|
|
170
221
|
end
|
|
171
222
|
|
|
172
223
|
def on_mime_body(mime_index, content)
|
|
@@ -178,13 +229,23 @@ module Rack
|
|
|
178
229
|
|
|
179
230
|
private
|
|
180
231
|
|
|
181
|
-
def
|
|
182
|
-
|
|
183
|
-
|
|
232
|
+
def check_part_limits
|
|
233
|
+
file_limit = Utils.multipart_file_limit
|
|
234
|
+
part_limit = Utils.multipart_total_part_limit
|
|
235
|
+
|
|
236
|
+
if file_limit && file_limit > 0
|
|
237
|
+
if @open_files >= file_limit
|
|
184
238
|
@mime_parts.each(&:close)
|
|
185
239
|
raise MultipartPartLimitError, 'Maximum file multiparts in content reached'
|
|
186
240
|
end
|
|
187
241
|
end
|
|
242
|
+
|
|
243
|
+
if part_limit && part_limit > 0
|
|
244
|
+
if @mime_parts.size >= part_limit
|
|
245
|
+
@mime_parts.each(&:close)
|
|
246
|
+
raise MultipartTotalPartLimitError, 'Maximum total multiparts in content reached'
|
|
247
|
+
end
|
|
248
|
+
end
|
|
188
249
|
end
|
|
189
250
|
end
|
|
190
251
|
|
|
@@ -197,15 +258,22 @@ module Rack
|
|
|
197
258
|
|
|
198
259
|
@state = :FAST_FORWARD
|
|
199
260
|
@mime_index = 0
|
|
261
|
+
@body_retained = nil
|
|
262
|
+
@retained_size = 0
|
|
263
|
+
@total_bytes_read = (0 if PARSER_BYTESIZE_LIMIT)
|
|
264
|
+
@content_disposition_quoted_escapes = 0
|
|
200
265
|
@collector = Collector.new tempfile
|
|
201
266
|
|
|
202
267
|
@sbuf = StringScanner.new("".dup)
|
|
203
268
|
@body_regex = /(?:#{EOL}|\A)--#{Regexp.quote(boundary)}(?:#{EOL}|--)/m
|
|
269
|
+
@body_regex_at_end = /#{@body_regex}\z/m
|
|
270
|
+
@end_boundary_size = boundary.bytesize + 4 # (-- at start, -- at finish)
|
|
204
271
|
@rx_max_size = boundary.bytesize + 6 # (\r\n-- at start, either \r\n or -- at finish)
|
|
205
272
|
@head_regex = /(.*?#{EOL})#{EOL}/m
|
|
206
273
|
end
|
|
207
274
|
|
|
208
275
|
def parse(io)
|
|
276
|
+
@total_bytes_read &&= nil if io.is_a?(BoundedIO)
|
|
209
277
|
outbuf = String.new
|
|
210
278
|
read_data(io, outbuf)
|
|
211
279
|
|
|
@@ -232,7 +300,8 @@ module Rack
|
|
|
232
300
|
@collector.each do |part|
|
|
233
301
|
part.get_data do |data|
|
|
234
302
|
tag_multipart_encoding(part.filename, part.content_type, part.name, data)
|
|
235
|
-
|
|
303
|
+
name, data = handle_dummy_encoding(part.name, data)
|
|
304
|
+
@query_parser.normalize_params(@params, name, data)
|
|
236
305
|
end
|
|
237
306
|
end
|
|
238
307
|
MultipartInfo.new @params.to_params_hash, @collector.find_all(&:file?).map(&:body)
|
|
@@ -240,15 +309,15 @@ module Rack
|
|
|
240
309
|
|
|
241
310
|
private
|
|
242
311
|
|
|
243
|
-
def dequote(str) # From WEBrick::HTTPUtils
|
|
244
|
-
ret = (/\A"(.*)"\Z/ =~ str) ? $1 : str.dup
|
|
245
|
-
ret.gsub!(/\\(.)/, "\\1")
|
|
246
|
-
ret
|
|
247
|
-
end
|
|
248
|
-
|
|
249
312
|
def read_data(io, outbuf)
|
|
250
313
|
content = io.read(@bufsize, outbuf)
|
|
251
314
|
handle_empty_content!(content)
|
|
315
|
+
if @total_bytes_read
|
|
316
|
+
@total_bytes_read += content.bytesize
|
|
317
|
+
if @total_bytes_read > PARSER_BYTESIZE_LIMIT
|
|
318
|
+
raise Error, "multipart upload exceeds limit of #{PARSER_BYTESIZE_LIMIT} bytes"
|
|
319
|
+
end
|
|
320
|
+
end
|
|
252
321
|
@sbuf.concat(content)
|
|
253
322
|
end
|
|
254
323
|
|
|
@@ -267,8 +336,19 @@ module Rack
|
|
|
267
336
|
@state = :MIME_HEAD
|
|
268
337
|
return
|
|
269
338
|
when :END_BOUNDARY
|
|
270
|
-
# invalid multipart upload
|
|
339
|
+
# invalid multipart upload
|
|
340
|
+
if @sbuf.pos == @end_boundary_size && @sbuf.rest == EOL
|
|
341
|
+
# stop parsing a buffer if a buffer is only an end boundary.
|
|
342
|
+
@state = :DONE
|
|
343
|
+
return
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
# retry for opening boundary
|
|
271
347
|
else
|
|
348
|
+
# We raise if we don't find the multipart boundary, to avoid unbounded memory
|
|
349
|
+
# buffering. Note that the actual limit is the higher of 16KB and the buffer size (1MB by default)
|
|
350
|
+
raise Error, "multipart boundary not found within limit" if @sbuf.string.bytesize > BOUNDARY_START_LIMIT
|
|
351
|
+
|
|
272
352
|
# no boundary found, keep reading data
|
|
273
353
|
return :want_read
|
|
274
354
|
end
|
|
@@ -285,32 +365,143 @@ module Rack
|
|
|
285
365
|
end
|
|
286
366
|
end
|
|
287
367
|
|
|
368
|
+
CONTENT_DISPOSITION_MAX_PARAMS = 16
|
|
369
|
+
CONTENT_DISPOSITION_MAX_BYTES = 1536
|
|
370
|
+
OBS_UNFOLD = /\r\n([ \t])/
|
|
371
|
+
private_constant :OBS_UNFOLD
|
|
372
|
+
|
|
288
373
|
def handle_mime_head
|
|
289
374
|
if @sbuf.scan_until(@head_regex)
|
|
290
375
|
head = @sbuf[1]
|
|
291
376
|
content_type = head[MULTIPART_CONTENT_TYPE, 1]
|
|
292
|
-
|
|
293
|
-
|
|
377
|
+
content_type.gsub!(OBS_UNFOLD, '\1') if content_type
|
|
378
|
+
|
|
379
|
+
if (disposition = head[MULTIPART_CONTENT_DISPOSITION, 1]) &&
|
|
380
|
+
disposition.bytesize <= CONTENT_DISPOSITION_MAX_BYTES
|
|
381
|
+
|
|
382
|
+
# Implement OBS unfolding (RFC 5322 Section 2.2.3)
|
|
383
|
+
disposition.gsub!(OBS_UNFOLD, '\1')
|
|
384
|
+
|
|
385
|
+
# ignore actual content-disposition value (should always be form-data)
|
|
386
|
+
i = disposition.index(';')
|
|
387
|
+
disposition.slice!(0, i+1)
|
|
388
|
+
param = nil
|
|
389
|
+
num_params = 0
|
|
390
|
+
|
|
391
|
+
# Parse parameter list
|
|
392
|
+
while i = disposition.index('=')
|
|
393
|
+
# Only parse up to max parameters, to avoid potential denial of service
|
|
394
|
+
num_params += 1
|
|
395
|
+
break if num_params > CONTENT_DISPOSITION_MAX_PARAMS
|
|
396
|
+
|
|
397
|
+
# Found end of parameter name, ensure forward progress in loop
|
|
398
|
+
param = disposition.slice!(0, i+1)
|
|
399
|
+
|
|
400
|
+
# Remove ending equals and preceding whitespace from parameter name
|
|
401
|
+
param.chomp!('=')
|
|
402
|
+
param.lstrip!
|
|
403
|
+
|
|
404
|
+
if disposition[0] == '"'
|
|
405
|
+
# Parameter value is quoted, parse it, handling backslash escapes
|
|
406
|
+
disposition.slice!(0, 1)
|
|
407
|
+
value = String.new
|
|
408
|
+
|
|
409
|
+
while i = disposition.index(/(["\\])/)
|
|
410
|
+
c = $1
|
|
411
|
+
|
|
412
|
+
# Append all content until ending quote or escape
|
|
413
|
+
value << disposition.slice!(0, i)
|
|
414
|
+
|
|
415
|
+
# Remove either backslash or ending quote,
|
|
416
|
+
# ensures forward progress in loop
|
|
417
|
+
disposition.slice!(0, 1)
|
|
418
|
+
|
|
419
|
+
# stop parsing parameter value if found ending quote
|
|
420
|
+
break if c == '"'
|
|
421
|
+
|
|
422
|
+
@content_disposition_quoted_escapes += 1
|
|
423
|
+
if @content_disposition_quoted_escapes > CONTENT_DISPOSITION_QUOTED_ESCAPES_LIMIT
|
|
424
|
+
raise Error, "number of quoted escapes during content disposition parsing exceeds limit"
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
escaped_char = disposition.slice!(0, 1)
|
|
428
|
+
if param == 'filename' && escaped_char != '"'
|
|
429
|
+
# Possible IE uploaded filename, append both escape backslash and value
|
|
430
|
+
value << c << escaped_char
|
|
431
|
+
else
|
|
432
|
+
# Other only append escaped value
|
|
433
|
+
value << escaped_char
|
|
434
|
+
end
|
|
435
|
+
end
|
|
436
|
+
else
|
|
437
|
+
if i = disposition.index(';')
|
|
438
|
+
# Parameter value unquoted (which may be invalid), value ends at semicolon
|
|
439
|
+
value = disposition.slice!(0, i)
|
|
440
|
+
else
|
|
441
|
+
# If no ending semicolon, assume remainder of line is value and stop
|
|
442
|
+
# parsing
|
|
443
|
+
disposition.strip!
|
|
444
|
+
value = disposition
|
|
445
|
+
disposition = ''
|
|
446
|
+
end
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
case param
|
|
450
|
+
when 'name'
|
|
451
|
+
name = value
|
|
452
|
+
when 'filename'
|
|
453
|
+
filename = value
|
|
454
|
+
when 'filename*'
|
|
455
|
+
filename_star = value
|
|
456
|
+
# else
|
|
457
|
+
# ignore other parameters
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
# skip trailing semicolon, to proceed to next parameter
|
|
461
|
+
if i = disposition.index(';')
|
|
462
|
+
disposition.slice!(0, i+1)
|
|
463
|
+
end
|
|
464
|
+
end
|
|
294
465
|
else
|
|
295
466
|
name = head[MULTIPART_CONTENT_ID, 1]
|
|
296
467
|
end
|
|
297
468
|
|
|
298
|
-
|
|
469
|
+
if filename_star
|
|
470
|
+
encoding, _, filename = filename_star.split("'", 3)
|
|
471
|
+
filename = normalize_filename(filename || '')
|
|
472
|
+
filename.force_encoding(find_encoding(encoding))
|
|
473
|
+
elsif filename
|
|
474
|
+
filename = normalize_filename(filename)
|
|
475
|
+
end
|
|
299
476
|
|
|
300
477
|
if name.nil? || name.empty?
|
|
301
478
|
name = filename || "#{content_type || TEXT_PLAIN}[]".dup
|
|
302
479
|
end
|
|
303
480
|
|
|
481
|
+
# Mime part head data is retained for both TempfilePart and BufferPart
|
|
482
|
+
# for the entireity of the parse, even though it isn't used for BufferPart.
|
|
483
|
+
update_retained_size(head.bytesize)
|
|
484
|
+
|
|
485
|
+
# If a filename is given, a TempfilePart will be used, so the body will
|
|
486
|
+
# not be buffered in memory. However, if a filename is not given, a BufferPart
|
|
487
|
+
# will be used, and the body will be buffered in memory.
|
|
488
|
+
@body_retained = !filename
|
|
489
|
+
|
|
304
490
|
@collector.on_mime_head @mime_index, head, filename, content_type, name
|
|
305
491
|
@state = :MIME_BODY
|
|
306
492
|
else
|
|
307
|
-
|
|
493
|
+
# We raise if the mime part header is too large, to avoid unbounded memory
|
|
494
|
+
# buffering. Note that the actual limit is the higher of 64KB and the buffer size (1MB by default)
|
|
495
|
+
raise Error, "multipart mime part header too large" if @sbuf.rest.bytesize > MIME_HEADER_BYTESIZE_LIMIT
|
|
496
|
+
|
|
497
|
+
return :want_read
|
|
308
498
|
end
|
|
309
499
|
end
|
|
310
500
|
|
|
311
501
|
def handle_mime_body
|
|
312
502
|
if (body_with_boundary = @sbuf.check_until(@body_regex)) # check but do not advance the pointer yet
|
|
313
|
-
body = body_with_boundary.sub(
|
|
503
|
+
body = body_with_boundary.sub(@body_regex_at_end, '') # remove the boundary from the string
|
|
504
|
+
update_retained_size(body.bytesize) if @body_retained
|
|
314
505
|
@collector.on_mime_body @mime_index, body
|
|
315
506
|
@sbuf.pos += body.length + 2 # skip \r\n after the content
|
|
316
507
|
@state = :CONSUME_TOKEN
|
|
@@ -319,7 +510,9 @@ module Rack
|
|
|
319
510
|
# Save what we have so far
|
|
320
511
|
if @rx_max_size < @sbuf.rest_size
|
|
321
512
|
delta = @sbuf.rest_size - @rx_max_size
|
|
322
|
-
|
|
513
|
+
body = @sbuf.peek(delta)
|
|
514
|
+
update_retained_size(body.bytesize) if @body_retained
|
|
515
|
+
@collector.on_mime_body @mime_index, body
|
|
323
516
|
@sbuf.pos += delta
|
|
324
517
|
@sbuf.string = @sbuf.rest
|
|
325
518
|
end
|
|
@@ -327,6 +520,13 @@ module Rack
|
|
|
327
520
|
end
|
|
328
521
|
end
|
|
329
522
|
|
|
523
|
+
def update_retained_size(size)
|
|
524
|
+
@retained_size += size
|
|
525
|
+
if @retained_size > BUFFERED_UPLOAD_BYTESIZE_LIMIT
|
|
526
|
+
raise Error, "multipart data over retained size limit"
|
|
527
|
+
end
|
|
528
|
+
end
|
|
529
|
+
|
|
330
530
|
# Scan until the we find the start or end of the boundary.
|
|
331
531
|
# If we find it, return the appropriate symbol for the start or
|
|
332
532
|
# end of the boundary. If we don't find the start or end of the
|
|
@@ -340,39 +540,14 @@ module Rack
|
|
|
340
540
|
end
|
|
341
541
|
end
|
|
342
542
|
|
|
343
|
-
def
|
|
344
|
-
filename = nil
|
|
345
|
-
case head
|
|
346
|
-
when RFC2183
|
|
347
|
-
params = Hash[*head.scan(DISPPARM).flat_map(&:compact)]
|
|
348
|
-
|
|
349
|
-
if filename = params['filename*']
|
|
350
|
-
encoding, _, filename = filename.split("'", 3)
|
|
351
|
-
elsif filename = params['filename']
|
|
352
|
-
filename = $1 if filename =~ /^"(.*)"$/
|
|
353
|
-
end
|
|
354
|
-
when BROKEN
|
|
355
|
-
filename = $1
|
|
356
|
-
filename = $1 if filename =~ /^"(.*)"$/
|
|
357
|
-
end
|
|
358
|
-
|
|
359
|
-
return unless filename
|
|
360
|
-
|
|
543
|
+
def normalize_filename(filename)
|
|
361
544
|
if filename.scan(/%.?.?/).all? { |s| /%[0-9a-fA-F]{2}/.match?(s) }
|
|
362
545
|
filename = Utils.unescape_path(filename)
|
|
363
546
|
end
|
|
364
547
|
|
|
365
548
|
filename.scrub!
|
|
366
549
|
|
|
367
|
-
|
|
368
|
-
filename = filename.gsub(/\\(.)/, '\1')
|
|
369
|
-
end
|
|
370
|
-
|
|
371
|
-
if encoding
|
|
372
|
-
filename.force_encoding ::Encoding.find(encoding)
|
|
373
|
-
end
|
|
374
|
-
|
|
375
|
-
filename
|
|
550
|
+
filename.split(/[\/\\]/).last || String.new
|
|
376
551
|
end
|
|
377
552
|
|
|
378
553
|
CHARSET = "charset"
|
|
@@ -398,11 +573,7 @@ module Rack
|
|
|
398
573
|
v.strip!
|
|
399
574
|
v = v[1..-2] if v.start_with?('"') && v.end_with?('"')
|
|
400
575
|
if k == "charset"
|
|
401
|
-
encoding =
|
|
402
|
-
Encoding.find v
|
|
403
|
-
rescue ArgumentError
|
|
404
|
-
Encoding::BINARY
|
|
405
|
-
end
|
|
576
|
+
encoding = find_encoding(v)
|
|
406
577
|
end
|
|
407
578
|
end
|
|
408
579
|
end
|
|
@@ -412,6 +583,34 @@ module Rack
|
|
|
412
583
|
body.force_encoding(encoding)
|
|
413
584
|
end
|
|
414
585
|
|
|
586
|
+
# Return the related Encoding object. However, because
|
|
587
|
+
# enc is submitted by the user, it may be invalid, so
|
|
588
|
+
# use a binary encoding in that case.
|
|
589
|
+
def find_encoding(enc)
|
|
590
|
+
Encoding.find enc
|
|
591
|
+
rescue ArgumentError
|
|
592
|
+
Encoding::BINARY
|
|
593
|
+
end
|
|
594
|
+
|
|
595
|
+
REENCODE_DUMMY_ENCODINGS = {
|
|
596
|
+
# ISO-2022-JP is a legacy but still widely used encoding in Japan
|
|
597
|
+
# Here we convert ISO-2022-JP to UTF-8 so that it can be handled.
|
|
598
|
+
Encoding::ISO_2022_JP => true
|
|
599
|
+
|
|
600
|
+
# Other dummy encodings are rarely used and have not been supported yet.
|
|
601
|
+
# Adding support for them will require careful considerations.
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
def handle_dummy_encoding(name, body)
|
|
605
|
+
# A string object with a 'dummy' encoding does not have full functionality and can cause errors.
|
|
606
|
+
# So here we covert it to UTF-8 so that it can be handled properly.
|
|
607
|
+
if name.encoding.dummy? && REENCODE_DUMMY_ENCODINGS[name.encoding]
|
|
608
|
+
name = name.encode(Encoding::UTF_8)
|
|
609
|
+
body = body.encode(Encoding::UTF_8)
|
|
610
|
+
end
|
|
611
|
+
return name, body
|
|
612
|
+
end
|
|
613
|
+
|
|
415
614
|
def handle_empty_content!(content)
|
|
416
615
|
if content.nil? || content.empty?
|
|
417
616
|
raise EmptyContentError
|
|
@@ -5,14 +5,47 @@ require 'fileutils'
|
|
|
5
5
|
|
|
6
6
|
module Rack
|
|
7
7
|
module Multipart
|
|
8
|
+
# Despite the misleading name, UploadedFile is designed for use for
|
|
9
|
+
# preparing multipart file upload bodies, generally for use in tests.
|
|
10
|
+
# It is not designed for and should not be used for handling uploaded
|
|
11
|
+
# files (there is no need for that, since Rack's multipart parser
|
|
12
|
+
# already creates Tempfiles for that). Using this with non-trusted
|
|
13
|
+
# filenames can create a security vulnerability.
|
|
14
|
+
#
|
|
15
|
+
# You should only use this class if you plan on passing the instances
|
|
16
|
+
# to Rack::MockRequest for use in creating multipart request bodies.
|
|
17
|
+
#
|
|
18
|
+
# UploadedFile delegates most methods to the tempfile it contains.
|
|
8
19
|
class UploadedFile
|
|
9
|
-
|
|
10
|
-
#
|
|
20
|
+
# The provided name of the file. This generally is the basename of
|
|
21
|
+
# path provided during initialization, but it can contain slashes if they
|
|
22
|
+
# were present in the filename argument when the instance was created.
|
|
11
23
|
attr_reader :original_filename
|
|
12
24
|
|
|
13
|
-
# The content type of the
|
|
25
|
+
# The content type of the instance.
|
|
14
26
|
attr_accessor :content_type
|
|
15
27
|
|
|
28
|
+
# Create a new UploadedFile. For backwards compatibility, this accepts
|
|
29
|
+
# both positional and keyword versions of the same arguments:
|
|
30
|
+
#
|
|
31
|
+
# filepath/path :: The path to the file
|
|
32
|
+
# ct/content_type :: The content_type of the file
|
|
33
|
+
# bin/binary :: Whether to set binmode on the file before copying data into it.
|
|
34
|
+
#
|
|
35
|
+
# If both positional and keyword arguments are present, the keyword arguments
|
|
36
|
+
# take precedence.
|
|
37
|
+
#
|
|
38
|
+
# The following keyword-only arguments are also accepted:
|
|
39
|
+
#
|
|
40
|
+
# filename :: Override the filename to use for the file. This is so the
|
|
41
|
+
# filename for the upload does not need to match the basename of
|
|
42
|
+
# the file path. This should not contain slashes, unless you are
|
|
43
|
+
# trying to test how an application handles invalid filenames in
|
|
44
|
+
# multipart upload bodies.
|
|
45
|
+
# io :: Use the given IO-like instance as the tempfile, instead of creating
|
|
46
|
+
# a Tempfile instance. This is useful for building multipart file
|
|
47
|
+
# upload bodies without a file being present on the filesystem. If you are
|
|
48
|
+
# providing this, you should also provide the filename argument.
|
|
16
49
|
def initialize(filepath = nil, ct = "text/plain", bin = false,
|
|
17
50
|
path: filepath, content_type: ct, binary: bin, filename: nil, io: nil)
|
|
18
51
|
if io
|
|
@@ -28,15 +61,19 @@ module Rack
|
|
|
28
61
|
@content_type = content_type
|
|
29
62
|
end
|
|
30
63
|
|
|
64
|
+
# The path of the tempfile for the instance, if the tempfile has a path.
|
|
65
|
+
# nil if the tempfile does not have a path.
|
|
31
66
|
def path
|
|
32
67
|
@tempfile.path if @tempfile.respond_to?(:path)
|
|
33
68
|
end
|
|
34
69
|
alias_method :local_path, :path
|
|
35
70
|
|
|
36
|
-
|
|
37
|
-
|
|
71
|
+
# Return true if the tempfile responds to the method.
|
|
72
|
+
def respond_to_missing?(*args)
|
|
73
|
+
@tempfile.respond_to?(*args)
|
|
38
74
|
end
|
|
39
75
|
|
|
76
|
+
# Delegate method missing calls to the tempfile.
|
|
40
77
|
def method_missing(method_name, *args, &block) #:nodoc:
|
|
41
78
|
@tempfile.__send__(method_name, *args, &block)
|
|
42
79
|
end
|
data/lib/rack/multipart.rb
CHANGED
|
@@ -6,6 +6,8 @@ require_relative 'utils'
|
|
|
6
6
|
require_relative 'multipart/parser'
|
|
7
7
|
require_relative 'multipart/generator'
|
|
8
8
|
|
|
9
|
+
require_relative 'bad_request'
|
|
10
|
+
|
|
9
11
|
module Rack
|
|
10
12
|
# A multipart form data parser, adapted from IOWA.
|
|
11
13
|
#
|
|
@@ -13,9 +15,40 @@ module Rack
|
|
|
13
15
|
module Multipart
|
|
14
16
|
MULTIPART_BOUNDARY = "AaB03x"
|
|
15
17
|
|
|
18
|
+
class MissingInputError < StandardError
|
|
19
|
+
include BadRequest
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Accumulator for multipart form data, conforming to the QueryParser API.
|
|
23
|
+
# In future, the Parser could return the pair list directly, but that would
|
|
24
|
+
# change its API.
|
|
25
|
+
class ParamList # :nodoc:
|
|
26
|
+
def self.make_params
|
|
27
|
+
new
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.normalize_params(params, key, value)
|
|
31
|
+
params << [key, value]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def initialize
|
|
35
|
+
@pairs = []
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def <<(pair)
|
|
39
|
+
@pairs << pair
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def to_params_hash
|
|
43
|
+
@pairs
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
16
47
|
class << self
|
|
17
48
|
def parse_multipart(env, params = Rack::Utils.default_query_parser)
|
|
18
|
-
io = env[RACK_INPUT]
|
|
49
|
+
unless io = env[RACK_INPUT]
|
|
50
|
+
raise MissingInputError, "Missing input stream!"
|
|
51
|
+
end
|
|
19
52
|
|
|
20
53
|
if content_length = env['CONTENT_LENGTH']
|
|
21
54
|
content_length = content_length.to_i
|