rack 2.2.6 → 3.1.2

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 (87) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +295 -72
  3. data/CONTRIBUTING.md +63 -55
  4. data/MIT-LICENSE +1 -1
  5. data/README.md +328 -0
  6. data/SPEC.rdoc +213 -136
  7. data/lib/rack/auth/abstract/handler.rb +3 -1
  8. data/lib/rack/auth/abstract/request.rb +3 -1
  9. data/lib/rack/auth/basic.rb +1 -4
  10. data/lib/rack/bad_request.rb +8 -0
  11. data/lib/rack/body_proxy.rb +21 -3
  12. data/lib/rack/builder.rb +102 -69
  13. data/lib/rack/cascade.rb +2 -3
  14. data/lib/rack/common_logger.rb +23 -18
  15. data/lib/rack/conditional_get.rb +18 -15
  16. data/lib/rack/constants.rb +67 -0
  17. data/lib/rack/content_length.rb +12 -16
  18. data/lib/rack/content_type.rb +8 -5
  19. data/lib/rack/deflater.rb +40 -26
  20. data/lib/rack/directory.rb +9 -3
  21. data/lib/rack/etag.rb +14 -23
  22. data/lib/rack/events.rb +4 -0
  23. data/lib/rack/files.rb +15 -17
  24. data/lib/rack/head.rb +9 -8
  25. data/lib/rack/headers.rb +238 -0
  26. data/lib/rack/lint.rb +864 -681
  27. data/lib/rack/lock.rb +2 -5
  28. data/lib/rack/logger.rb +3 -0
  29. data/lib/rack/media_type.rb +9 -4
  30. data/lib/rack/method_override.rb +5 -1
  31. data/lib/rack/mime.rb +14 -5
  32. data/lib/rack/mock.rb +1 -271
  33. data/lib/rack/mock_request.rb +171 -0
  34. data/lib/rack/mock_response.rb +124 -0
  35. data/lib/rack/multipart/generator.rb +7 -5
  36. data/lib/rack/multipart/parser.rb +232 -94
  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 +246 -121
  44. data/lib/rack/response.rb +146 -66
  45. data/lib/rack/rewindable_input.rb +24 -5
  46. data/lib/rack/runtime.rb +7 -6
  47. data/lib/rack/sendfile.rb +30 -25
  48. data/lib/rack/show_exceptions.rb +21 -4
  49. data/lib/rack/show_status.rb +17 -7
  50. data/lib/rack/static.rb +8 -8
  51. data/lib/rack/tempfile_reaper.rb +15 -4
  52. data/lib/rack/urlmap.rb +3 -1
  53. data/lib/rack/utils.rb +247 -244
  54. data/lib/rack/version.rb +1 -9
  55. data/lib/rack.rb +13 -89
  56. metadata +15 -41
  57. data/README.rdoc +0 -306
  58. data/Rakefile +0 -130
  59. data/bin/rackup +0 -5
  60. data/contrib/rack.png +0 -0
  61. data/contrib/rack.svg +0 -150
  62. data/contrib/rack_logo.svg +0 -164
  63. data/contrib/rdoc.css +0 -412
  64. data/example/lobster.ru +0 -6
  65. data/example/protectedlobster.rb +0 -16
  66. data/example/protectedlobster.ru +0 -10
  67. data/lib/rack/auth/digest/md5.rb +0 -131
  68. data/lib/rack/auth/digest/nonce.rb +0 -54
  69. data/lib/rack/auth/digest/params.rb +0 -54
  70. data/lib/rack/auth/digest/request.rb +0 -43
  71. data/lib/rack/chunked.rb +0 -117
  72. data/lib/rack/core_ext/regexp.rb +0 -14
  73. data/lib/rack/file.rb +0 -7
  74. data/lib/rack/handler/cgi.rb +0 -59
  75. data/lib/rack/handler/fastcgi.rb +0 -100
  76. data/lib/rack/handler/lsws.rb +0 -61
  77. data/lib/rack/handler/scgi.rb +0 -71
  78. data/lib/rack/handler/thin.rb +0 -36
  79. data/lib/rack/handler/webrick.rb +0 -129
  80. data/lib/rack/handler.rb +0 -104
  81. data/lib/rack/lobster.rb +0 -70
  82. data/lib/rack/server.rb +0 -466
  83. data/lib/rack/session/abstract/id.rb +0 -523
  84. data/lib/rack/session/cookie.rb +0 -203
  85. data/lib/rack/session/memcache.rb +0 -10
  86. data/lib/rack/session/pool.rb +0 -85
  87. data/rack.gemspec +0 -46
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cgi/cookie'
4
+ require 'time'
5
+
6
+ require_relative 'response'
7
+
8
+ module Rack
9
+ # Rack::MockResponse provides useful helpers for testing your apps.
10
+ # Usually, you don't create the MockResponse on your own, but use
11
+ # MockRequest.
12
+
13
+ class MockResponse < Rack::Response
14
+ class << self
15
+ alias [] new
16
+ end
17
+
18
+ # Headers
19
+ attr_reader :original_headers, :cookies
20
+
21
+ # Errors
22
+ attr_accessor :errors
23
+
24
+ def initialize(status, headers, body, errors = nil)
25
+ @original_headers = headers
26
+
27
+ if errors
28
+ @errors = errors.string if errors.respond_to?(:string)
29
+ else
30
+ @errors = ""
31
+ end
32
+
33
+ super(body, status, headers)
34
+
35
+ @cookies = parse_cookies_from_header
36
+ buffered_body!
37
+ end
38
+
39
+ def =~(other)
40
+ body =~ other
41
+ end
42
+
43
+ def match(other)
44
+ body.match other
45
+ end
46
+
47
+ def body
48
+ return @buffered_body if defined?(@buffered_body)
49
+
50
+ # FIXME: apparently users of MockResponse expect the return value of
51
+ # MockResponse#body to be a string. However, the real response object
52
+ # returns the body as a list.
53
+ #
54
+ # See spec_showstatus.rb:
55
+ #
56
+ # should "not replace existing messages" do
57
+ # ...
58
+ # res.body.should == "foo!"
59
+ # end
60
+ buffer = @buffered_body = String.new
61
+
62
+ @body.each do |chunk|
63
+ buffer << chunk
64
+ end
65
+
66
+ return buffer
67
+ end
68
+
69
+ def empty?
70
+ [201, 204, 304].include? status
71
+ end
72
+
73
+ def cookie(name)
74
+ cookies.fetch(name, nil)
75
+ end
76
+
77
+ private
78
+
79
+ def parse_cookies_from_header
80
+ cookies = Hash.new
81
+ 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,20 +2,48 @@
2
2
 
