rack 3.0.5 → 3.2.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.

Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +383 -1
  3. data/CONTRIBUTING.md +11 -9
  4. data/README.md +95 -28
  5. data/SPEC.rdoc +206 -288
  6. data/lib/rack/auth/abstract/request.rb +2 -0
  7. data/lib/rack/auth/basic.rb +1 -2
  8. data/lib/rack/bad_request.rb +8 -0
  9. data/lib/rack/body_proxy.rb +18 -2
  10. data/lib/rack/builder.rb +29 -10
  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 +3 -1
  15. data/lib/rack/etag.rb +3 -0
  16. data/lib/rack/head.rb +2 -3
  17. data/lib/rack/headers.rb +86 -2
  18. data/lib/rack/lint.rb +482 -425
  19. data/lib/rack/media_type.rb +18 -9
  20. data/lib/rack/mime.rb +6 -5
  21. data/lib/rack/mock_request.rb +10 -15
  22. data/lib/rack/mock_response.rb +39 -18
  23. data/lib/rack/multipart/parser.rb +169 -73
  24. data/lib/rack/multipart/uploaded_file.rb +42 -5
  25. data/lib/rack/multipart.rb +9 -1
  26. data/lib/rack/query_parser.rb +86 -97
  27. data/lib/rack/request.rb +67 -100
  28. data/lib/rack/response.rb +29 -19
  29. data/lib/rack/rewindable_input.rb +4 -1
  30. data/lib/rack/sendfile.rb +2 -2
  31. data/lib/rack/show_exceptions.rb +10 -4
  32. data/lib/rack/show_status.rb +0 -2
  33. data/lib/rack/static.rb +2 -1
  34. data/lib/rack/utils.rb +78 -110
  35. data/lib/rack/version.rb +3 -20
  36. data/lib/rack.rb +11 -17
  37. metadata +6 -15
  38. data/lib/rack/auth/digest/md5.rb +0 -1
  39. data/lib/rack/auth/digest/nonce.rb +0 -1
  40. data/lib/rack/auth/digest/params.rb +0 -1
  41. data/lib/rack/auth/digest/request.rb +0 -1
  42. data/lib/rack/auth/digest.rb +0 -256
  43. data/lib/rack/chunked.rb +0 -120
  44. data/lib/rack/file.rb +0 -9
  45. data/lib/rack/logger.rb +0 -22
@@ -4,7 +4,7 @@ module Rack
4
4
  # Rack::MediaType parse media type and parameters out of content_type string
5
5
 
6
6
  class MediaType
7
- SPLIT_PATTERN = %r{\s*[;,]\s*}
7
+ SPLIT_PATTERN = /[;,]/
8
8
 
9
9
  class << self
10
10
  # The media type (type/subtype) portion of the CONTENT_TYPE header
@@ -14,8 +14,11 @@ module Rack
14
14
  # For more information on the use of media types in HTTP, see:
15
15
  # http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7
16
16
  def type(content_type)
17
- return nil unless content_type
18
- content_type.split(SPLIT_PATTERN, 2).first.tap(&:downcase!)
17
+ return nil unless content_type && !content_type.empty?
18
+ type = content_type.split(SPLIT_PATTERN, 2).first
19
+ type.rstrip!
20
+ type.downcase!
21
+ type
19
22
  end
20
23
 
21
24
  # The media type parameters provided in CONTENT_TYPE as a Hash, or
@@ -23,21 +26,27 @@ module Rack
23
26
  # provided. e.g., when the CONTENT_TYPE is "text/plain;charset=utf-8",
24
27
  # this method responds with the following Hash:
25
28
  # { 'charset' => 'utf-8' }
29
+ #
30
+ # This will pass back parameters with empty strings in the hash if they
31
+ # lack a value (e.g., "text/plain;charset=" will return { 'charset' => '' },
32
+ # and "text/plain;charset" will return { 'charset' => '' }, similarly to
33
+ # the query params parser (barring the latter case, which returns nil instead)).
26
34
  def params(content_type)
