rack 2.2.15 → 3.1.16

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 +440 -70
  3. data/CONTRIBUTING.md +63 -55
  4. data/MIT-LICENSE +1 -1
  5. data/README.md +355 -0
  6. data/SPEC.rdoc +204 -131
  7. data/lib/rack/auth/abstract/handler.rb +3 -1
  8. data/lib/rack/auth/abstract/request.rb +3 -1
  9. data/lib/rack/auth/basic.rb +1 -3
  10. data/lib/rack/bad_request.rb +8 -0
  11. data/lib/rack/body_proxy.rb +21 -3
  12. data/lib/rack/builder.rb +102 -69
  13. data/lib/rack/cascade.rb +2 -3
  14. data/lib/rack/common_logger.rb +22 -17
  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 +17 -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 +8 -3
  30. data/lib/rack/method_override.rb +5 -1
  31. data/lib/rack/mime.rb +14 -5
  32. data/lib/rack/mock.rb +1 -300
  33. data/lib/rack/mock_request.rb +161 -0
  34. data/lib/rack/mock_response.rb +153 -0
  35. data/lib/rack/multipart/generator.rb +7 -5
  36. data/lib/rack/multipart/parser.rb +215 -95
  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 +79 -101
  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 +236 -237
  54. data/lib/rack/version.rb +1 -9
  55. data/lib/rack.rb +13 -89
  56. metadata +15 -41
  57. data/README.rdoc +0 -347
  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 -53
  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 -90
  87. data/rack.gemspec +0 -46
@@ -0,0 +1,153 @@
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
+ begin
14
+ # Recent versions of the CGI gem may not provide `CGI::Cookie`.
15
+ require 'cgi/cookie'
16
+ Cookie = CGI::Cookie
17
+ rescue LoadError
18
+ class Cookie
19
+ attr_reader :name, :value, :path, :domain, :expires, :secure
20
+
21
+ def initialize(args)
22
+ @name = args["name"]
23
+ @value = args["value"]
24
+ @path = args["path"]
25
+ @domain = args["domain"]
26
+ @expires = args["expires"]
27
+ @secure = args["secure"]
28
+ end
29
+
30
+ def method_missing(method_name, *args, &block)
31
+ @value.send(method_name, *args, &block)
32
+ end
33
+ # :nocov:
34
+ ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
35
+ # :nocov:
36
+
37
+ def respond_to_missing?(method_name, include_all = false)
38
+ @value.respond_to?(method_name, include_all) || super
39
+ end
40
+ end
41
+ end
42
+
43
+ class << self
44
+ alias [] new
45
+ end
46
+
47
+ # Headers
48
+ attr_reader :original_headers, :cookies
49
+
50
+ # Errors
51
+ attr_accessor :errors
52
+
53
+ def initialize(status, headers, body, errors = nil)
54
+ @original_headers = headers
55
+
56
+ if errors
57
+ @errors = errors.string if errors.respond_to?(:string)
58
+ else
59
+ @errors = ""
60
+ end
61
+
62
+ super(body, status, headers)
63
+
64
+ @cookies = parse_cookies_from_header
65
+ buffered_body!
66
+ end
67
+
68
+ def =~(other)
69
+ body =~ other
70
+ end
71
+
72
+ def match(other)
73
+ body.match other
74
+ end
75
+
76
+ def body
77
+ return @buffered_body if defined?(@buffered_body)
78
+
79
+ # FIXME: apparently users of MockResponse expect the return value of
80
+ # MockResponse#body to be a string. However, the real response object
81
+ # returns the body as a list.
82
+ #
83
+ # See spec_showstatus.rb:
84
+ #
85
+ # should "not replace existing messages" do
86
+ # ...
87
+ # res.body.should == "foo!"
88
+ # end
89
+ buffer = @buffered_body = String.new
90
+
91
+ @body.each do |chunk|
92
+ buffer << chunk
93
+ end
94
+
95
+ return buffer
96
+ end
97
+
98
+ def empty?
99
+ [201, 204, 304].include? status
100
+ end
101
+
102
+ def cookie(name)
103
+ cookies.fetch(name, nil)
104
+ end
105
+
106
+ private
107
+
108
+ def parse_cookies_from_header
109
+ cookies = Hash.new
110
+ set_cookie_header = headers['set-cookie']
111
+ if set_cookie_header && !set_cookie_header.empty?
112
+ Array(set_cookie_header).each do |cookie|
113
+ cookie_name, cookie_filling = cookie.split('=', 2)
114
+ cookie_attributes = identify_cookie_attributes cookie_filling
115
+ parsed_cookie = Cookie.new(
116
+ 'name' => cookie_name.strip,
117
+ 'value' => cookie_attributes.fetch('value'),
118
+ 'path' => cookie_attributes.fetch('path', nil),
119
+ 'domain' => cookie_attributes.fetch('domain', nil),
120
+ 'expires' => cookie_attributes.fetch('expires', nil),
121
+ 'secure' => cookie_attributes.fetch('secure', false)
122
+ )
123
+ cookies.store(cookie_name, parsed_cookie)
124
+ end
125
+ end
126
+ cookies
127
+ end
128
+
129
+ def identify_cookie_attributes(cookie_filling)
130
+ cookie_bits = cookie_filling.split(';')
131
+ cookie_attributes = Hash.new
132
+ cookie_attributes.store('value', Array(cookie_bits[0].strip))
133
+ cookie_bits.drop(1).each do |bit|
134
+ if bit.include? '='
135
+ cookie_attribute, attribute_value = bit.split('=', 2)
136
+ cookie_attributes.store(cookie_attribute.strip.downcase, attribute_value.strip)
137
+ end
138
+ if bit.include? 'secure'
139
+ cookie_attributes.store('secure', true)
140
+ end
141
+ end
142
+
143
+ if cookie_attributes.key? 'max-age'
144
+ cookie_attributes.store('expires', Time.now + cookie_attributes['max-age'].to_i)
145
+ elsif cookie_attributes.key? 'expires'
146
+ cookie_attributes.store('expires', Time.httpdate(cookie_attributes['expires']))
147
+ end
148
+
149
+ cookie_attributes
150
+ end
151
+
152
+ end
153
+ 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,43 @@
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
+ class Parser
13
42
  BUFSIZE = 1_048_576
