rack 2.2.12 → 3.1.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 (86) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +259 -94
  3. data/CONTRIBUTING.md +63 -55
  4. data/MIT-LICENSE +1 -1
  5. data/README.md +328 -0
  6. data/SPEC.rdoc +204 -131
  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 -3
  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 +24 -20
  15. data/lib/rack/conditional_get.rb +18 -15
  16. data/lib/rack/constants.rb +66 -0
  17. data/lib/rack/content_length.rb +11 -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 +838 -644
  27. data/lib/rack/lock.rb +2 -5
  28. data/lib/rack/logger.rb +3 -0
  29. data/lib/rack/method_override.rb +5 -1
  30. data/lib/rack/mime.rb +14 -5
  31. data/lib/rack/mock.rb +1 -271
  32. data/lib/rack/mock_request.rb +171 -0
  33. data/lib/rack/mock_response.rb +124 -0
  34. data/lib/rack/multipart/generator.rb +7 -5
  35. data/lib/rack/multipart/parser.rb +214 -95
  36. data/lib/rack/multipart/uploaded_file.rb +4 -0
  37. data/lib/rack/multipart.rb +53 -40
  38. data/lib/rack/null_logger.rb +9 -0
  39. data/lib/rack/query_parser.rb +81 -102
  40. data/lib/rack/recursive.rb +2 -0
  41. data/lib/rack/reloader.rb +0 -2
  42. data/lib/rack/request.rb +248 -123
  43. data/lib/rack/response.rb +145 -70
  44. data/lib/rack/rewindable_input.rb +24 -5
  45. data/lib/rack/runtime.rb +7 -6
  46. data/lib/rack/sendfile.rb +31 -26
  47. data/lib/rack/show_exceptions.rb +21 -4
  48. data/lib/rack/show_status.rb +17 -7
  49. data/lib/rack/static.rb +8 -8
  50. data/lib/rack/tempfile_reaper.rb +15 -4
  51. data/lib/rack/urlmap.rb +3 -1
  52. data/lib/rack/utils.rb +224 -237
  53. data/lib/rack/version.rb +1 -9
  54. data/lib/rack.rb +13 -89
  55. metadata +18 -41
  56. data/README.rdoc +0 -320
  57. data/Rakefile +0 -130
  58. data/bin/rackup +0 -5
  59. data/contrib/rack.png +0 -0
  60. data/contrib/rack.svg +0 -150
  61. data/contrib/rack_logo.svg +0 -164
  62. data/contrib/rdoc.css +0 -412
  63. data/example/lobster.ru +0 -6
  64. data/example/protectedlobster.rb +0 -16
  65. data/example/protectedlobster.ru +0 -10
  66. data/lib/rack/auth/digest/md5.rb +0 -131
  67. data/lib/rack/auth/digest/nonce.rb +0 -53
  68. data/lib/rack/auth/digest/params.rb +0 -54
  69. data/lib/rack/auth/digest/request.rb +0 -43
  70. data/lib/rack/chunked.rb +0 -117
  71. data/lib/rack/core_ext/regexp.rb +0 -14
  72. data/lib/rack/file.rb +0 -7
  73. data/lib/rack/handler/cgi.rb +0 -59
  74. data/lib/rack/handler/fastcgi.rb +0 -100
  75. data/lib/rack/handler/lsws.rb +0 -61
  76. data/lib/rack/handler/scgi.rb +0 -71
  77. data/lib/rack/handler/thin.rb +0 -36
  78. data/lib/rack/handler/webrick.rb +0 -129
  79. data/lib/rack/handler.rb +0 -104
  80. data/lib/rack/lobster.rb +0 -70
  81. data/lib/rack/server.rb +0 -466
  82. data/lib/rack/session/abstract/id.rb +0 -523
  83. data/lib/rack/session/cookie.rb +0 -203
  84. data/lib/rack/session/memcache.rb +0 -10
  85. data/lib/rack/session/pool.rb +0 -85
  86. 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
+ if headers.has_key? 'set-cookie'
82
+ set_cookie_header = headers.fetch('set-cookie')
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,14 +2,41 @@
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|
@@ -18,8 +45,6 @@ module Rack
18
45
  Tempfile.new(["RackMultipart", extension])
19
46
  }
