rack 2.2.8 → 3.1.10

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 (87) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +353 -81
  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 -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 +25 -19
  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 +840 -644
  27. data/lib/rack/lock.rb +2 -5
  28. data/lib/rack/logger.rb +3 -0
  29. data/lib/rack/media_type.rb +17 -7
  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 +161 -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 +214 -90
  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 +260 -123
  44. data/lib/rack/response.rb +151 -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 +240 -237
  54. data/lib/rack/version.rb +1 -9
  55. data/lib/rack.rb +13 -89
  56. metadata +15 -44
  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 -204
  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,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,32 +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
- @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)
195
214
  @head_regex = /(.*?#{EOL})#{EOL}/m
196
215
  end
197
216
 
198
- def on_read(content)
199
- handle_empty_content!(content)
200
- @sbuf.concat content
201
- 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
202
238
  end
203
239
 
204
240
  def result
205
241
  @collector.each do |part|
206
242
  part.get_data do |data|
207
243
  tag_multipart_encoding(part.filename, part.content_type, part.name, data)
208
- @query_parser.normalize_params(@params, part.name, data, @query_parser.param_depth_limit)
244
+ @query_parser.normalize_params(@params, part.name, data)
209
245
  end
210
246
  end
211
247
  MultipartInfo.new @params.to_params_hash, @collector.find_all(&:file?).map(&:body)
@@ -213,29 +249,45 @@ module Rack
213
249
 
214
250
  private
215
251
 
216
- def run_parser
217
- loop do
218
- case @state
219
- when :FAST_FORWARD
220
- break if handle_fast_forward == :want_read
221
- when :CONSUME_TOKEN
222
- break if handle_consume_token == :want_read
223
- when :MIME_HEAD
224
- break if handle_mime_head == :want_read
225
- when :MIME_BODY
226
- break if handle_mime_body == :want_read
227
- when :DONE
228
- break
229
- end
230
- end
252
+ def dequote(str) # From WEBrick::HTTPUtils
253
+ ret = (/\A"(.*)"\Z/ =~ str) ? $1 : str.dup
254
+ ret.gsub!(/\\(.)/, "\\1")
255
+ ret
256
+ end
257
+
258
+ def read_data(io, outbuf)
259
+ content = io.read(@bufsize, outbuf)
260
+ handle_empty_content!(content)
261
+ @sbuf.concat(content)
231
262
  end
232
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.
233
271
  def handle_fast_forward
234
- if consume_boundary
235
- @state = :MIME_HEAD
236
- else
237
- raise EOFError, "bad content body" if @sbuf.rest_size >= @bufsize
238
- :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
239
291
  end
240
292
  end
241
293
 
@@ -249,17 +301,101 @@ module Rack
249
301
  end
250
302
  end
251
303
 
304
+ CONTENT_DISPOSITION_MAX_PARAMS = 16
305
+ CONTENT_DISPOSITION_MAX_BYTES = 1536
252
306
  def handle_mime_head
253
307
  if @sbuf.scan_until(@head_regex)
254
308
  head = @sbuf[1]
255
309
  content_type = head[MULTIPART_CONTENT_TYPE, 1]
256
- if name = head[MULTIPART_CONTENT_DISPOSITION, 1]
257
- 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
258
388
  else
259
389
  name = head[MULTIPART_CONTENT_ID, 1]
260
390
  end
261
391
 
262
- 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 = normalize_filename(filename)
398
+ end
263
399
 
264
400
  if name.nil? || name.empty?
265
401
  name = filename || "#{content_type || TEXT_PLAIN}[]".dup
@@ -274,7 +410,7 @@ module Rack
274
410
 
275
411
  def handle_mime_body
276
412
  if (body_with_boundary = @sbuf.check_until(@body_regex)) # check but do not advance the pointer yet
277
- body = body_with_boundary.sub(/#{@body_regex}\z/m, '') # remove the boundary from the string
413
+ body = body_with_boundary.sub(@body_regex_at_end, '') # remove the boundary from the string
278
414
  @collector.on_mime_body @mime_index, body
279
415
  @sbuf.pos += body.length + 2 # skip \r\n after the content
280
416
  @state = :CONSUME_TOKEN
@@ -291,54 +427,31 @@ module Rack
291
427
  end
292
428
  end
293
429
 
294
- def full_boundary; @full_boundary; end
295
-
430
+ # Scan until the we find the start or end of the boundary.
431
+ # If we find it, return the appropriate symbol for the start or
432
+ # end of the boundary. If we don't find the start or end of the
433
+ # boundary, clear the buffer and return nil.
296
434
  def consume_boundary
297
- while read_buffer = @sbuf.scan_until(BOUNDARY_REGEX)
298
- case read_buffer.strip
299
- when full_boundary then return :BOUNDARY
300
- when @end_boundary then return :END_BOUNDARY
301
- end
302
- return if @sbuf.eos?
435
+ if read_buffer = @sbuf.scan_until(@body_regex)
436
+ read_buffer.end_with?(EOL) ? :BOUNDARY : :END_BOUNDARY
437
+ else
438
+ @sbuf.terminate
439
+ nil
303
440
  end
304
441
  end
305
442
 
306
- def get_filename(head)
307
- filename = nil
308
- case head
309
- when RFC2183
310
- params = Hash[*head.scan(DISPPARM).flat_map(&:compact)]
311
-
312
- if filename = params['filename']
313
- filename = $1 if filename =~ /^"(.*)"$/
314
- elsif filename = params['filename*']
315
- encoding, _, filename = filename.split("'", 3)
316
- end
317
- when BROKEN
318
- filename = $1
319
- filename = $1 if filename =~ /^"(.*)"$/
320
- end
321
-
322
- return unless filename
323
-
443
+ def normalize_filename(filename)
324
444
  if filename.scan(/%.?.?/).all? { |s| /%[0-9a-fA-F]{2}/.match?(s) }
325
445
  filename = Utils.unescape_path(filename)
326
446
  end
327
447
 
328
448
  filename.scrub!
329
449
 
330
- if filename !~ /\\[^\\"]/
331
- filename = filename.gsub(/\\(.)/, '\1')
332
- end
333
-
334
- if encoding
335
- filename.force_encoding ::Encoding.find(encoding)
336
- end
337
-
338
- filename
450
+ filename.split(/[\/\\]/).last || String.new
339
451
  end
340
452
 
341
453
  CHARSET = "charset"
454
+ deprecate_constant :CHARSET
342
455
 
343
456
  def tag_multipart_encoding(filename, content_type, name, body)
344
457
  name = name.to_s
@@ -359,7 +472,9 @@ module Rack
359
472
  k.strip!
360
473
  v.strip!
361
474
  v = v[1..-2] if v.start_with?('"') && v.end_with?('"')
362
- encoding = Encoding.find v if k == CHARSET
475
+ if k == "charset"
476
+ encoding = find_encoding(v)
477
+ end
363
478
  end
364
479
  end
365
480
  end
@@ -368,9 +483,18 @@ module Rack
368
483
  body.force_encoding(encoding)
369
484
  end
370
485
 
486
+ # Return the related Encoding object. However, because
487
+ # enc is submitted by the user, it may be invalid, so
488
+ # use a binary encoding in that case.
489
+ def find_encoding(enc)
490
+ Encoding.find enc
491
+ rescue ArgumentError
492
+ Encoding::BINARY
493
+ end
494
+
371
495
  def handle_empty_content!(content)
372
496
  if content.nil? || content.empty?
373
- raise EOFError
497
+ raise EmptyContentError
374
498
  end
375
499
  end
376
500
  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