14
43
  TEXT_PLAIN = "text/plain"
15
44
  TEMPFILE_FACTORY = lambda { |filename, content_type|
@@ -18,8 +47,6 @@ module Rack
18
47
  Tempfile.new(["RackMultipart", extension])
19
48
  }
20
49
 
21
- BOUNDARY_REGEX = /\A([^\n]*(?:\n|\Z))/
22
-
23
50
  class BoundedIO # :nodoc:
24
51
  def initialize(io, content_length)
25
52
  @io = io
@@ -41,16 +68,12 @@ module Rack
41
68
  if str
42
69
  @cursor += str.bytesize
43
70
  else
44
- # Raise an error for mismatching Content-Length and actual contents
71
+ # Raise an error for mismatching content-length and actual contents
45
72
  raise EOFError, "bad content body"
46
73
  end
47
74
 
48
75
  str
49
76
  end
50
-
51
- def rewind
52
- @io.rewind
53
- end
54
77
  end
55
78
 
56
79
  MultipartInfo = Struct.new :params, :tmp_files
@@ -69,18 +92,17 @@ module Rack
69
92
  boundary = parse_boundary content_type
70
93
  return EMPTY unless boundary
71
94
 
95
+ if boundary.length > 70
96
+ # RFC 1521 Section 7.2.1 imposes a 70 character maximum for the boundary.
97
+ # Most clients use no more than 55 characters.
98
+ raise BoundaryTooLongError, "multipart boundary size too large (#{boundary.length} characters)"
99
+ end
100
+
72
101
  io = BoundedIO.new(io, content_length) if content_length
73
- outbuf = String.new
74
102
 
75
103
  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
104
+ parser.parse(io)
82
105
 
83
- io.rewind
84
106
  parser.result
85
107
  end
86
108
 
@@ -180,33 +202,48 @@ module Rack
180
202
  def initialize(boundary, tempfile, bufsize, query_parser)
181
203
  @query_parser = query_parser
182
204
  @params = query_parser.make_params
183
- @boundary = "--#{boundary}"
184
205
  @bufsize = bufsize
185
206
 
186
- @full_boundary = @boundary
187
- @end_boundary = @boundary + '--'
188
207
  @state = :FAST_FORWARD
189
208
  @mime_index = 0
190
209
  @collector = Collector.new tempfile
191
210
 
192
211
  @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