20
47
 
21
- BOUNDARY_REGEX = /\A([^\n]*(?:\n|\Z))/
22
-
23
48
  class BoundedIO # :nodoc:
24
49
  def initialize(io, content_length)
25
50
  @io = io
@@ -41,16 +66,12 @@ module Rack
41
66
  if str
42
67
  @cursor += str.bytesize
43
68
  else
44
- # Raise an error for mismatching Content-Length and actual contents
69
+ # Raise an error for mismatching content-length and actual contents
45
70
  raise EOFError, "bad content body"
46
71
  end
47
72
 
48
73
  str
49
74
  end
50
-
51
- def rewind
52
- @io.rewind
53
- end
54
75
  end
55
76
 
56
77
  MultipartInfo = Struct.new :params, :tmp_files
@@ -69,18 +90,17 @@ module Rack
69
90
  boundary = parse_boundary content_type
70
91
  return EMPTY unless boundary
71
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
+
72
99
  io = BoundedIO.new(io, content_length) if content_length
73
- outbuf = String.new
74
100
 
75
101
  parser = new(boundary, tmpfile, bufsize, qp)
76
- parser.on_read io.read(bufsize, outbuf)
77
-
78
- loop do
79
- break if parser.state == :DONE
80
- parser.on_read io.read(bufsize, outbuf)
81
- end
102
+ parser.parse(io)
82
103
 
83
- io.rewind
84
104
  parser.result
85
105
  end
86
106
 
@@ -180,33 +200,48 @@ module Rack
180
200
  def initialize(boundary, tempfile, bufsize, query_parser)
181
201
  @query_parser = query_parser
182
202
  @params = query_parser.make_params
183
- @boundary = "--#{boundary}"
184
203
  @bufsize = bufsize
185
204
 
186
- @full_boundary = @boundary
187
- @end_boundary = @boundary + '--'
188
205
  @state = :FAST_FORWARD
189
206
  @mime_index = 0
190
207
  @collector = Collector.new tempfile
191
208
 
192
209
  @sbuf = StringScanner.new("".dup)
