rack 2.2.7 → 3.1.3

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of rack might be problematic. Click here for more details.

Files changed (87) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +291 -78
  3. data/CONTRIBUTING.md +63 -55
  4. data/MIT-LICENSE +1 -1
  5. data/README.md +328 -0
  6. data/SPEC.rdoc +213 -136
  7. data/lib/rack/auth/abstract/handler.rb +3 -1
  8. data/lib/rack/auth/abstract/request.rb +3 -1
  9. data/lib/rack/auth/basic.rb +1 -4
  10. data/lib/rack/bad_request.rb +8 -0
  11. data/lib/rack/body_proxy.rb +21 -3
  12. data/lib/rack/builder.rb +102 -69
  13. data/lib/rack/cascade.rb +2 -3
  14. data/lib/rack/common_logger.rb +23 -18
  15. data/lib/rack/conditional_get.rb +18 -15
  16. data/lib/rack/constants.rb +67 -0
  17. data/lib/rack/content_length.rb +12 -16
  18. data/lib/rack/content_type.rb +8 -5
  19. data/lib/rack/deflater.rb +40 -26
  20. data/lib/rack/directory.rb +9 -3
  21. data/lib/rack/etag.rb +14 -23
  22. data/lib/rack/events.rb +4 -0
  23. data/lib/rack/files.rb +15 -17
  24. data/lib/rack/head.rb +9 -8
  25. data/lib/rack/headers.rb +238 -0
  26. data/lib/rack/lint.rb +864 -681
  27. data/lib/rack/lock.rb +2 -5
  28. data/lib/rack/logger.rb +3 -0
  29. data/lib/rack/media_type.rb +9 -4
  30. data/lib/rack/method_override.rb +5 -1
  31. data/lib/rack/mime.rb +14 -5
  32. data/lib/rack/mock.rb +1 -271
  33. data/lib/rack/mock_request.rb +171 -0
  34. data/lib/rack/mock_response.rb +124 -0
  35. data/lib/rack/multipart/generator.rb +7 -5
  36. data/lib/rack/multipart/parser.rb +218 -91
  37. data/lib/rack/multipart/uploaded_file.rb +4 -0
  38. data/lib/rack/multipart.rb +53 -40
  39. data/lib/rack/null_logger.rb +9 -0
  40. data/lib/rack/query_parser.rb +81 -102
  41. data/lib/rack/recursive.rb +2 -0
  42. data/lib/rack/reloader.rb +0 -2
  43. data/lib/rack/request.rb +248 -123
  44. data/lib/rack/response.rb +146 -66
  45. data/lib/rack/rewindable_input.rb +24 -5
  46. data/lib/rack/runtime.rb +7 -6
  47. data/lib/rack/sendfile.rb +30 -25
  48. data/lib/rack/show_exceptions.rb +21 -4
  49. data/lib/rack/show_status.rb +17 -7
  50. data/lib/rack/static.rb +8 -8
  51. data/lib/rack/tempfile_reaper.rb +15 -4
  52. data/lib/rack/urlmap.rb +3 -1
  53. data/lib/rack/utils.rb +237 -235
  54. data/lib/rack/version.rb +1 -9
  55. data/lib/rack.rb +13 -89
  56. metadata +15 -41
  57. data/README.rdoc +0 -320
  58. data/Rakefile +0 -130
  59. data/bin/rackup +0 -5
  60. data/contrib/rack.png +0 -0
  61. data/contrib/rack.svg +0 -150
  62. data/contrib/rack_logo.svg +0 -164
  63. data/contrib/rdoc.css +0 -412
  64. data/example/lobster.ru +0 -6
  65. data/example/protectedlobster.rb +0 -16
  66. data/example/protectedlobster.ru +0 -10
  67. data/lib/rack/auth/digest/md5.rb +0 -131
  68. data/lib/rack/auth/digest/nonce.rb +0 -54
  69. data/lib/rack/auth/digest/params.rb +0 -54
  70. data/lib/rack/auth/digest/request.rb +0 -43
  71. data/lib/rack/chunked.rb +0 -117
  72. data/lib/rack/core_ext/regexp.rb +0 -14
  73. data/lib/rack/file.rb +0 -7
  74. data/lib/rack/handler/cgi.rb +0 -59
  75. data/lib/rack/handler/fastcgi.rb +0 -100
  76. data/lib/rack/handler/lsws.rb +0 -61
  77. data/lib/rack/handler/scgi.rb +0 -71
  78. data/lib/rack/handler/thin.rb +0 -36
  79. data/lib/rack/handler/webrick.rb +0 -129
  80. data/lib/rack/handler.rb +0 -104
  81. data/lib/rack/lobster.rb +0 -70
  82. data/lib/rack/server.rb +0 -466
  83. data/lib/rack/session/abstract/id.rb +0 -523
  84. data/lib/rack/session/cookie.rb +0 -203
  85. data/lib/rack/session/memcache.rb +0 -10
  86. data/lib/rack/session/pool.rb +0 -85
  87. data/rack.gemspec +0 -46
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cgi/cookie'
4
+ require 'time'
5
+
6
+ require_relative 'response'
7
+
8
+ module Rack
9
+ # Rack::MockResponse provides useful helpers for testing your apps.
10
+ # Usually, you don't create the MockResponse on your own, but use
11
+ # MockRequest.
12
+
13
+ class MockResponse < Rack::Response
14
+ class << self
15
+ alias [] new
16
+ end
17
+
18
+ # Headers
19
+ attr_reader :original_headers, :cookies
20
+
21
+ # Errors
22
+ attr_accessor :errors
23
+
24
+ def initialize(status, headers, body, errors = nil)
25
+ @original_headers = headers
26
+
27
+ if errors
28
+ @errors = errors.string if errors.respond_to?(:string)
29
+ else
30
+ @errors = ""
31
+ end
32
+
33
+ super(body, status, headers)
34
+
35
+ @cookies = parse_cookies_from_header
36
+ buffered_body!
37
+ end
38
+
39
+ def =~(other)
40
+ body =~ other
41
+ end
42
+
43
+ def match(other)
44
+ body.match other
45
+ end
46
+
47
+ def body
48
+ return @buffered_body if defined?(@buffered_body)
49
+
50
+ # FIXME: apparently users of MockResponse expect the return value of
51
+ # MockResponse#body to be a string. However, the real response object
52
+ # returns the body as a list.
53
+ #
54
+ # See spec_showstatus.rb:
55
+ #
56
+ # should "not replace existing messages" do
57
+ # ...
58
+ # res.body.should == "foo!"
59
+ # end
60
+ buffer = @buffered_body = String.new
61
+
62
+ @body.each do |chunk|
63
+ buffer << chunk
64
+ end
65
+
66
+ return buffer
67
+ end
68
+
69
+ def empty?
70
+ [201, 204, 304].include? status
71
+ end
72
+
73
+ def cookie(name)
74
+ cookies.fetch(name, nil)
75
+ end
76
+
77
+ private
78
+
79
+ def parse_cookies_from_header
80
+ cookies = Hash.new
81
+ set_cookie_header = headers['set-cookie']
82
+ if set_cookie_header && !set_cookie_header.empty?
83
+ Array(set_cookie_header).each do |cookie|
84
+ cookie_name, cookie_filling = cookie.split('=', 2)
85
+ cookie_attributes = identify_cookie_attributes cookie_filling
86
+ parsed_cookie = CGI::Cookie.new(
87
+ 'name' => cookie_name.strip,
88
+ 'value' => cookie_attributes.fetch('value'),
89
+ 'path' => cookie_attributes.fetch('path', nil),
90
+ 'domain' => cookie_attributes.fetch('domain', nil),
91
+ 'expires' => cookie_attributes.fetch('expires', nil),
92
+ 'secure' => cookie_attributes.fetch('secure', false)
93
+ )
94
+ cookies.store(cookie_name, parsed_cookie)
95
+ end
96
+ end
97
+ cookies
98
+ end
99
+
100
+ def identify_cookie_attributes(cookie_filling)
101
+ cookie_bits = cookie_filling.split(';')
102
+ cookie_attributes = Hash.new
103
+ cookie_attributes.store('value', cookie_bits[0].strip)
104
+ cookie_bits.drop(1).each do |bit|
105
+ if bit.include? '='
106
+ cookie_attribute, attribute_value = bit.split('=', 2)
107
+ cookie_attributes.store(cookie_attribute.strip.downcase, attribute_value.strip)
108
+ end
109
+ if bit.include? 'secure'
110
+ cookie_attributes.store('secure', true)
111
+ end
112
+ end
113
+
114
+ if cookie_attributes.key? 'max-age'
115
+ cookie_attributes.store('expires', Time.now + cookie_attributes['max-age'].to_i)
116
+ elsif cookie_attributes.key? 'expires'
117
+ cookie_attributes.store('expires', Time.httpdate(cookie_attributes['expires']))
118
+ end
119
+
120
+ cookie_attributes
121
+ end
122
+
123
+ end
124
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'uploaded_file'
4
+
3
5
  module Rack