27
- return {} if content_type.nil?
35
+ return {} if content_type.nil? || content_type.empty?
28
36
 
29
37
  content_type.split(SPLIT_PATTERN)[1..-1].each_with_object({}) do |s, hsh|
38
+ s.strip!
30
39
  k, v = s.split('=', 2)
31
-
32
- hsh[k.tap(&:downcase!)] = strip_doublequotes(v)
40
+ k.downcase!
41
+ hsh[k] = strip_doublequotes(v)
33
42
  end
34
43
  end
35
44
 
36
45
  private
37
46
 
38
- def strip_doublequotes(str)
39
- (str.start_with?('"') && str.end_with?('"')) ? str[1..-2] : str
40
- end
47
+ def strip_doublequotes(str)
48
+ (str && str.start_with?('"') && str.end_with?('"')) ? str[1..-2] : str || ''
49
+ end
41
50
  end
42
51
  end
43
52
  end
data/lib/rack/mime.rb CHANGED
@@ -290,7 +290,7 @@ module Rack
290
290
  ".jpg" => "image/jpeg",
291
291
  ".jpgv" => "video/jpeg",
292
292
  ".jpm" => "video/jpm",
293
- ".js" => "application/javascript",
293
+ ".js" => "text/javascript",
294
294
  ".json" => "application/json",
295
295
  ".karbon" => "application/vnd.kde.karbon",
296
296
  ".kfo" => "application/vnd.kde.kformula",
@@ -338,6 +338,7 @@ module Rack
338
338
  ".mif" => "application/vnd.mif",
339
339
  ".mime" => "message/rfc822",
340
340
  ".mj2" => "video/mj2",
341
+ ".mjs" => "text/javascript",
341
342
  ".mlp" => "application/vnd.dolby.mlp",
342
343
  ".mmd" => "application/vnd.chipnuts.karaoke-mmd",
343
344
  ".mmf" => "application/vnd.smaf",
@@ -409,7 +410,7 @@ module Rack
409
410
  ".ogx" => "application/ogg",
410
411
  ".org" => "application/vnd.lotus-organizer",
411
412
  ".otc" => "application/vnd.oasis.opendocument.chart-template",
412
- ".otf" => "application/vnd.oasis.opendocument.formula-template",
413
+ ".otf" => "font/otf",
413
414
  ".otg" => "application/vnd.oasis.opendocument.graphics-template",
414
415
  ".oth" => "application/vnd.oasis.opendocument.text-web",
415
416
  ".oti" => "application/vnd.oasis.opendocument.image-template",
@@ -590,7 +591,7 @@ module Rack
590
591
  ".trm" => "application/x-msterminal",
591
592
  ".ts" => "video/mp2t",
592
593
  ".tsv" => "text/tab-separated-values",
593
- ".ttf" => "application/octet-stream",
594
+ ".ttf" => "font/ttf",
594
595
  ".twd" => "application/vnd.simtech-mindmapper",
595
596
  ".txd" => "application/vnd.genomatix.tuxedo",
596
597
  ".txf" => "application/vnd.mobius.txf",
@@ -636,8 +637,8 @@ module Rack
636
637
  ".wmv" => "video/x-ms-wmv",
637
638
  ".wmx" => "video/x-ms-wmx",
638
639
  ".wmz" => "application/x-ms-wmz",
639
- ".woff" => "application/font-woff",
640
- ".woff2" => "application/font-woff2",
640
+ ".woff" => "font/woff",
641
+ ".woff2" => "font/woff2",
641
642
  ".wpd" => "application/vnd.wordperfect",
642
643
  ".wpl" => "application/vnd.ms-wpl",
643
644
  ".wps" => "application/vnd.ms-works",
@@ -41,11 +41,6 @@ module Rack
41
41
  end
42
42
  end
43
43
 
44
- DEFAULT_ENV = {
45
- RACK_INPUT => StringIO.new,
46
- RACK_ERRORS => StringIO.new,
47
- }.freeze
48
-
49
44
  def initialize(app)