3
3
  require 'strscan'
4
4
 
5
+ require_relative '../utils'
6
+ require_relative '../bad_request'
7
+
5
8
  module Rack
6
9
  module Multipart
7
- class MultipartPartLimitError < Errno::EMFILE; end
10
+ class MultipartPartLimitError < Errno::EMFILE
11
+ include BadRequest
12
+ end
8
13
 
9
- class Parser
10
- (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
29
+
30
+ # Prefer to use the BoundaryTooLongError class or Rack::BadRequest.
31
+ Error = BoundaryTooLongError
11
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
12
40
  BUFSIZE = 1_048_576
13
41
  TEXT_PLAIN = "text/plain"
14
42
  TEMPFILE_FACTORY = lambda { |filename, content_type|
15
- Tempfile.new(["RackMultipart", ::File.extname(filename.gsub("\0", '%00'))])
16
- }
43
+ extension = ::File.extname(filename.gsub("\0", '%00'))[0, 129]
17
44
 
18
- BOUNDARY_REGEX = /\A([^\n]*(?:\n|\Z))/
45
+ Tempfile.new(["RackMultipart", extension])
46
+ }
19
47
 
20
48
  class BoundedIO # :nodoc:
21
49
  def initialize(io, content_length)
@@ -38,16 +66,12 @@ module Rack
38
66
  if str
39
67
  @cursor += str.bytesize
40
68
  else
41
- # Raise an error for mismatching Content-Length and actual contents
69
+ # Raise an error for mismatching content-length and actual contents
42
70
  raise EOFError, "bad content body"
43
71
  end
44
72
 
45
73
  str
46
74
  end
47
-
48
- def rewind
49
- @io.rewind
50
- end
51
75
  end
52
76
 
53
77
  MultipartInfo = Struct.new :params, :tmp_files