4
6
  module Multipart
5
7
  class Generator
@@ -74,12 +76,12 @@ module Rack
74
76
 
75
77
  def content_for_tempfile(io, file, name)
76
78
  length = ::File.stat(file.path).size if file.path
77
- filename = "; filename=\"#{Utils.escape(file.original_filename)}\"" if file.original_filename
79
+ filename = "; filename=\"#{Utils.escape_path(file.original_filename)}\""
78
80
  <<-EOF
79
81
  --#{MULTIPART_BOUNDARY}\r
80
- Content-Disposition: form-data; name="#{name}"#{filename}\r
81
- Content-Type: #{file.content_type}\r
82
- #{"Content-Length: #{length}\r\n" if length}\r
82
+ content-disposition: form-data; name="#{name}"#{filename}\r
83
+ content-type: #{file.content_type}\r
84
+ #{"content-length: #{length}\r\n" if length}\r
83
85
  #{io.read}\r
84
86
  EOF
85
87
  end
@@ -87,7 +89,7 @@ EOF
87
89
  def content_for_other(file, name)
88
90
  <<-EOF
89
91
  --#{MULTIPART_BOUNDARY}\r
90
- Content-Disposition: form-data; name="#{name}"\r
92
+ content-disposition: form-data; name="#{name}"\r
91
93
  \r
