rack 2.2.20 → 3.2.4

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 +557 -70
  3. data/CONTRIBUTING.md +63 -55
  4. data/MIT-LICENSE +1 -1
  5. data/README.md +384 -0
  6. data/SPEC.rdoc +243 -277
  7. data/lib/rack/auth/abstract/handler.rb +3 -1
  8. data/lib/rack/auth/abstract/request.rb +5 -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 +108 -69
  13. data/lib/rack/cascade.rb +2 -3
  14. data/lib/rack/common_logger.rb +22 -17
  15. data/lib/rack/conditional_get.rb +20 -16
  16. data/lib/rack/constants.rb +68 -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 +17 -23
  22. data/lib/rack/events.rb +25 -6
  23. data/lib/rack/files.rb +15 -17
  24. data/lib/rack/head.rb +8 -8
  25. data/lib/rack/headers.rb +238 -0
  26. data/lib/rack/lint.rb +817 -648
  27. data/lib/rack/lock.rb +2 -5
  28. data/lib/rack/media_type.rb +6 -7
  29. data/lib/rack/method_override.rb +5 -1
  30. data/lib/rack/mime.rb +14 -5
  31. data/lib/rack/mock.rb +1 -300
  32. data/lib/rack/mock_request.rb +161 -0
  33. data/lib/rack/mock_response.rb +147 -0
  34. data/lib/rack/multipart/generator.rb +7 -5
  35. data/lib/rack/multipart/parser.rb +242 -102
  36. data/lib/rack/multipart/uploaded_file.rb +45 -4
  37. data/lib/rack/multipart.rb +53 -40
  38. data/lib/rack/null_logger.rb +9 -0
  39. data/lib/rack/query_parser.rb +116 -121
  40. data/lib/rack/recursive.rb +2 -0
  41. data/lib/rack/reloader.rb +0 -2
  42. data/lib/rack/request.rb +272 -144
  43. data/lib/rack/response.rb +151 -66
  44. data/lib/rack/rewindable_input.rb +27 -5
  45. data/lib/rack/runtime.rb +7 -6
  46. data/lib/rack/sendfile.rb +35 -30
  47. data/lib/rack/show_exceptions.rb +25 -6
  48. data/lib/rack/show_status.rb +17 -9
  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 +228 -238
  53. data/lib/rack/version.rb +3 -15
  54. data/lib/rack.rb +13 -90
  55. metadata +14 -40
  56. data/README.rdoc +0 -355
  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 -34
  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/logger.rb +0 -20
  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 -90
  87. data/rack.gemspec +0 -46
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'time'
4
+
5
+ require_relative 'response'
6
+
7
+ module Rack
8
+ # Rack::MockResponse provides useful helpers for testing your apps.
9
+ # Usually, you don't create the MockResponse on your own, but use
10
+ # MockRequest.
11
+
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
+
37
+ class << self
38
+ alias [] new
39
+ end
40
+
41
+ # Headers
42
+ attr_reader :original_headers, :cookies
43
+
44
+ # Errors
45
+ attr_accessor :errors
46
+
47
+ def initialize(status, headers, body, errors = nil)
48
+ @original_headers = headers
49
+
50
+ if errors
51
+ @errors = errors.string if errors.respond_to?(:string)
52
+ else
53
+ @errors = ""
54
+ end
55
+
56
+ super(body, status, headers)
57
+
58
+ @cookies = parse_cookies_from_header
59
+ buffered_body!
60
+ end
61
+
62
+ def =~(other)
63
+ body =~ other
64
+ end
65
+
66
+ def match(other)
67
+ body.match other
68
+ end
69
+
70
+ def body
71
+ return @buffered_body if defined?(@buffered_body)
72
+
73
+ # FIXME: apparently users of MockResponse expect the return value of
74
+ # MockResponse#body to be a string. However, the real response object
75
+ # returns the body as a list.
76
+ #
77
+ # See spec_showstatus.rb:
78
+ #
79
+ # should "not replace existing messages" do
80
+ # ...
81
+ # res.body.should == "foo!"
82
+ # end
83
+ buffer = @buffered_body = String.new
84
+
85
+ @body.each do |chunk|
86
+ buffer << chunk
87
+ end
88
+
89
+ return buffer
90
+ end
91
+
92
+ def empty?
93
+ [201, 204, 304].include? status
94
+ end
95
+
96
+ def cookie(name)
97
+ cookies.fetch(name, nil)
98
+ end
99
+
100
+ private
101
+
102
+ def parse_cookies_from_header
103
+ cookies = Hash.new
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)
118
+ end
119
+ end
120
+ cookies
121
+ end
122
+
123
+ def identify_cookie_attributes(cookie_filling)
124
+ cookie_bits = cookie_filling.split(';')
125
+ cookie_attributes = Hash.new
126
+ cookie_attributes.store('value', Array(cookie_bits[0].strip))
127
+ cookie_bits.drop(1).each do |bit|
128
+ if bit.include? '='
129
+ cookie_attribute, attribute_value = bit.split('=', 2)
130
+ cookie_attributes.store(cookie_attribute.strip.downcase, attribute_value.strip)
131
+ end
132
+ if bit.include? 'secure'
133
+ cookie_attributes.store('secure', true)
134
+ end
135
+ end
136
+
137
+ if cookie_attributes.key? 'max-age'
138
+ cookie_attributes.store('expires', Time.now + cookie_attributes['max-age'].to_i)
139
+ elsif cookie_attributes.key? 'expires'
140
+ cookie_attributes.store('expires', Time.httpdate(cookie_attributes['expires']))
141
+ end
142
+
143
+ cookie_attributes
144
+ end
145
+
146
+ end
147
+ 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,55 @@
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
+ FWS = /[ \t]+(?:\r\n[ \t]+)?/ # whitespace with optional folding
35
+ HEADER_VALUE = "(?:[^\r\n]|\r\n[ \t])*" # anything but a non-folding CRLF
36
+ MULTIPART = %r|\Amultipart/.*boundary=\"?([^\";,]+)\"?|ni
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
53
+ class Parser
13
54
  BUFSIZE = 1_048_576