@@ -66,18 +90,17 @@ module Rack
66
90
  boundary = parse_boundary content_type
67
91
  return EMPTY unless boundary
68
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
+
69
99
  io = BoundedIO.new(io, content_length) if content_length
70
- outbuf = String.new
71
100
 
72
101
  parser = new(boundary, tmpfile, bufsize, qp)
73
- parser.on_read io.read(bufsize, outbuf)
102
+ parser.parse(io)
74
103
 
75
- loop do
76
- break if parser.state == :DONE
77
- parser.on_read io.read(bufsize, outbuf)
78
- end
79
-
80
- io.rewind
81
104
  parser.result
82
105
  end
83
106
 
@@ -140,7 +163,7 @@ module Rack
140
163
 
141
164
  @mime_parts[mime_index] = klass.new(body, head, filename, content_type, name)
142
165
 
143
- check_open_files
166
+ check_part_limits
144
167
  end
145
168
 
146
169
  def on_mime_body(mime_index, content)
@@ -152,13 +175,23 @@ module Rack
152
175
 
153
176
  private
154
177
 
155
- def check_open_files
156
- if Utils.multipart_part_limit > 0
157
- if @open_files >= Utils.multipart_part_limit
178
+ def check_part_limits
179
+ file_limit = Utils.multipart_file_limit
180
+ part_limit = Utils.multipart_total_part_limit
181
+
182
+ if file_limit && file_limit > 0
183
+ if @open_files >= file_limit
158
184
  @mime_parts.each(&:close)
159
185
  raise MultipartPartLimitError, 'Maximum file multiparts in content reached'
160
186
  end
161
187
  end
188
+
189
+ if part_limit && part_limit > 0
190
+ if @mime_parts.size >= part_limit
191
+ @mime_parts.each(&:close)
192
+ raise MultipartTotalPartLimitError, 'Maximum total multiparts in content reached'
193
+ end
194
+ end
162
195
  end
163
196
  end
164
197
 
@@ -167,32 +200,48 @@ module Rack
167
200
  def initialize(boundary, tempfile, bufsize, query_parser)
168
201
  @query_parser = query_parser
169
202
  @params = query_parser.make_params
170
- @boundary = "--#{boundary}"
171
203
  @bufsize = bufsize
172
204
 
173
- @full_boundary = @boundary
174
- @end_boundary = @boundary + '--'
175
205
  @state = :FAST_FORWARD
176
206
  @mime_index = 0
177
207
  @collector = Collector.new tempfile
178
208
 
179
209
  @sbuf = StringScanner.new("".dup)