50
45
  @app = app
51
46
  end
@@ -104,7 +99,7 @@ module Rack
104
99
  uri = parse_uri_rfc2396(uri)
105
100
  uri.path = "/#{uri.path}" unless uri.path[0] == ?/
106
101
 
107
- env = DEFAULT_ENV.dup
102
+ env = {}
108
103
 
109
104
  env[REQUEST_METHOD] = (opts[:method] ? opts[:method].to_s.upcase : GET).b
110
105
  env[SERVER_NAME] = (uri.host || "example.org").b
@@ -144,20 +139,20 @@ module Rack
144
139
  end
145
140
  end
146
141
 
147
- opts[:input] ||= String.new
148
- if String === opts[:input]
149
- rack_input = StringIO.new(opts[:input])
150
- else
151
- rack_input = opts[:input]
142
+ rack_input = opts[:input]
143
+ if String === rack_input
144
+ rack_input = StringIO.new(rack_input)
152
145
  end
153
146
 
154
- rack_input.set_encoding(Encoding::BINARY)
155
- env[RACK_INPUT] = rack_input
147
+ if rack_input
148
+ rack_input.set_encoding(Encoding::BINARY) if rack_input.respond_to?(:set_encoding)
149
+ env[RACK_INPUT] = rack_input
156
150
 
157
- env["CONTENT_LENGTH"] ||= env[RACK_INPUT].size.to_s if env[RACK_INPUT].respond_to?(:size)
151
+ env["CONTENT_LENGTH"] ||= env[RACK_INPUT].size.to_s if env[RACK_INPUT].respond_to?(:size)
152
+ end
158
153
 
159
154
  opts.each { |field, value|
160
- env[field] = value if String === field
155
+ env[field] = value if String === field
161
156
  }
162
157
 
163
158
  env
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'cgi/cookie'
4
3
  require 'time'
5
4
 
6
5
  require_relative 'response'
@@ -11,6 +10,30 @@ module Rack
11
10
  # MockRequest.
12
11
 
13
12
  class MockResponse < Rack::Response
13
+ class Cookie
14
+ attr_reader :name, :value, :path, :domain, :expires, :secure
15
+
16
+ def initialize(args)
17
+ @name = args["name"]
18
+ @value = args["value"]
19
+ @path = args["path"]
20
+ @domain = args["domain"]
21
+ @expires = args["expires"]
22
+ @secure = args["secure"]
23
+ end
24
+
25
+ def method_missing(method_name, *args, &block)
26
+ @value.send(method_name, *args, &block)
27
+ end
28
+ # :nocov:
29
+ ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
30
+ # :nocov:
31
+
32
+ def respond_to_missing?(method_name, include_all = false)
33
+ @value.respond_to?(method_name, include_all) || super
34
+ end
35
+ end
36
+
14
37
  class << self
15
38
  alias [] new
16
39
  end
@@ -78,22 +101,20 @@ module Rack
78
101
 
79
102
  def parse_cookies_from_header
80
103
  cookies = Hash.new
81
- if headers.has_key? 'set-cookie'
82
- set_cookie_header = headers.fetch('set-cookie')
83
- Array(set_cookie_header).each do |header_value|
84
- header_value.split("\n").each do |cookie|
85
- cookie_name, cookie_filling = cookie.split('=', 2)
86
- cookie_attributes = identify_cookie_attributes cookie_filling
87
- parsed_cookie = CGI::Cookie.new(
88
- 'name' => cookie_name.strip,
89
- 'value' => cookie_attributes.fetch('value'),
90
- 'path' => cookie_attributes.fetch('path', nil),
91
- 'domain' => cookie_attributes.fetch('domain', nil),
92
- 'expires' => cookie_attributes.fetch('expires', nil),
93
- 'secure' => cookie_attributes.fetch('secure', false)
94
- )
95
- cookies.store(cookie_name, parsed_cookie)
96
- end
104
+ set_cookie_header = headers['set-cookie']
105
+ if set_cookie_header && !set_cookie_header.empty?
106
+ Array(set_cookie_header).each do |cookie|
107
+ cookie_name, cookie_filling = cookie.split('=', 2)
108
+ cookie_attributes = identify_cookie_attributes cookie_filling
109
+ parsed_cookie = Cookie.new(
110
+ 'name' => cookie_name.strip,
111
+ 'value' => cookie_attributes.fetch('value'),
112
+ 'path' => cookie_attributes.fetch('path', nil),
113
+ 'domain' => cookie_attributes.fetch('domain', nil),
114
+ 'expires' => cookie_attributes.fetch('expires', nil),
115
+ 'secure' => cookie_attributes.fetch('secure', false)
116
+ )
117
+ cookies.store(cookie_name, parsed_cookie)
97
118
  end