212
+ @body_regex = /(?:#{EOL}|\A)--#{Regexp.quote(boundary)}(?:#{EOL}|--)/m
213
+ @body_regex_at_end = /#{@body_regex}\z/m
214
+ @end_boundary_size = boundary.bytesize + 4 # (-- at start, -- at finish)
215
+ @rx_max_size = boundary.bytesize + 6 # (\r\n-- at start, either \r\n or -- at finish)
196
216
  @head_regex = /(.*?#{EOL})#{EOL}/m
197
217
  end
198
218
 
199
- def on_read(content)
200
- handle_empty_content!(content)
201
- @sbuf.concat content
202
- run_parser
219
+ def parse(io)
220
+ outbuf = String.new
221
+ read_data(io, outbuf)
222
+
223
+ loop do
224
+ status =
225
+ case @state
226
+ when :FAST_FORWARD
227
+ handle_fast_forward
228
+ when :CONSUME_TOKEN
229
+ handle_consume_token
230
+ when :MIME_HEAD
231
+ handle_mime_head
232
+ when :MIME_BODY
233
+ handle_mime_body
234
+ else # when :DONE
235
+ return
236
+ end
237
+
238
+ read_data(io, outbuf) if status == :want_read
239
+ end
203
240
  end
204
241
 
205
242
  def result
206
243
  @collector.each do |part|
207
244
  part.get_data do |data|
208
245
  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)
246
+ @query_parser.normalize_params(@params, part.name, data)
210
247
  end
211
248
  end
212
249
  MultipartInfo.new @params.to_params_hash, @collector.find_all(&:file?).map(&:body)
@@ -214,34 +251,45 @@ module Rack
214
251
 
215
252
  private
216
253
 
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
254
+ def dequote(str) # From WEBrick::HTTPUtils
255
+ ret = (/\A"(.*)"\Z/ =~ str) ? $1 : str.dup
256
+ ret.gsub!(/\\(.)/, "\\1")
257
+ ret
258
+ end
259
+
260
+ def read_data(io, outbuf)
261
+ content = io.read(@bufsize, outbuf)
262
+ handle_empty_content!(content)
263
+ @sbuf.concat(content)
232
264
  end
233
265
 
266
+ # This handles the initial parser state. We read until we find the starting
267
+ # boundary, then we can transition to the next state. If we find the ending
268
+ # boundary, this is an invalid multipart upload, but keep scanning for opening
269
+ # boundary in that case. If no boundary found, we need to keep reading data
270
+ # and retry. It's highly unlikely the initial read will not consume the
271
+ # boundary. The client would have to deliberately craft a response
272
+ # with the opening boundary beyond the buffer size for that to happen.
234
273
  def handle_fast_forward
235
- tok = consume_boundary
274
+ while true
275
+ case consume_boundary
276
+ when :BOUNDARY
277
+ # found opening boundary, transition to next state
278
+ @state = :MIME_HEAD
279
+ return
280
+ when :END_BOUNDARY
281
+ # invalid multipart upload
282
+ if @sbuf.pos == @end_boundary_size && @sbuf.rest == EOL
283
+ # stop parsing a buffer if a buffer is only an end boundary.
284
+ @state = :DONE
285
+ return
286
+ end
236
287
 
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
288
+ # retry for opening boundary
289
+ else
290
+ # no boundary found, keep reading data
291
+ return :want_read
292
+ end
245
293
  end
246
294
  end
247
295
 
@@ -255,17 +303,101 @@ module Rack
255
303
  end
256
304
  end
257
305
 
306
+ CONTENT_DISPOSITION_MAX_PARAMS = 16
307
+ CONTENT_DISPOSITION_MAX_BYTES = 1536
258
308
  def handle_mime_head
259
309
  if @sbuf.scan_until(@head_regex)
260
310
  head = @sbuf[1]
261
311
  content_type = head[MULTIPART_CONTENT_TYPE, 1]
262
- if name = head[MULTIPART_CONTENT_DISPOSITION, 1]
263
- name = Rack::Auth::Digest::Params::dequote(name)
312
+ if (disposition = head[MULTIPART_CONTENT_DISPOSITION, 1]) &&
313
+ disposition.bytesize <= CONTENT_DISPOSITION_MAX_BYTES
314
+
315
+ # ignore actual content-disposition value (should always be form-data)
316
+ i = disposition.index(';')
317
+ disposition.slice!(0, i+1)
318
+ param = nil
319
+ num_params = 0
320
+
321
+ # Parse parameter list
322
+ while i = disposition.index('=')
323
+ # Only parse up to max parameters, to avoid potential denial of service
324
+ num_params += 1
325
+ break if num_params > CONTENT_DISPOSITION_MAX_PARAMS
326
+
327
+ # Found end of parameter name, ensure forward progress in loop
328
+ param = disposition.slice!(0, i+1)
329
+
330
+ # Remove ending equals and preceding whitespace from parameter name
331
+ param.chomp!('=')
332
+ param.lstrip!
333
+
334
+ if disposition[0] == '"'
335
+ # Parameter value is quoted, parse it, handling backslash escapes
336
+ disposition.slice!(0, 1)
337
+ value = String.new
338
+
339
+ while i = disposition.index(/(["\\])/)
340
+ c = $1
341
+
342
+ # Append all content until ending quote or escape
343
+ value << disposition.slice!(0, i)
344
+
345
+ # Remove either backslash or ending quote,
346
+ # ensures forward progress in loop
347
+ disposition.slice!(0, 1)
348
+
349
+ # stop parsing parameter value if found ending quote
350
+ break if c == '"'
351
+
352
+ escaped_char = disposition.slice!(0, 1)
353
+ if param == 'filename' && escaped_char != '"'
354
+ # Possible IE uploaded filename, append both escape backslash and value
355
+ value << c << escaped_char
356
+ else
357
+ # Other only append escaped value
358
+ value << escaped_char
359
+ end
360
+ end
361
+ else
362
+ if i = disposition.index(';')
363
+ # Parameter value unquoted (which may be invalid), value ends at semicolon
364
+ value = disposition.slice!(0, i)
365
+ else
366
+ # If no ending semicolon, assume remainder of line is value and stop
367
+ # parsing
368
+ disposition.strip!
369
+ value = disposition
370
+ disposition = ''
371
+ end
372
+ end
373
+
374
+ case param
375
+ when 'name'
376
+ name = value
377
+ when 'filename'
378
+ filename = value
379
+ when 'filename*'
380
+ filename_star = value
381
+ # else
382
+ # ignore other parameters
383
+ end
384
+
385
+ # skip trailing semicolon, to proceed to next parameter
386
+ if i = disposition.index(';')
387
+ disposition.slice!(0, i+1)
388
+ end
389
+ end
264
390
  else
265
391
  name = head[MULTIPART_CONTENT_ID, 1]
266
392
  end
267
393
 
268
- filename = get_filename(head)
394
+ if filename_star
395
+ encoding, _, filename = filename_star.split("'", 3)
396
+ filename = normalize_filename(filename || '')
397
+ filename.force_encoding(find_encoding(encoding))
398
+ elsif filename
399
+ filename = normalize_filename(filename)
400
+ end
269
401
 
270
402
  if name.nil? || name.empty?
271
403
  name = filename || "#{content_type || TEXT_PLAIN}[]".dup
@@ -280,7 +412,7 @@ module Rack
280
412
 
281
413
  def handle_mime_body
282
414
  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
415
+ body = body_with_boundary.sub(@body_regex_at_end, '') # remove the boundary from the string
284
416
  @collector.on_mime_body @mime_index, body
285
417
  @sbuf.pos += body.length + 2 # skip \r\n after the content
286
418
  @state = :CONSUME_TOKEN
@@ -297,54 +429,31 @@ module Rack
297
429
  end
298
430
  end
299
431
 
300
- def full_boundary; @full_boundary; end
301
-
432
+ # Scan until the we find the start or end of the boundary.
433
+ # If we find it, return the appropriate symbol for the start or
434
+ # end of the boundary. If we don't find the start or end of the
435
+ # boundary, clear the buffer and return nil.
302
436
  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?
437
+ if read_buffer = @sbuf.scan_until(@body_regex)
438
+ read_buffer.end_with?(EOL) ? :BOUNDARY : :END_BOUNDARY
439
+ else
440
+ @sbuf.terminate
441
+ nil
309
442
  end
310
443
  end
311
444
 
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
-
445
+ def normalize_filename(filename)
330
446
  if filename.scan(/%.?.?/).all? { |s| /%[0-9a-fA-F]{2}/.match?(s) }
331
447
  filename = Utils.unescape_path(filename)
332
448
  end
333
449
 
334
450
  filename.scrub!
335
451
 
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
452
+ filename.split(/[\/\\]/).last || String.new
345
453
  end
346
454
 
347
455
  CHARSET = "charset"
456
+ deprecate_constant :CHARSET
348
457
 
349
458
  def tag_multipart_encoding(filename, content_type, name, body)
350
459
  name = name.to_s
@@ -365,7 +474,9 @@ module Rack
365
474
  k.strip!
366
475
  v.strip!
367
476
  v = v[1..-2] if v.start_with?('"') && v.end_with?('"')
368
- encoding = Encoding.find v if k == CHARSET
477
+ if k == "charset"
478
+ encoding = find_encoding(v)
479
+ end
369
480
  end
370
481
  end
371
482
  end
@@ -374,9 +485,18 @@ module Rack
374
485
  body.force_encoding(encoding)
375
486
  end
376
487
 
488
+ # Return the related Encoding object. However, because
489
+ # enc is submitted by the user, it may be invalid, so
490
+ # use a binary encoding in that case.
491
+ def find_encoding(enc)
492
+ Encoding.find enc
493
+ rescue ArgumentError
494
+ Encoding::BINARY
495
+ end
496
+
377
497
  def handle_empty_content!(content)
378
498
  if content.nil? || content.empty?
379
- raise EOFError
499
+ raise EmptyContentError
380
500
  end
381
501
  end
382
502
  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