14
55
  TEXT_PLAIN = "text/plain"
15
56
  TEMPFILE_FACTORY = lambda { |filename, content_type|
@@ -18,8 +59,6 @@ module Rack
18
59
  Tempfile.new(["RackMultipart", extension])
19
60
  }
20
61
 
21
- BOUNDARY_REGEX = /\A([^\n]*(?:\n|\Z))/
22
-
23
62
  BOUNDARY_START_LIMIT = 16 * 1024
24
63
  private_constant :BOUNDARY_START_LIMIT
25
64
 
@@ -62,16 +101,12 @@ module Rack
62
101
  if str
63
102
  @cursor += str.bytesize
64
103
  else
65
- # Raise an error for mismatching Content-Length and actual contents
104
+ # Raise an error for mismatching content-length and actual contents
66
105
  raise EOFError, "bad content body"
67
106
  end
68
107
 
69
108
  str
70
109
  end
71
-
72
- def rewind
73
- @io.rewind
74
- end
75
110
  end
76
111
 
77
112
  MultipartInfo = Struct.new :params, :tmp_files
@@ -90,18 +125,17 @@ module Rack
90
125
  boundary = parse_boundary content_type
91
126
  return EMPTY unless boundary
92
127
 
128
+ if boundary.length > 70
129
+ # RFC 1521 Section 7.2.1 imposes a 70 character maximum for the boundary.
130
+ # Most clients use no more than 55 characters.
131
+ raise BoundaryTooLongError, "multipart boundary size too large (#{boundary.length} characters)"
132
+ end
133
+
93
134
  io = BoundedIO.new(io, content_length) if content_length
94
- outbuf = String.new
95
135
 
96
136
  parser = new(boundary, tmpfile, bufsize, qp)