98
119
  end
99
120
  cookies
@@ -102,7 +123,7 @@ module Rack
102
123
  def identify_cookie_attributes(cookie_filling)
103
124
  cookie_bits = cookie_filling.split(';')
104
125
  cookie_attributes = Hash.new
105
- cookie_attributes.store('value', cookie_bits[0].strip)
126
+ cookie_attributes.store('value', Array(cookie_bits[0].strip))
106
127
  cookie_bits.drop(1).each do |bit|
107
128
  if bit.include? '='
108
129
  cookie_attribute, attribute_value = bit.split('=', 2)
@@ -3,51 +3,60 @@
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
10
13
 
11
- class MultipartTotalPartLimitError < StandardError; end
14
+ class MultipartTotalPartLimitError < StandardError
15
+ include BadRequest
16
+ end
12
17
 
13
18
  # Use specific error class when parsing multipart request
14
19
  # that ends early.
15
- class EmptyContentError < ::EOFError; end
20
+ class EmptyContentError < ::EOFError
21
+ include BadRequest
22
+ end
16
23
 
17
24
  # Base class for multipart exceptions that do not subclass from
18
25
  # other exception classes for backwards compatibility.
19
- class Error < StandardError; end
26
+ class BoundaryTooLongError < StandardError
27
+ include BadRequest
28
+ end
29
+
30
+ # Prefer to use the BoundaryTooLongError class or Rack::BadRequest.
31
+ Error = BoundaryTooLongError
20
32
 
21
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
22
36
  MULTIPART = %r|\Amultipart/.*boundary=\"?([^\";,]+)\"?|ni
