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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +592 -1
  3. data/CONTRIBUTING.md +11 -9
  4. data/README.md +120 -29
  5. data/SPEC.rdoc +206 -288
  6. data/lib/rack/auth/abstract/request.rb +2 -0
  7. data/lib/rack/auth/basic.rb +1 -4
  8. data/lib/rack/bad_request.rb +8 -0
  9. data/lib/rack/body_proxy.rb +18 -2
  10. data/lib/rack/builder.rb +61 -40
  11. data/lib/rack/cascade.rb +0 -3
  12. data/lib/rack/common_logger.rb +3 -2
  13. data/lib/rack/conditional_get.rb +4 -3
  14. data/lib/rack/constants.rb +6 -1
  15. data/lib/rack/directory.rb +6 -3
  16. data/lib/rack/etag.rb +3 -0
  17. data/lib/rack/events.rb +21 -6
  18. data/lib/rack/files.rb +1 -1
  19. data/lib/rack/head.rb +2 -3
  20. data/lib/rack/headers.rb +98 -14
  21. data/lib/rack/lint.rb +491 -428
  22. data/lib/rack/media_type.rb +18 -9
  23. data/lib/rack/method_override.rb +2 -2
  24. data/lib/rack/mime.rb +6 -5
  25. data/lib/rack/mock_request.rb +10 -15
  26. data/lib/rack/mock_response.rb +50 -20
  27. data/lib/rack/multipart/parser.rb +279 -80
  28. data/lib/rack/multipart/uploaded_file.rb +42 -5
  29. data/lib/rack/multipart.rb +34 -1
  30. data/lib/rack/query_parser.rb +88 -80
  31. data/lib/rack/request.rb +98 -65
  32. data/lib/rack/response.rb +35 -20
  33. data/lib/rack/rewindable_input.rb +4 -1
  34. data/lib/rack/sendfile.rb +53 -23
  35. data/lib/rack/show_exceptions.rb +10 -4
  36. data/lib/rack/show_status.rb +0 -2
  37. data/lib/rack/static.rb +9 -4
  38. data/lib/rack/urlmap.rb +1 -1
  39. data/lib/rack/utils.rb +205 -133
  40. data/lib/rack/version.rb +3 -20
  41. data/lib/rack.rb +11 -17
  42. metadata +6 -15
  43. data/lib/rack/auth/digest/md5.rb +0 -1
  44. data/lib/rack/auth/digest/nonce.rb +0 -1
  45. data/lib/rack/auth/digest/params.rb +0 -1
  46. data/lib/rack/auth/digest/request.rb +0 -1
  47. data/lib/rack/auth/digest.rb +0 -256
  48. data/lib/rack/chunked.rb +0 -120
  49. data/lib/rack/file.rb +0 -9
  50. 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; end
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; end
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 Error < StandardError; end
26
+ class BoundaryTooLongError < StandardError
27
+ include BadRequest
28
+ end
18
29
 
19
- EOL = "\r\n"
20
- MULTIPART = %r|\Amultipart/.*boundary=\"?([^\";,]+)\"?|ni
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
- Tempfile.new(["RackMultipart", ::File.extname(filename.gsub("\0", '%00'))])
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
- data[1]
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 Error, "multipart boundary size too large (#{boundary.length} characters)"
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
- check_open_files
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 check_open_files
182
- if Utils.multipart_part_limit > 0
183
- if @open_files >= Utils.multipart_part_limit
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
- @query_parser.normalize_params(@params, part.name, data)
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, but retry for opening boundary
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
- if name = head[MULTIPART_CONTENT_DISPOSITION, 1]
293
- name = dequote(name)
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
- filename = get_filename(head)
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
- :want_read
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(/#{@body_regex}\z/m, '') # remove the boundary from the string
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
- @collector.on_mime_body @mime_index, @sbuf.peek(delta)
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 get_filename(head)
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
- if filename !~ /\\[^\\"]/
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 = begin
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
- # The filename, *not* including the path, of the "uploaded" file
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 "uploaded" file
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
- def respond_to?(*args)
37
- super or @tempfile.respond_to?(*args)
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
@@ -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