180
- @body_regex = /(?:#{EOL})?#{Regexp.quote(@boundary)}(?:#{EOL}|--)/m
181
- @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)
182
214
  @head_regex = /(.*?#{EOL})#{EOL}/m
183
215
  end
184
216
 
185
- def on_read(content)
186
- handle_empty_content!(content)
187
- @sbuf.concat content
188
- 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
189
238
  end
190
239
 
191
240
  def result
192
241
  @collector.each do |part|
193
242
  part.get_data do |data|
194
243
  tag_multipart_encoding(part.filename, part.content_type, part.name, data)
195
- @query_parser.normalize_params(@params, part.name, data, @query_parser.param_depth_limit)
244
+ @query_parser.normalize_params(@params, part.name, data)
196
245
  end
197
246
  end
198
247
  MultipartInfo.new @params.to_params_hash, @collector.find_all(&:file?).map(&:body)
@@ -200,29 +249,45 @@ module Rack
200
249
 
201
250
  private
202
251
 
203
- def run_parser
204
- loop do
205
- case @state
206
- when :FAST_FORWARD
207
- break if handle_fast_forward == :want_read
208
- when :CONSUME_TOKEN
209
- break if handle_consume_token == :want_read
210
- when :MIME_HEAD
211
- break if handle_mime_head == :want_read
212
- when :MIME_BODY
213
- break if handle_mime_body == :want_read
214
- when :DONE
215
- break
216
- end
217
- end
252
+ def dequote(str) # From WEBrick::HTTPUtils
253
+ ret = (/\A"(.*)"\Z/ =~ str) ? $1 : str.dup
254
+ ret.gsub!(/\\(.)/, "\\1")
255
+ ret
218
256
  end
219
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.
220
271
  def handle_fast_forward
221
- if consume_boundary
222
- @state = :MIME_HEAD
223
- else
224
- raise EOFError, "bad content body" if @sbuf.rest_size >= @bufsize
225
- :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
226
291
  end
227
292
  end
228
293
 
@@ -236,17 +301,102 @@ module Rack
236
301
  end
237
302
  end
238
303
 
304
+ CONTENT_DISPOSITION_MAX_PARAMS = 16
305
+ CONTENT_DISPOSITION_MAX_BYTES = 1536
239
306
  def handle_mime_head
240
307
  if @sbuf.scan_until(@head_regex)
241
308
  head = @sbuf[1]
242
309
  content_type = head[MULTIPART_CONTENT_TYPE, 1]
243
- if name = head[MULTIPART_CONTENT_DISPOSITION, 1]
244
- 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
245
388
  else
246
389
  name = head[MULTIPART_CONTENT_ID, 1]
247
390
  end
248
391
 
249
- 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
250
400
 
251
401
  if name.nil? || name.empty?
252
402
  name = filename || "#{content_type || TEXT_PLAIN}[]".dup
@@ -261,7 +411,7 @@ module Rack
261
411
 
262
412
  def handle_mime_body
263
413
  if (body_with_boundary = @sbuf.check_until(@body_regex)) # check but do not advance the pointer yet
264
- 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
265
415
  @collector.on_mime_body @mime_index, body
266
416
  @sbuf.pos += body.length + 2 # skip \r\n after the content
267
417
  @state = :CONSUME_TOKEN
@@ -278,54 +428,31 @@ module Rack
278
428
  end
279
429
  end
280
430
 
281
- def full_boundary; @full_boundary; end
282
-
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.
283
435
  def consume_boundary
284
- while read_buffer = @sbuf.scan_until(BOUNDARY_REGEX)
285
- case read_buffer.strip
286
- when full_boundary then return :BOUNDARY
287
- when @end_boundary then return :END_BOUNDARY
288
- end
289
- 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
290
441
  end
291
442
  end
292
443
 
293
- def get_filename(head)
294
- filename = nil
295
- case head
296
- when RFC2183
297
- params = Hash[*head.scan(DISPPARM).flat_map(&:compact)]
298
-
299
- if filename = params['filename']
300
- filename = $1 if filename =~ /^"(.*)"$/
301
- elsif filename = params['filename*']
302
- encoding, _, filename = filename.split("'", 3)
303
- end
304
- when BROKEN
305
- filename = $1
306
- filename = $1 if filename =~ /^"(.*)"$/
307
- end
308
-
309
- return unless filename
310
-
444
+ def normalize_filename(filename)
311
445
  if filename.scan(/%.?.?/).all? { |s| /%[0-9a-fA-F]{2}/.match?(s) }
312
446
  filename = Utils.unescape_path(filename)
313
447
  end
314
448
 
315
449
  filename.scrub!
316
450
 
317
- if filename !~ /\\[^\\"]/
318
- filename = filename.gsub(/\\(.)/, '\1')
319
- end
320
-
321
- if encoding
322
- filename.force_encoding ::Encoding.find(encoding)
323
- end
324
-
325
- filename
451
+ filename.split(/[\/\\]/).last || String.new
326
452
  end
327
453
 
328
454
  CHARSET = "charset"
455
+ deprecate_constant :CHARSET
329
456
 
330
457
  def tag_multipart_encoding(filename, content_type, name, body)
331
458
  name = name.to_s
@@ -346,7 +473,9 @@ module Rack
346
473
  k.strip!
347
474
  v.strip!
348
475
  v = v[1..-2] if v.start_with?('"') && v.end_with?('"')
349
- encoding = Encoding.find v if k == CHARSET
476
+ if k == "charset"
477
+ encoding = find_encoding(v)
478
+ end
350
479
  end
351
480
  end
352
481
  end
@@ -355,9 +484,18 @@ module Rack
355
484
  body.force_encoding(encoding)
356
485
  end
357
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
+
358
496
  def handle_empty_content!(content)
359
497
  if content.nil? || content.empty?
360
- raise EOFError
498
+ raise EmptyContentError
361
499
  end
362
500
  end
363
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