23
- TOKEN = /[^\s()<>,;:\\"\/\[\]?=]+/
24
- CONDISP = /Content-Disposition:\s*#{TOKEN}\s*/i
25
- VALUE = /"(?:\\"|[^"])*"|#{TOKEN}/
26
- BROKEN = /^#{CONDISP}.*;\s*filename=(#{VALUE})/i
27
- MULTIPART_CONTENT_TYPE = /Content-Type: (.*)#{EOL}/ni
28
- MULTIPART_CONTENT_DISPOSITION = /Content-Disposition:[^:]*;\s*name=(#{VALUE})/ni
29
- MULTIPART_CONTENT_ID = /Content-ID:\s*([^#{EOL}]*)/ni
30
- # Updated definitions from RFC 2231
31
- ATTRIBUTE_CHAR = %r{[^ \x00-\x1f\x7f)(><@,;:\\"/\[\]?='*%]}
32
- ATTRIBUTE = /#{ATTRIBUTE_CHAR}+/
33
- SECTION = /\*[0-9]+/
34
- REGULAR_PARAMETER_NAME = /#{ATTRIBUTE}#{SECTION}?/
35
- REGULAR_PARAMETER = /(#{REGULAR_PARAMETER_NAME})=(#{VALUE})/
36
- EXTENDED_OTHER_NAME = /#{ATTRIBUTE}\*[1-9][0-9]*\*/
37
- EXTENDED_OTHER_VALUE = /%[0-9a-fA-F]{2}|#{ATTRIBUTE_CHAR}/
38
- EXTENDED_OTHER_PARAMETER = /(#{EXTENDED_OTHER_NAME})=(#{EXTENDED_OTHER_VALUE}*)/
39
- EXTENDED_INITIAL_NAME = /#{ATTRIBUTE}(?:\*0)?\*/
40
- EXTENDED_INITIAL_VALUE = /[a-zA-Z0-9\-]*'[a-zA-Z0-9\-]*'#{EXTENDED_OTHER_VALUE}*/
41
- EXTENDED_INITIAL_PARAMETER = /(#{EXTENDED_INITIAL_NAME})=(#{EXTENDED_INITIAL_VALUE})/
42
- EXTENDED_PARAMETER = /#{EXTENDED_INITIAL_PARAMETER}|#{EXTENDED_OTHER_PARAMETER}/
43
- DISPPARM = /;\s*(?:#{REGULAR_PARAMETER}|#{EXTENDED_PARAMETER})\s*/
44
- RFC2183 = /^#{CONDISP}(#{DISPPARM})+$/i
45
-
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
46
53
  class Parser
47
54
  BUFSIZE = 1_048_576
48
55
  TEXT_PLAIN = "text/plain"
49
56
  TEMPFILE_FACTORY = lambda { |filename, content_type|
50
- 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])
51
60
  }
52
61
 
53
62
  class BoundedIO # :nodoc:
@@ -98,7 +107,7 @@ module Rack
98
107
  if boundary.length > 70
99
108
  # RFC 1521 Section 7.2.1 imposes a 70 character maximum for the boundary.
100
109
  # Most clients use no more than 55 characters.
101
- raise Error, "multipart boundary size too large (#{boundary.length} characters)"
110
+ raise BoundaryTooLongError, "multipart boundary size too large (#{boundary.length} characters)"
102
111
  end
103
112
 
104
113
  io = BoundedIO.new(io, content_length) if content_length
@@ -213,6 +222,8 @@ module Rack
213
222
 
214
223
  @sbuf = StringScanner.new("".dup)
215
224
  @body_regex = /(?:#{EOL}|\A)--#{Regexp.quote(boundary)}(?:#{EOL}|--)/m
225
+ @body_regex_at_end = /#{@body_regex}\z/m
226
+ @end_boundary_size = boundary.bytesize + 4 # (-- at start, -- at finish)
216
227
  @rx_max_size = boundary.bytesize + 6 # (\r\n-- at start, either \r\n or -- at finish)
217
228
  @head_regex = /(.*?#{EOL})#{EOL}/m
218
229
  end
@@ -244,7 +255,8 @@ module Rack
244
255
  @collector.each do |part|
245
256
  part.get_data do |data|
246
257
  tag_multipart_encoding(part.filename, part.content_type, part.name, data)
247
- @query_parser.normalize_params(@params, part.name, data)
258
+ name, data = handle_dummy_encoding(part.name, data)
259
+ @query_parser.normalize_params(@params, name, data)
248
260
  end
249
261
  end
250
262
  MultipartInfo.new @params.to_params_hash, @collector.find_all(&:file?).map(&:body)
@@ -252,12 +264,6 @@ module Rack
252
264
 
253
265
  private
254
266
 
255
- def dequote(str) # From WEBrick::HTTPUtils
256
- ret = (/\A"(.*)"\Z/ =~ str) ? $1 : str.dup
257
- ret.gsub!(/\\(.)/, "\\1")
258
- ret
259
- end
260
-
261
267
  def read_data(io, outbuf)
262
268
  content = io.read(@bufsize, outbuf)
263
269
  handle_empty_content!(content)
@@ -279,7 +285,14 @@ module Rack
279
285
  @state = :MIME_HEAD
280
286
  return
281
287
  when :END_BOUNDARY
282
- # invalid multipart upload, but retry for opening boundary
288
+ # invalid multipart upload
289
+ if @sbuf.pos == @end_boundary_size && @sbuf.rest == EOL
290
+ # stop parsing a buffer if a buffer is only an end boundary.
291
+ @state = :DONE
292
+ return
293
+ end
294
+
295
+ # retry for opening boundary
283
296
  else
284
297
  # no boundary found, keep reading data
285
298
  return :want_read
@@ -297,17 +310,101 @@ module Rack
297
310
  end
298
311
  end
299
312
 
313
+ CONTENT_DISPOSITION_MAX_PARAMS = 16
314
+ CONTENT_DISPOSITION_MAX_BYTES = 1536
300
315
  def handle_mime_head
301
316
  if @sbuf.scan_until(@head_regex)
302
317
  head = @sbuf[1]
303
318
  content_type = head[MULTIPART_CONTENT_TYPE, 1]
304
- if name = head[MULTIPART_CONTENT_DISPOSITION, 1]
305
- name = dequote(name)
319
+ if (disposition = head[MULTIPART_CONTENT_DISPOSITION, 1]) &&
320
+ disposition.bytesize <= CONTENT_DISPOSITION_MAX_BYTES
321
+
322
+ # ignore actual content-disposition value (should always be form-data)
323
+ i = disposition.index(';')
324
+ disposition.slice!(0, i+1)
325
+ param = nil
326
+ num_params = 0
327
+
328
+ # Parse parameter list
329
+ while i = disposition.index('=')
330
+ # Only parse up to max parameters, to avoid potential denial of service
331
+ num_params += 1
332
+ break if num_params > CONTENT_DISPOSITION_MAX_PARAMS
333
+
334
+ # Found end of parameter name, ensure forward progress in loop
335
+ param = disposition.slice!(0, i+1)
336
+
337
+ # Remove ending equals and preceding whitespace from parameter name
338
+ param.chomp!('=')
339
+ param.lstrip!
340
+
341
+ if disposition[0] == '"'
342
+ # Parameter value is quoted, parse it, handling backslash escapes
343
+ disposition.slice!(0, 1)
344
+ value = String.new
345
+
346
+ while i = disposition.index(/(["\\])/)
347
+ c = $1
348
+
349
+ # Append all content until ending quote or escape
350
+ value << disposition.slice!(0, i)
351
+
352
+ # Remove either backslash or ending quote,
353
+ # ensures forward progress in loop
354
+ disposition.slice!(0, 1)
355
+
356
+ # stop parsing parameter value if found ending quote
357
+ break if c == '"'
358
+
359
+ escaped_char = disposition.slice!(0, 1)
360
+ if param == 'filename' && escaped_char != '"'
361
+ # Possible IE uploaded filename, append both escape backslash and value
362
+ value << c << escaped_char
363
+ else
364
+ # Other only append escaped value
365
+ value << escaped_char
366
+ end
367
+ end
368
+ else
369
+ if i = disposition.index(';')
370
+ # Parameter value unquoted (which may be invalid), value ends at semicolon
371
+ value = disposition.slice!(0, i)
372
+ else
373
+ # If no ending semicolon, assume remainder of line is value and stop
374
+ # parsing
375
+ disposition.strip!
376
+ value = disposition
377
+ disposition = ''
378
+ end
379
+ end
380
+
381
+ case param
382
+ when 'name'
383
+ name = value
384
+ when 'filename'
385
+ filename = value
386
+ when 'filename*'
387
+ filename_star = value
388
+ # else
389
+ # ignore other parameters
390
+ end
391
+
392
+ # skip trailing semicolon, to proceed to next parameter
393
+ if i = disposition.index(';')
394
+ disposition.slice!(0, i+1)
395
+ end
396
+ end
306
397
  else
307
398
  name = head[MULTIPART_CONTENT_ID, 1]
308
399
  end
309
400
 
310
- filename = get_filename(head)
401
+ if filename_star
402
+ encoding, _, filename = filename_star.split("'", 3)
403
+ filename = normalize_filename(filename || '')
404
+ filename.force_encoding(find_encoding(encoding))
405
+ elsif filename
406
+ filename = normalize_filename(filename)
407
+ end
311
408
 
312
409
  if name.nil? || name.empty?
313
410
  name = filename || "#{content_type || TEXT_PLAIN}[]".dup
@@ -322,7 +419,7 @@ module Rack
322
419
 
323
420
  def handle_mime_body
324
421
  if (body_with_boundary = @sbuf.check_until(@body_regex)) # check but do not advance the pointer yet
325
- body = body_with_boundary.sub(/#{@body_regex}\z/m, '') # remove the boundary from the string
422
+ body = body_with_boundary.sub(@body_regex_at_end, '') # remove the boundary from the string
326
423
  @collector.on_mime_body @mime_index, body
327
424
  @sbuf.pos += body.length + 2 # skip \r\n after the content
328
425
  @state = :CONSUME_TOKEN
@@ -352,39 +449,14 @@ module Rack
352
449
  end
353
450
  end
354
451
 
355
- def get_filename(head)
356
- filename = nil
357
- case head
358
- when RFC2183
359
- params = Hash[*head.scan(DISPPARM).flat_map(&:compact)]
360
-
361
- if filename = params['filename*']
362
- encoding, _, filename = filename.split("'", 3)
363
- elsif filename = params['filename']
364
- filename = $1 if filename =~ /^"(.*)"$/
365
- end
366
- when BROKEN
367
- filename = $1
368
- filename = $1 if filename =~ /^"(.*)"$/
369
- end
370
-
371
- return unless filename
372
-
452
+ def normalize_filename(filename)
373
453
  if filename.scan(/%.?.?/).all? { |s| /%[0-9a-fA-F]{2}/.match?(s) }
374
454
  filename = Utils.unescape_path(filename)
375
455
  end
376
456
 
377
457
  filename.scrub!
378
458
 
379
- if filename !~ /\\[^\\"]/
380
- filename = filename.gsub(/\\(.)/, '\1')
381
- end
382
-
383
- if encoding
384
- filename.force_encoding ::Encoding.find(encoding)
385
- end
386
-
387
- filename
459
+ filename.split(/[\/\\]/).last || String.new
388
460
  end
389
461
 
390
462
  CHARSET = "charset"
@@ -410,11 +482,7 @@ module Rack
410
482
  v.strip!
411
483
  v = v[1..-2] if v.start_with?('"') && v.end_with?('"')
412
484
  if k == "charset"
413
- encoding = begin
414
- Encoding.find v
415
- rescue ArgumentError
416
- Encoding::BINARY
417
- end
485
+ encoding = find_encoding(v)
418
486
  end
419
487
  end
420
488
  end
@@ -424,6 +492,34 @@ module Rack
424
492
  body.force_encoding(encoding)
425
493
  end
426
494
 
495
+ # Return the related Encoding object. However, because
496
+ # enc is submitted by the user, it may be invalid, so
497
+ # use a binary encoding in that case.
498
+ def find_encoding(enc)
499
+ Encoding.find enc
500
+ rescue ArgumentError
501
+ Encoding::BINARY
502
+ end
503
+
504
+ REENCODE_DUMMY_ENCODINGS = {
505
+ # ISO-2022-JP is a legacy but still widely used encoding in Japan
506
+ # Here we convert ISO-2022-JP to UTF-8 so that it can be handled.
507
+ Encoding::ISO_2022_JP => true
508
+
509
+ # Other dummy encodings are rarely used and have not been supported yet.
510
+ # Adding support for them will require careful considerations.
511
+ }
512
+
513
+ def handle_dummy_encoding(name, body)
514
+ # A string object with a 'dummy' encoding does not have full functionality and can cause errors.
515
+ # So here we covert it to UTF-8 so that it can be handled properly.
516
+ if name.encoding.dummy? && REENCODE_DUMMY_ENCODINGS[name.encoding]
517
+ name = name.encode(Encoding::UTF_8)
518
+ body = body.encode(Encoding::UTF_8)
519
+ end
520
+ return name, body
521
+ end
522
+
427
523
  def handle_empty_content!(content)
428
524
  if content.nil? || content.empty?
429
525
  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