92
94
  #{file}\r
93
95
  EOF
@@ -2,21 +2,48 @@
2
2
 
3
3
  require 'strscan'
4
4
 
5
+ require_relative '../utils'
6
+ require_relative '../bad_request'
7
+
5
8
  module Rack
6
9
  module Multipart
7
- class MultipartPartLimitError < Errno::EMFILE; end
8
- class MultipartTotalPartLimitError < StandardError; end
10
+ class MultipartPartLimitError < Errno::EMFILE
11
+ include BadRequest
12
+ end
9
13
 
10
- class Parser
11
- (require_relative '../core_ext/regexp'; using ::Rack::RegexpExtensions) if RUBY_VERSION < '2.4'
14
+ class MultipartTotalPartLimitError < StandardError
15
+ include BadRequest
16
+ end
17
+
18
+ # Use specific error class when parsing multipart request
19
+ # that ends early.
20
+ class EmptyContentError < ::EOFError
21
+ include BadRequest
22
+ end
23
+
24
+ # Base class for multipart exceptions that do not subclass from
25
+ # other exception classes for backwards compatibility.
26
+ class BoundaryTooLongError < StandardError
27
+ include BadRequest
28
+ end
12
29
 
30
+ # Prefer to use the BoundaryTooLongError class or Rack::BadRequest.
31
+ Error = BoundaryTooLongError
32
+
33
+ EOL = "\r\n"
34
+ MULTIPART = %r|\Amultipart/.*boundary=\"?([^\";,]+)\"?|ni
35
+ MULTIPART_CONTENT_TYPE = /Content-Type: (.*)#{EOL}/ni
36
+ MULTIPART_CONTENT_DISPOSITION = /Content-Disposition:(.*)(?=#{EOL}(\S|\z))/ni
37
+ MULTIPART_CONTENT_ID = /Content-ID:\s*([^#{EOL}]*)/ni
38
+
39
+ class Parser
13
40
  BUFSIZE = 1_048_576
14
41
  TEXT_PLAIN = "text/plain"
15
42
  TEMPFILE_FACTORY = lambda { |filename, content_type|
16
- Tempfile.new(["RackMultipart", ::File.extname(filename.gsub("\0", '%00'))])
17
- }
43
+ extension = ::File.extname(filename.gsub("\0", '%00'))[0, 129]
18
44
 
19
- BOUNDARY_REGEX = /\A([^\n]*(?:\n|\Z))/
45
+ Tempfile.new(["RackMultipart", extension])
46
+ }
20
47
 
21
48
  class BoundedIO # :nodoc:
22
49
  def initialize(io, content_length)
@@ -39,16 +66,12 @@ module Rack
39
66
  if str
40
67
  @cursor += str.bytesize
41
68
  else
42
- # Raise an error for mismatching Content-Length and actual contents
69
+ # Raise an error for mismatching content-length and actual contents
43
70
  raise EOFError, "bad content body"
44
71
  end
45
72
 
46
73
  str
47
74
  end
48
-
49
- def rewind
50
- @io.rewind
51
- end
52
75
  end
53
76
 
54
77
  MultipartInfo = Struct.new :params, :tmp_files
@@ -67,18 +90,17 @@ module Rack
67
90
  boundary = parse_boundary content_type
68
91
  return EMPTY unless boundary
69
92
 
93
+ if boundary.length > 70
94
+ # RFC 1521 Section 7.2.1 imposes a 70 character maximum for the boundary.
95
+ # Most clients use no more than 55 characters.
96
+ raise BoundaryTooLongError, "multipart boundary size too large (#{boundary.length} characters)"
97
+ end
98
+
70
99
  io = BoundedIO.new(io, content_length) if content_length
71
- outbuf = String.new
72
100
 
73
101
  parser = new(boundary, tmpfile, bufsize, qp)
74
- parser.on_read io.read(bufsize, outbuf)
75
-
76
- loop do
77
- break if parser.state == :DONE
78
- parser.on_read io.read(bufsize, outbuf)
79
- end
102
+ parser.parse(io)
80
103
 
81
- io.rewind
82
104
  parser.result
83
105
  end
84
106
 
@@ -178,32 +200,48 @@ module Rack
178
200
  def initialize(boundary, tempfile, bufsize, query_parser)
179
201
  @query_parser = query_parser
180
202
  @params = query_parser.make_params
181
- @boundary = "--#{boundary}"
182
203
  @bufsize = bufsize
183
204
 
184
- @full_boundary = @boundary
185
- @end_boundary = @boundary + '--'
186
205
  @state = :FAST_FORWARD
187
206
  @mime_index = 0
188
207
  @collector = Collector.new tempfile
189
208
 
190
209
  @sbuf = StringScanner.new("".dup)
191
- @body_regex = /(?:#{EOL})?#{Regexp.quote(@boundary)}(?:#{EOL}|--)/m
192
- @rx_max_size = EOL.size + @boundary.bytesize + [EOL.size, '--'.size].max
210
+ @body_regex = /(?:#{EOL}|\A)--#{Regexp.quote(boundary)}(?:#{EOL}|--)/m
211
+ @body_regex_at_end = /#{@body_regex}\z/m
212
+ @end_boundary_size = boundary.bytesize + 4 # (-- at start, -- at finish)
213
+ @rx_max_size = boundary.bytesize + 6 # (\r\n-- at start, either \r\n or -- at finish)
193
214
  @head_regex = /(.*?#{EOL})#{EOL}/m
194
215
  end
195
216
 
196
- def on_read(content)
197
- handle_empty_content!(content)
198
- @sbuf.concat content
199
- run_parser
217
+ def parse(io)
218
+ outbuf = String.new
219
+ read_data(io, outbuf)
220
+
221
+ loop do
222
+ status =
223
+ case @state
224
+ when :FAST_FORWARD
225
+ handle_fast_forward
226
+ when :CONSUME_TOKEN
227
+ handle_consume_token
228
+ when :MIME_HEAD
229
+ handle_mime_head
230
+ when :MIME_BODY
231
+ handle_mime_body
232
+ else # when :DONE
233
+ return
234
+ end
235
+
236
+ read_data(io, outbuf) if status == :want_read
237
+ end
200
238
  end
201
239
 
202
240
  def result
203
241
  @collector.each do |part|
204
242
  part.get_data do |data|
205
243
  tag_multipart_encoding(part.filename, part.content_type, part.name, data)
206
- @query_parser.normalize_params(@params, part.name, data, @query_parser.param_depth_limit)
244
+ @query_parser.normalize_params(@params, part.name, data)
207
245
  end
208
246
  end
209
247
  MultipartInfo.new @params.to_params_hash, @collector.find_all(&:file?).map(&:body)
@@ -211,29 +249,45 @@ module Rack
211
249
 
212
250
  private
213
251
 
214
- def run_parser
215
- loop do
216
- case @state
217
- when :FAST_FORWARD
218
- break if handle_fast_forward == :want_read
219
- when :CONSUME_TOKEN
220
- break if handle_consume_token == :want_read
221
- when :MIME_HEAD
222
- break if handle_mime_head == :want_read
223
- when :MIME_BODY
224
- break if handle_mime_body == :want_read
225
- when :DONE
226
- break
227
- end
228
- end
252
+ def dequote(str) # From WEBrick::HTTPUtils
253
+ ret = (/\A"(.*)"\Z/ =~ str) ? $1 : str.dup
254
+ ret.gsub!(/\\(.)/, "\\1")
255
+ ret
229
256
  end
230
257
 
258
+ def read_data(io, outbuf)
259
+ content = io.read(@bufsize, outbuf)
260
+ handle_empty_content!(content)
261
+ @sbuf.concat(content)
262
+ end
263
+
264
+ # This handles the initial parser state. We read until we find the starting
265
+ # boundary, then we can transition to the next state. If we find the ending
266
+ # boundary, this is an invalid multipart upload, but keep scanning for opening
267
+ # boundary in that case. If no boundary found, we need to keep reading data
268
+ # and retry. It's highly unlikely the initial read will not consume the
269
+ # boundary. The client would have to deliberately craft a response
270
+ # with the opening boundary beyond the buffer size for that to happen.
231
271
  def handle_fast_forward
232
- if consume_boundary
233
- @state = :MIME_HEAD
234
- else
235
- raise EOFError, "bad content body" if @sbuf.rest_size >= @bufsize
236
- :want_read
272
+ while true
273
+ case consume_boundary
274
+ when :BOUNDARY
275
+ # found opening boundary, transition to next state
276
+ @state = :MIME_HEAD
277
+ return
278
+ when :END_BOUNDARY
279
+ # invalid multipart upload
280
+ if @sbuf.pos == @end_boundary_size && @sbuf.rest == EOL
281
+ # stop parsing a buffer if a buffer is only an end boundary.
282
+ @state = :DONE
283
+ return
284
+ end
285
+
286
+ # retry for opening boundary
287
+ else
288
+ # no boundary found, keep reading data
289
+ return :want_read
290
+ end
237
291
  end
238
292
  end
239
293
 
@@ -247,17 +301,102 @@ module Rack
247
301
  end
248
302
  end
249
303
 
304
+ CONTENT_DISPOSITION_MAX_PARAMS = 16
305
+ CONTENT_DISPOSITION_MAX_BYTES = 1536
250
306
  def handle_mime_head
251
307
  if @sbuf.scan_until(@head_regex)
252
308
  head = @sbuf[1]
253
309
  content_type = head[MULTIPART_CONTENT_TYPE, 1]
254
- if name = head[MULTIPART_CONTENT_DISPOSITION, 1]
255
- name = Rack::Auth::Digest::Params::dequote(name)
310
+ if (disposition = head[MULTIPART_CONTENT_DISPOSITION, 1]) &&
311
+ disposition.bytesize <= CONTENT_DISPOSITION_MAX_BYTES
312
+
313
+ # ignore actual content-disposition value (should always be form-data)
314
+ i = disposition.index(';')
315
+ disposition.slice!(0, i+1)
316
+ param = nil
317
+ num_params = 0
318
+
319
+ # Parse parameter list
320
+ while i = disposition.index('=')
321
+ # Only parse up to max parameters, to avoid potential denial of service
322
+ num_params += 1
323
+ break if num_params > CONTENT_DISPOSITION_MAX_PARAMS
324
+
325
+ # Found end of parameter name, ensure forward progress in loop
326
+ param = disposition.slice!(0, i+1)
327
+
328
+ # Remove ending equals and preceding whitespace from parameter name
329
+ param.chomp!('=')
330
+ param.lstrip!
331
+
332
+ if disposition[0] == '"'
333
+ # Parameter value is quoted, parse it, handling backslash escapes
334
+ disposition.slice!(0, 1)
335
+ value = String.new
336
+
337
+ while i = disposition.index(/(["\\])/)
338
+ c = $1
339
+
340
+ # Append all content until ending quote or escape
341
+ value << disposition.slice!(0, i)
342
+
343
+ # Remove either backslash or ending quote,
344
+ # ensures forward progress in loop
345
+ disposition.slice!(0, 1)
346
+
347
+ # stop parsing parameter value if found ending quote
348
+ break if c == '"'
349
+
350
+ escaped_char = disposition.slice!(0, 1)
351
+ if param == 'filename' && escaped_char != '"'
352
+ # Possible IE uploaded filename, append both escape backslash and value
353
+ value << c << escaped_char
354
+ else
355
+ # Other only append escaped value
356
+ value << escaped_char
357
+ end
358
+ end
359
+ else
360
+ if i = disposition.index(';')
361
+ # Parameter value unquoted (which may be invalid), value ends at semicolon
362
+ value = disposition.slice!(0, i)
363
+ else
364
+ # If no ending semicolon, assume remainder of line is value and stop
365
+ # parsing
366
+ disposition.strip!
367
+ value = disposition
368
+ disposition = ''
369
+ end
370
+ end
371
+
372
+ case param
373
+ when 'name'
374
+ name = value
375
+ when 'filename'
376
+ filename = value
377
+ when 'filename*'
378
+ filename_star = value
379
+ # else
380
+ # ignore other parameters
381
+ end
382
+
383
+ # skip trailing semicolon, to proceed to next parameter
384
+ if i = disposition.index(';')
385
+ disposition.slice!(0, i+1)
386
+ end
387
+ end
256
388
  else
257
389
  name = head[MULTIPART_CONTENT_ID, 1]
258
390
  end
259
391
 
260
- filename = get_filename(head)
392
+ if filename_star
393
+ encoding, _, filename = filename_star.split("'", 3)
394
+ filename = normalize_filename(filename || '')
395
+ filename.force_encoding(find_encoding(encoding))
396
+ elsif filename
397
+ filename = $1 if filename =~ /^"(.*)"$/
398
+ filename = normalize_filename(filename)
399
+ end
261
400
 
262
401
  if name.nil? || name.empty?
263
402
  name = filename || "#{content_type || TEXT_PLAIN}[]".dup
@@ -272,7 +411,7 @@ module Rack
272
411
 
273
412
  def handle_mime_body
274
413
  if (body_with_boundary = @sbuf.check_until(@body_regex)) # check but do not advance the pointer yet
275
- body = body_with_boundary.sub(/#{@body_regex}\z/m, '') # remove the boundary from the string
414
+ body = body_with_boundary.sub(@body_regex_at_end, '') # remove the boundary from the string
276
415
  @collector.on_mime_body @mime_index, body
277
416
  @sbuf.pos += body.length + 2 # skip \r\n after the content
278
417
  @state = :CONSUME_TOKEN
@@ -289,54 +428,31 @@ module Rack
289
428
  end
290
429
  end
291
430
 
292
- def full_boundary; @full_boundary; end
293
-
431
+ # Scan until the we find the start or end of the boundary.
432
+ # If we find it, return the appropriate symbol for the start or
433
+ # end of the boundary. If we don't find the start or end of the
434
+ # boundary, clear the buffer and return nil.
294
435
  def consume_boundary
295
- while read_buffer = @sbuf.scan_until(BOUNDARY_REGEX)
296
- case read_buffer.strip
297
- when full_boundary then return :BOUNDARY
298
- when @end_boundary then return :END_BOUNDARY
299
- end
300
- return if @sbuf.eos?
436
+ if read_buffer = @sbuf.scan_until(@body_regex)
437
+ read_buffer.end_with?(EOL) ? :BOUNDARY : :END_BOUNDARY
438
+ else
439
+ @sbuf.terminate
440
+ nil
301
441
  end
302
442
  end
303
443
 
304
- def get_filename(head)
305
- filename = nil
306
- case head
307
- when RFC2183
308
- params = Hash[*head.scan(DISPPARM).flat_map(&:compact)]
309
-
310
- if filename = params['filename']
311
- filename = $1 if filename =~ /^"(.*)"$/
312
- elsif filename = params['filename*']
313
- encoding, _, filename = filename.split("'", 3)
314
- end
315
- when BROKEN
316
- filename = $1
317
- filename = $1 if filename =~ /^"(.*)"$/
318
- end
319
-
320
- return unless filename
321
-
444
+ def normalize_filename(filename)
322
445
  if filename.scan(/%.?.?/).all? { |s| /%[0-9a-fA-F]{2}/.match?(s) }
323
446
  filename = Utils.unescape_path(filename)
324
447
  end
325
448
 
326
449
  filename.scrub!
327
450
 
328
- if filename !~ /\\[^\\"]/
329
- filename = filename.gsub(/\\(.)/, '\1')
330
- end
331
-
332
- if encoding
333
- filename.force_encoding ::Encoding.find(encoding)
334
- end
335
-
336
- filename
451
+ filename.split(/[\/\\]/).last || String.new
337
452
  end
338
453
 
339
454
  CHARSET = "charset"
455
+ deprecate_constant :CHARSET
340
456
 
341
457
  def tag_multipart_encoding(filename, content_type, name, body)
342
458
  name = name.to_s
@@ -357,7 +473,9 @@ module Rack
357
473
  k.strip!
358
474
  v.strip!
359
475
  v = v[1..-2] if v.start_with?('"') && v.end_with?('"')
360
- encoding = Encoding.find v if k == CHARSET
476
+ if k == "charset"
477
+ encoding = find_encoding(v)
478
+ end
361
479
  end
362
480
  end
363
481
  end
@@ -366,9 +484,18 @@ module Rack
366
484
  body.force_encoding(encoding)
367
485
  end
368
486
 
487
+ # Return the related Encoding object. However, because
488
+ # enc is submitted by the user, it may be invalid, so
489
+ # use a binary encoding in that case.
490
+ def find_encoding(enc)
491
+ Encoding.find enc
492
+ rescue ArgumentError
493
+ Encoding::BINARY
494
+ end
495
+
369
496
  def handle_empty_content!(content)
370
497
  if content.nil? || content.empty?
371
- raise EOFError
498
+ raise EmptyContentError
372
499
  end
373
500
  end
374
501
  end
@@ -1,8 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'tempfile'
4
+ require 'fileutils'
5
+
3
6
  module Rack
4
7
  module Multipart
5
8
  class UploadedFile
9
+
6
10
  # The filename, *not* including the path, of the "uploaded" file
7
11
  attr_reader :original_filename
8
12