97
- parser.on_read io.read(bufsize, outbuf)
98
-
99
- loop do
100
- break if parser.state == :DONE
101
- parser.on_read io.read(bufsize, outbuf)
102
- end
137
+ parser.parse(io)
103
138
 
104
- io.rewind
105
139
  parser.result
106
140
  end
107
141
 
@@ -201,11 +235,8 @@ module Rack
201
235
  def initialize(boundary, tempfile, bufsize, query_parser)
202
236
  @query_parser = query_parser
203
237
  @params = query_parser.make_params
204
- @boundary = "--#{boundary}"
205
238
  @bufsize = bufsize
206
239
 
207
- @full_boundary = @boundary
208
- @end_boundary = @boundary + '--'
209
240
  @state = :FAST_FORWARD
210
241
  @mime_index = 0
211
242
  @body_retained = nil
@@ -213,23 +244,42 @@ module Rack
213
244
  @collector = Collector.new tempfile
214
245
 
215
246
  @sbuf = StringScanner.new("".dup)
216
- @body_regex = /(?:#{EOL})?#{Regexp.quote(@boundary)}(?:#{EOL}|--)/m
217
- @end_boundary_size = boundary.bytesize + 6 # (-- at start, -- at finish, EOL at end)
218
- @rx_max_size = EOL.size + @boundary.bytesize + [EOL.size, '--'.size].max
247
+ @body_regex = /(?:#{EOL}|\A)--#{Regexp.quote(boundary)}(?:#{EOL}|--)/m
248
+ @body_regex_at_end = /#{@body_regex}\z/m
249
+ @end_boundary_size = boundary.bytesize + 4 # (-- at start, -- at finish)
250
+ @rx_max_size = boundary.bytesize + 6 # (\r\n-- at start, either \r\n or -- at finish)
219
251
  @head_regex = /(.*?#{EOL})#{EOL}/m
220
252
  end
221
253
 
222
- def on_read(content)
223
- handle_empty_content!(content)
224
- @sbuf.concat content
225
- run_parser
254
+ def parse(io)
255
+ outbuf = String.new
256
+ read_data(io, outbuf)
257
+
258
+ loop do
259
+ status =
260
+ case @state
261
+ when :FAST_FORWARD
262
+ handle_fast_forward
263
+ when :CONSUME_TOKEN
264
+ handle_consume_token
265
+ when :MIME_HEAD
266
+ handle_mime_head
267
+ when :MIME_BODY
268
+ handle_mime_body
269
+ else # when :DONE
270
+ return
271
+ end
272
+
273
+ read_data(io, outbuf) if status == :want_read
274
+ end
226
275
  end
227
276
 
228
277
  def result
229
278
  @collector.each do |part|
230
279
  part.get_data do |data|
231
280
  tag_multipart_encoding(part.filename, part.content_type, part.name, data)
232
- @query_parser.normalize_params(@params, part.name, data, @query_parser.param_depth_limit)
281
+ name, data = handle_dummy_encoding(part.name, data)
282
+ @query_parser.normalize_params(@params, name, data)
233
283
  end
234
284
  end
235
285
  MultipartInfo.new @params.to_params_hash, @collector.find_all(&:file?).map(&:body)
@@ -237,40 +287,43 @@ module Rack
237
287
 
238
288
  private
239
289
 
240
- def run_parser
241
- loop do
242
- case @state
243
- when :FAST_FORWARD
244
- break if handle_fast_forward == :want_read
245
- when :CONSUME_TOKEN
246
- break if handle_consume_token == :want_read
247
- when :MIME_HEAD
248
- break if handle_mime_head == :want_read
249
- when :MIME_BODY
250
- break if handle_mime_body == :want_read
251
- when :DONE
252
- break
253
- end
254
- end
290
+ def read_data(io, outbuf)
291
+ content = io.read(@bufsize, outbuf)
292
+ handle_empty_content!(content)
293
+ @sbuf.concat(content)
255
294
  end
256
295
 
296
+ # This handles the initial parser state. We read until we find the starting
297
+ # boundary, then we can transition to the next state. If we find the ending
298
+ # boundary, this is an invalid multipart upload, but keep scanning for opening
299
+ # boundary in that case. If no boundary found, we need to keep reading data
300
+ # and retry. It's highly unlikely the initial read will not consume the
301
+ # boundary. The client would have to deliberately craft a response
302
+ # with the opening boundary beyond the buffer size for that to happen.
257
303
  def handle_fast_forward
258
- tok = consume_boundary
259
-
260
- if tok == :END_BOUNDARY && @sbuf.pos == @end_boundary_size && @sbuf.eos?
261
- # stop parsing a buffer if a buffer is only an end boundary.
262
- @state = :DONE
263
- elsif tok
264
- @state = :MIME_HEAD
265
- else
266
- raise EOFError, "bad content body" if @sbuf.rest_size >= @bufsize
304
+ while true
305
+ case consume_boundary
306
+ when :BOUNDARY
307
+ # found opening boundary, transition to next state
308
+ @state = :MIME_HEAD
309
+ return
310
+ when :END_BOUNDARY
311
+ # invalid multipart upload
312
+ if @sbuf.pos == @end_boundary_size && @sbuf.rest == EOL
313
+ # stop parsing a buffer if a buffer is only an end boundary.
314
+ @state = :DONE
315
+ return
316
+ end
267
317
 
268
- # We raise if we don't find the multipart boundary, to avoid unbounded memory
269
- # buffering. Note that the actual limit is the higher of 16KB and the buffer size (1MB by default)
270
- raise EOFError, "multipart boundary not found within limit" if @sbuf.string.bytesize > BOUNDARY_START_LIMIT
318
+ # retry for opening boundary
319
+ else
320
+ # We raise if we don't find the multipart boundary, to avoid unbounded memory
321
+ # buffering. Note that the actual limit is the higher of 16KB and the buffer size (1MB by default)
322
+ raise Error, "multipart boundary not found within limit" if @sbuf.string.bytesize > BOUNDARY_START_LIMIT
271
323
 
272
- # no boundary found, keep reading data
273
- return :want_read
324
+ # no boundary found, keep reading data
325
+ return :want_read
326
+ end
274
327
  end
275
328
  end
276
329
 
@@ -284,17 +337,101 @@ module Rack
284
337
  end
285
338
  end
286
339
 
340
+ CONTENT_DISPOSITION_MAX_PARAMS = 16
341
+ CONTENT_DISPOSITION_MAX_BYTES = 1536
287
342
  def handle_mime_head
288
343
  if @sbuf.scan_until(@head_regex)
289
344
  head = @sbuf[1]
290
345
  content_type = head[MULTIPART_CONTENT_TYPE, 1]
291
- if name = head[MULTIPART_CONTENT_DISPOSITION, 1]
292
- name = Rack::Auth::Digest::Params::dequote(name)
346
+ if (disposition = head[MULTIPART_CONTENT_DISPOSITION, 1]) &&
347
+ disposition.bytesize <= CONTENT_DISPOSITION_MAX_BYTES
348
+
349
+ # ignore actual content-disposition value (should always be form-data)
350
+ i = disposition.index(';')
351
+ disposition.slice!(0, i+1)
352
+ param = nil
353
+ num_params = 0
354
+
355
+ # Parse parameter list
356
+ while i = disposition.index('=')
357
+ # Only parse up to max parameters, to avoid potential denial of service
358
+ num_params += 1
359
+ break if num_params > CONTENT_DISPOSITION_MAX_PARAMS
360
+
361
+ # Found end of parameter name, ensure forward progress in loop
362
+ param = disposition.slice!(0, i+1)
363
+
364
+ # Remove ending equals and preceding whitespace from parameter name
365
+ param.chomp!('=')
366
+ param.lstrip!
367
+
368
+ if disposition[0] == '"'
369
+ # Parameter value is quoted, parse it, handling backslash escapes
370
+ disposition.slice!(0, 1)
371
+ value = String.new
372
+
373
+ while i = disposition.index(/(["\\])/)
374
+ c = $1
375
+
376
+ # Append all content until ending quote or escape
377
+ value << disposition.slice!(0, i)
378
+
379
+ # Remove either backslash or ending quote,
380
+ # ensures forward progress in loop
381
+ disposition.slice!(0, 1)
382
+
383
+ # stop parsing parameter value if found ending quote
384
+ break if c == '"'
385
+
386
+ escaped_char = disposition.slice!(0, 1)
387
+ if param == 'filename' && escaped_char != '"'
388
+ # Possible IE uploaded filename, append both escape backslash and value
389
+ value << c << escaped_char
390
+ else
391
+ # Other only append escaped value
392
+ value << escaped_char
393
+ end
394
+ end
395
+ else
396
+ if i = disposition.index(';')
397
+ # Parameter value unquoted (which may be invalid), value ends at semicolon
398
+ value = disposition.slice!(0, i)
399
+ else
400
+ # If no ending semicolon, assume remainder of line is value and stop
401
+ # parsing
402
+ disposition.strip!
403
+ value = disposition
404
+ disposition = ''
405
+ end
406
+ end
407
+
408
+ case param
409
+ when 'name'
410
+ name = value
411
+ when 'filename'
412
+ filename = value
413
+ when 'filename*'
414
+ filename_star = value
415
+ # else
416
+ # ignore other parameters
417
+ end
418
+
419
+ # skip trailing semicolon, to proceed to next parameter
420
+ if i = disposition.index(';')
421
+ disposition.slice!(0, i+1)
422
+ end
423
+ end
293
424
  else
294
425
  name = head[MULTIPART_CONTENT_ID, 1]
295
426
  end
296
427
 
297
- filename = get_filename(head)
428
+ if filename_star
429
+ encoding, _, filename = filename_star.split("'", 3)
430
+ filename = normalize_filename(filename || '')
431
+ filename.force_encoding(find_encoding(encoding))
432
+ elsif filename
433
+ filename = normalize_filename(filename)
434
+ end
298
435
 
299
436
  if name.nil? || name.empty?
300
437
  name = filename || "#{content_type || TEXT_PLAIN}[]".dup
@@ -314,7 +451,7 @@ module Rack
314
451
  else
315
452
  # We raise if the mime part header is too large, to avoid unbounded memory
316
453
  # buffering. Note that the actual limit is the higher of 64KB and the buffer size (1MB by default)
317
- raise EOFError, "multipart mime part header too large" if @sbuf.string.bytesize > MIME_HEADER_BYTESIZE_LIMIT
454
+ raise Error, "multipart mime part header too large" if @sbuf.rest.bytesize > MIME_HEADER_BYTESIZE_LIMIT
318
455
 
319
456
  return :want_read
320
457
  end
@@ -322,7 +459,7 @@ module Rack
322
459
 
323
460
  def handle_mime_body
324
461
  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
462
+ body = body_with_boundary.sub(@body_regex_at_end, '') # remove the boundary from the string
326
463
  update_retained_size(body.bytesize) if @body_retained
327
464
  @collector.on_mime_body @mime_index, body
328
465
  @sbuf.pos += body.length + 2 # skip \r\n after the content
@@ -342,12 +479,10 @@ module Rack
342
479
  end
343
480
  end
344
481
 
345
- def full_boundary; @full_boundary; end
346
-
347
482
  def update_retained_size(size)
348
483
  @retained_size += size
349
484
  if @retained_size > BUFFERED_UPLOAD_BYTESIZE_LIMIT
350
- raise EOFError, "multipart data over retained size limit"
485
+ raise Error, "multipart data over retained size limit"
351
486
  end
352
487
  end
353
488
 
@@ -356,51 +491,26 @@ module Rack
356
491
  # end of the boundary. If we don't find the start or end of the
357
492
  # boundary, clear the buffer and return nil.
358
493
  def consume_boundary
359
- while read_buffer = @sbuf.scan_until(BOUNDARY_REGEX)
360
- case read_buffer.strip
361
- when full_boundary then return :BOUNDARY
362
- when @end_boundary then return :END_BOUNDARY
363
- end
364
- return if @sbuf.eos?
494
+ if read_buffer = @sbuf.scan_until(@body_regex)
495
+ read_buffer.end_with?(EOL) ? :BOUNDARY : :END_BOUNDARY
496
+ else
497
+ @sbuf.terminate
498
+ nil
365
499
  end
366
500
  end
367
501
 
368
- def get_filename(head)
369
- filename = nil
370
- case head
371
- when RFC2183
372
- params = Hash[*head.scan(DISPPARM).flat_map(&:compact)]
373
-
374
- if filename = params['filename']
375
- filename = $1 if filename =~ /^"(.*)"$/
376
- elsif filename = params['filename*']
377
- encoding, _, filename = filename.split("'", 3)
378
- end
379
- when BROKEN
380
- filename = $1
381
- filename = $1 if filename =~ /^"(.*)"$/
382
- end
383
-
384
- return unless filename
385
-
502
+ def normalize_filename(filename)
386
503
  if filename.scan(/%.?.?/).all? { |s| /%[0-9a-fA-F]{2}/.match?(s) }
387
504
  filename = Utils.unescape_path(filename)
388
505
  end
389
506
 
390
507
  filename.scrub!
391
508
 
392
- if filename !~ /\\[^\\"]/
393
- filename = filename.gsub(/\\(.)/, '\1')
394
- end
395
-
396
- if encoding
397
- filename.force_encoding ::Encoding.find(encoding)
398
- end
399
-
400
- filename
509
+ filename.split(/[\/\\]/).last || String.new
401
510
  end
402
511
 
403
512
  CHARSET = "charset"
513
+ deprecate_constant :CHARSET
404
514
 
405
515
  def tag_multipart_encoding(filename, content_type, name, body)
406
516
  name = name.to_s
@@ -421,7 +531,9 @@ module Rack
421
531
  k.strip!
422
532
  v.strip!
423
533
  v = v[1..-2] if v.start_with?('"') && v.end_with?('"')
424
- encoding = Encoding.find v if k == CHARSET
534
+ if k == "charset"
535
+ encoding = find_encoding(v)
536
+ end
425
537
  end
426
538
  end
427
539
  end
@@ -430,9 +542,37 @@ module Rack
430
542
  body.force_encoding(encoding)
431
543
  end
432
544
 
545
+ # Return the related Encoding object. However, because
546
+ # enc is submitted by the user, it may be invalid, so
547
+ # use a binary encoding in that case.
548
+ def find_encoding(enc)
549
+ Encoding.find enc
550
+ rescue ArgumentError
551
+ Encoding::BINARY
552
+ end
553
+
554
+ REENCODE_DUMMY_ENCODINGS = {
555
+ # ISO-2022-JP is a legacy but still widely used encoding in Japan
556
+ # Here we convert ISO-2022-JP to UTF-8 so that it can be handled.
557
+ Encoding::ISO_2022_JP => true
558
+
559
+ # Other dummy encodings are rarely used and have not been supported yet.
560
+ # Adding support for them will require careful considerations.
561
+ }
562
+
563
+ def handle_dummy_encoding(name, body)
564
+ # A string object with a 'dummy' encoding does not have full functionality and can cause errors.
565
+ # So here we covert it to UTF-8 so that it can be handled properly.
566
+ if name.encoding.dummy? && REENCODE_DUMMY_ENCODINGS[name.encoding]
567
+ name = name.encode(Encoding::UTF_8)
568
+ body = body.encode(Encoding::UTF_8)
569
+ end
570
+ return name, body
571
+ end
572
+
433
573
  def handle_empty_content!(content)
434
574
  if content.nil? || content.empty?
435
- raise EOFError
575
+ raise EmptyContentError
436
576
  end
437
577
  end
438
578
  end