rack 2.2.9 → 3.1.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (86) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +330 -88
  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 +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 +840 -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 +161 -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 +213 -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 +260 -123
  43. data/lib/rack/response.rb +151 -66
  44. data/lib/rack/rewindable_input.rb +24 -5
  45. data/lib/rack/runtime.rb +7 -6
  46. data/lib/rack/sendfile.rb +30 -25
  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 +232 -233
  53. data/lib/rack/version.rb +1 -9
  54. data/lib/rack.rb +13 -89
  55. metadata +15 -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 -54
  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 -204
  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
+ 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,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
256
+ end
257
+
258
+ def read_data(io, outbuf)
259
+ content = io.read(@bufsize, outbuf)
260
+ handle_empty_content!(content)
261
+ @sbuf.concat(content)
232
262
  end
233
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,101 @@ 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 = normalize_filename(filename)
398
+ end
269
399
 
270
400
  if name.nil? || name.empty?
271
401
  name = filename || "#{content_type || TEXT_PLAIN}[]".dup
@@ -280,7 +410,7 @@ module Rack
280
410
 
281
411
  def handle_mime_body
282
412
  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
413
+ body = body_with_boundary.sub(@body_regex_at_end, '') # remove the boundary from the string
284
414
  @collector.on_mime_body @mime_index, body
285
415
  @sbuf.pos += body.length + 2 # skip \r\n after the content
286
416
  @state = :CONSUME_TOKEN
@@ -297,54 +427,31 @@ module Rack
297
427
  end
298
428
  end
299
429
 
300
- def full_boundary; @full_boundary; end
301
-
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.
302
434
  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?
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
309
440
  end
310
441
  end
311
442
 
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
-
443
+ def normalize_filename(filename)
330
444
  if filename.scan(/%.?.?/).all? { |s| /%[0-9a-fA-F]{2}/.match?(s) }
331
445
  filename = Utils.unescape_path(filename)
332
446
  end
333
447
 
334
448
  filename.scrub!
335
449
 
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
450
+ filename.split(/[\/\\]/).last || String.new
345
451
  end
346
452
 
347
453
  CHARSET = "charset"
454
+ deprecate_constant :CHARSET
348
455
 
349
456
  def tag_multipart_encoding(filename, content_type, name, body)
350
457
  name = name.to_s
@@ -365,7 +472,9 @@ module Rack
365
472
  k.strip!
366
473
  v.strip!
367
474
  v = v[1..-2] if v.start_with?('"') && v.end_with?('"')
368
- encoding = Encoding.find v if k == CHARSET
475
+ if k == "charset"
476
+ encoding = find_encoding(v)
477
+ end
369
478
  end
370
479
  end
371
480
  end
@@ -374,9 +483,18 @@ module Rack
374
483
  body.force_encoding(encoding)
375
484
  end
376
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
+
377
495
  def handle_empty_content!(content)
378
496
  if content.nil? || content.empty?
379
- raise EOFError
497
+ raise EmptyContentError
380
498
  end
381
499
  end
382
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