193
- @body_regex = /(?:#{EOL})?#{Regexp.quote(@boundary)}(?:#{EOL}|--)/m
194
- @end_boundary_size = boundary.bytesize + 6 # (-- at start, -- at finish, EOL at end)
195
- @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)
196
214
  @head_regex = /(.*?#{EOL})#{EOL}/m
197
215
  end
198
216
 
199
- def on_read(content)
200
- handle_empty_content!(content)
201
- @sbuf.concat content
202
- 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
203
238
  end
204
239
 
205
240
  def result
206
241
  @collector.each do |part|
207
242
  part.get_data do |data|
208
243
  tag_multipart_encoding(part.filename, part.content_type, part.name, data)
209
- @query_parser.normalize_params(@params, part.name, data, @query_parser.param_depth_limit)
244
+ @query_parser.normalize_params(@params, part.name, data)
210
245
  end
211
246
  end
212
247
  MultipartInfo.new @params.to_params_hash, @collector.find_all(&:file?).map(&:body)
@@ -214,34 +249,45 @@ module Rack
214
249
 
215
250
  private
216
251
 
217
- def run_parser
218
- loop do
219
- case @state
220
- when :FAST_FORWARD
221
- break if handle_fast_forward == :want_read
222
- when :CONSUME_TOKEN
223
- break if handle_consume_token == :want_read
224
- when :MIME_HEAD
225
- break if handle_mime_head == :want_read
226
- when :MIME_BODY
227
- break if handle_mime_body == :want_read
228
- when :DONE
229
- break
230
- end
231
- end
252
+ def dequote(str) # From WEBrick::HTTPUtils
253
+ ret = (/\A"(.*)"\Z/ =~ str) ? $1 : str.dup
254
+ ret.gsub!(/\\(.)/, "\\1")
255
+ ret
232
256
  end
233
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.
234
271
  def handle_fast_forward
235
- tok = consume_boundary
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
236
285
 
237
- if tok == :END_BOUNDARY && @sbuf.pos == @end_boundary_size && @sbuf.eos?
238
- # stop parsing a buffer if a buffer is only an end boundary.
239
- @state = :DONE
240
- elsif tok
241
- @state = :MIME_HEAD
242
- else
243
- raise EOFError, "bad content body" if @sbuf.rest_size >= @bufsize
244
- :want_read
286
+ # retry for opening boundary
287
+ else
288
+ # no boundary found, keep reading data
289
+ return :want_read
290
+ end
245
291
  end
246
292
  end
247
293
 
@@ -255,17 +301,102 @@ module Rack
255
301
  end
256
302
  end
257
303
 
304
+ CONTENT_DISPOSITION_MAX_PARAMS = 16
305
+ CONTENT_DISPOSITION_MAX_BYTES = 1536
258
306
  def handle_mime_head
259
307
  if @sbuf.scan_until(@head_regex)
260
308
  head = @sbuf[1]
261
309
  content_type = head[MULTIPART_CONTENT_TYPE, 1]
262
- if name = head[MULTIPART_CONTENT_DISPOSITION, 1]
263
- 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
264
388
  else
265
389
  name = head[MULTIPART_CONTENT_ID, 1]
266
390
  end
267
391
 
268
- 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
269
400
 
270
401
  if name.nil? || name.empty?
271
402
  name = filename || "#{content_type || TEXT_PLAIN}[]".dup
@@ -280,7 +411,7 @@ module Rack
280
411
 
281
412
  def handle_mime_body
282
413
  if (body_with_boundary = @sbuf.check_until(@body_regex)) # check but do not advance the pointer yet
283
- 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
284
415
  @collector.on_mime_body @mime_index, body
285
416
  @sbuf.pos += body.length + 2 # skip \r\n after the content
286
417
  @state = :CONSUME_TOKEN
@@ -297,54 +428,31 @@ module Rack
297
428
  end
298
429
  end
299
430
 
300
- def full_boundary; @full_boundary; end
301
-
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.
302
435
  def consume_boundary
303
- while read_buffer = @sbuf.scan_until(BOUNDARY_REGEX)
304
- case read_buffer.strip
305
- when full_boundary then return :BOUNDARY
306
- when @end_boundary then return :END_BOUNDARY
307
- end
308
- 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
309
441
  end
310
442
  end
311
443
 
312
- def get_filename(head)
313
- filename = nil
314
- case head
315
- when RFC2183
316
- params = Hash[*head.scan(DISPPARM).flat_map(&:compact)]
317
-
318
- if filename = params['filename']
319
- filename = $1 if filename =~ /^"(.*)"$/
320
- elsif filename = params['filename*']
321
- encoding, _, filename = filename.split("'", 3)
322
- end
323
- when BROKEN
324
- filename = $1
325
- filename = $1 if filename =~ /^"(.*)"$/
326
- end
327
-
328
- return unless filename
329
-
444
+ def normalize_filename(filename)
330
445
  if filename.scan(/%.?.?/).all? { |s| /%[0-9a-fA-F]{2}/.match?(s) }
331
446
  filename = Utils.unescape_path(filename)
332
447
  end
333
448
 
334
449
  filename.scrub!
335
450
 
336
- if filename !~ /\\[^\\"]/
337
- filename = filename.gsub(/\\(.)/, '\1')
338
- end
339
-
340
- if encoding
341
- filename.force_encoding ::Encoding.find(encoding)
342
- end
343
-
344
- filename
451
+ filename.split(/[\/\\]/).last || String.new
345
452
  end
346
453
 
347
454
  CHARSET = "charset"
455
+ deprecate_constant :CHARSET
348
456
 
349
457
  def tag_multipart_encoding(filename, content_type, name, body)
350
458
  name = name.to_s
@@ -365,7 +473,9 @@ module Rack
365
473
  k.strip!
366
474
  v.strip!
367
475
  v = v[1..-2] if v.start_with?('"') && v.end_with?('"')
368
- encoding = Encoding.find v if k == CHARSET
476
+ if k == "charset"
477
+ encoding = find_encoding(v)
478
+ end
369
479
  end
370
480
  end
371
481
  end
@@ -374,9 +484,18 @@ module Rack
374
484
  body.force_encoding(encoding)
375
485
  end
376
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
+
377
496
  def handle_empty_content!(content)
378
497
  if content.nil? || content.empty?
379
- raise EOFError
498
+ raise EmptyContentError
380
499
  end
381
500
  end
382
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