rack 2.2.23 → 3.0.0.beta1

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 (85) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +134 -188
  3. data/CONTRIBUTING.md +53 -47
  4. data/MIT-LICENSE +1 -1
  5. data/README.md +287 -0
  6. data/Rakefile +40 -7
  7. data/SPEC.rdoc +166 -125
  8. data/contrib/LICENSE.md +7 -0
  9. data/contrib/logo.webp +0 -0
  10. data/lib/rack/auth/abstract/handler.rb +3 -1
  11. data/lib/rack/auth/abstract/request.rb +3 -1
  12. data/lib/rack/auth/basic.rb +2 -1
  13. data/lib/rack/auth/digest/md5.rb +1 -131
  14. data/lib/rack/auth/digest/nonce.rb +1 -53
  15. data/lib/rack/auth/digest/params.rb +1 -54
  16. data/lib/rack/auth/digest/request.rb +1 -43
  17. data/lib/rack/auth/digest.rb +256 -0
  18. data/lib/rack/body_proxy.rb +3 -1
  19. data/lib/rack/builder.rb +60 -42
  20. data/lib/rack/cascade.rb +2 -0
  21. data/lib/rack/chunked.rb +16 -13
  22. data/lib/rack/common_logger.rb +24 -20
  23. data/lib/rack/conditional_get.rb +18 -15
  24. data/lib/rack/constants.rb +62 -0
  25. data/lib/rack/content_length.rb +12 -16
  26. data/lib/rack/content_type.rb +8 -5
  27. data/lib/rack/deflater.rb +40 -26
  28. data/lib/rack/directory.rb +12 -9
  29. data/lib/rack/etag.rb +14 -23
  30. data/lib/rack/events.rb +4 -0
  31. data/lib/rack/file.rb +2 -0
  32. data/lib/rack/files.rb +16 -18
  33. data/lib/rack/head.rb +9 -8
  34. data/lib/rack/headers.rb +154 -0
  35. data/lib/rack/lint.rb +740 -649
  36. data/lib/rack/lock.rb +2 -5
  37. data/lib/rack/logger.rb +2 -0
  38. data/lib/rack/media_type.rb +7 -17
  39. data/lib/rack/method_override.rb +5 -1
  40. data/lib/rack/mime.rb +8 -0
  41. data/lib/rack/mock.rb +1 -300
  42. data/lib/rack/mock_request.rb +166 -0
  43. data/lib/rack/mock_response.rb +124 -0
  44. data/lib/rack/multipart/generator.rb +7 -5
  45. data/lib/rack/multipart/parser.rb +119 -160
  46. data/lib/rack/multipart/uploaded_file.rb +4 -0
  47. data/lib/rack/multipart.rb +20 -40
  48. data/lib/rack/null_logger.rb +9 -0
  49. data/lib/rack/query_parser.rb +78 -91
  50. data/lib/rack/recursive.rb +2 -0
  51. data/lib/rack/reloader.rb +0 -2
  52. data/lib/rack/request.rb +190 -95
  53. data/lib/rack/response.rb +131 -61
  54. data/lib/rack/rewindable_input.rb +24 -5
  55. data/lib/rack/runtime.rb +7 -6
  56. data/lib/rack/sendfile.rb +40 -65
  57. data/lib/rack/show_exceptions.rb +15 -2
  58. data/lib/rack/show_status.rb +17 -7
  59. data/lib/rack/static.rb +12 -17
  60. data/lib/rack/tempfile_reaper.rb +15 -4
  61. data/lib/rack/urlmap.rb +4 -2
  62. data/lib/rack/utils.rb +219 -240
  63. data/lib/rack/version.rb +9 -4
  64. data/lib/rack.rb +5 -76
  65. data/rack.gemspec +6 -6
  66. metadata +22 -31
  67. data/README.rdoc +0 -355
  68. data/bin/rackup +0 -5
  69. data/contrib/rack.png +0 -0
  70. data/contrib/rack.svg +0 -150
  71. data/contrib/rack_logo.svg +0 -164
  72. data/lib/rack/core_ext/regexp.rb +0 -14
  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/server.rb +0 -466
  82. data/lib/rack/session/abstract/id.rb +0 -523
  83. data/lib/rack/session/cookie.rb +0 -203
  84. data/lib/rack/session/memcache.rb +0 -10
  85. data/lib/rack/session/pool.rb +0 -90
@@ -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
+ # FIXME: apparently users of MockResponse expect the return value of
49
+ # MockResponse#body to be a string. However, the real response object
50
+ # returns the body as a list.
51
+ #
52
+ # See spec_showstatus.rb:
53
+ #
54
+ # should "not replace existing messages" do
55
+ # ...
56
+ # res.body.should == "foo!"
57
+ # end
58
+ buffer = String.new
59
+
60
+ super.each do |chunk|
61
+ buffer << chunk
62
+ end
63
+
64
+ return buffer
65
+ end
66
+
67
+ def empty?
68
+ [201, 204, 304].include? status
69
+ end
70
+
71
+ def cookie(name)
72
+ cookies.fetch(name, nil)
73
+ end
74
+
75
+ private
76
+
77
+ def parse_cookies_from_header
78
+ cookies = Hash.new
79
+ if headers.has_key? 'set-cookie'
80
+ set_cookie_header = headers.fetch('set-cookie')
81
+ Array(set_cookie_header).each do |header_value|
82
+ header_value.split("\n").each do |cookie|
83
+ cookie_name, cookie_filling = cookie.split('=', 2)
84
+ cookie_attributes = identify_cookie_attributes cookie_filling
85
+ parsed_cookie = CGI::Cookie.new(
86
+ 'name' => cookie_name.strip,
87
+ 'value' => cookie_attributes.fetch('value'),
88
+ 'path' => cookie_attributes.fetch('path', nil),
89
+ 'domain' => cookie_attributes.fetch('domain', nil),
90
+ 'expires' => cookie_attributes.fetch('expires', nil),
91
+ 'secure' => cookie_attributes.fetch('secure', false)
92
+ )
93
+ cookies.store(cookie_name, parsed_cookie)
94
+ end
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,49 +2,52 @@
2
2
 
3
3
  require 'strscan'
4
4
 
5
+ require_relative '../utils'
6
+
5
7
  module Rack
6
8
  module Multipart
7
9
  class MultipartPartLimitError < Errno::EMFILE; end
8
- class MultipartTotalPartLimitError < StandardError; end
9
10
 
10
- class Parser
11
- (require_relative '../core_ext/regexp'; using ::Rack::RegexpExtensions) if RUBY_VERSION < '2.4'
11
+ # Use specific error class when parsing multipart request
12
+ # that ends early.
13
+ class EmptyContentError < ::EOFError; end
14
+
15
+ # Base class for multipart exceptions that do not subclass from
16
+ # other exception classes for backwards compatibility.
17
+ class Error < StandardError; end
18
+
19
+ EOL = "\r\n"
20
+ MULTIPART = %r|\Amultipart/.*boundary=\"?([^\";,]+)\"?|ni
21
+ TOKEN = /[^\s()<>,;:\\"\/\[\]?=]+/
22
+ CONDISP = /Content-Disposition:\s*#{TOKEN}\s*/i
23
+ VALUE = /"(?:\\"|[^"])*"|#{TOKEN}/
24
+ BROKEN = /^#{CONDISP}.*;\s*filename=(#{VALUE})/i
25
+ MULTIPART_CONTENT_TYPE = /Content-Type: (.*)#{EOL}/ni
26
+ MULTIPART_CONTENT_DISPOSITION = /Content-Disposition:.*;\s*name=(#{VALUE})/ni
27
+ MULTIPART_CONTENT_ID = /Content-ID:\s*([^#{EOL}]*)/ni
28
+ # Updated definitions from RFC 2231
29
+ ATTRIBUTE_CHAR = %r{[^ \t\v\n\r)(><@,;:\\"/\[\]?='*%]}
30
+ ATTRIBUTE = /#{ATTRIBUTE_CHAR}+/
31
+ SECTION = /\*[0-9]+/
32
+ REGULAR_PARAMETER_NAME = /#{ATTRIBUTE}#{SECTION}?/
33
+ REGULAR_PARAMETER = /(#{REGULAR_PARAMETER_NAME})=(#{VALUE})/
34
+ EXTENDED_OTHER_NAME = /#{ATTRIBUTE}\*[1-9][0-9]*\*/
35
+ EXTENDED_OTHER_VALUE = /%[0-9a-fA-F]{2}|#{ATTRIBUTE_CHAR}/
36
+ EXTENDED_OTHER_PARAMETER = /(#{EXTENDED_OTHER_NAME})=(#{EXTENDED_OTHER_VALUE}*)/
37
+ EXTENDED_INITIAL_NAME = /#{ATTRIBUTE}(?:\*0)?\*/
38
+ EXTENDED_INITIAL_VALUE = /[a-zA-Z0-9\-]*'[a-zA-Z0-9\-]*'#{EXTENDED_OTHER_VALUE}*/
39
+ EXTENDED_INITIAL_PARAMETER = /(#{EXTENDED_INITIAL_NAME})=(#{EXTENDED_INITIAL_VALUE})/
40
+ EXTENDED_PARAMETER = /#{EXTENDED_INITIAL_PARAMETER}|#{EXTENDED_OTHER_PARAMETER}/
41
+ DISPPARM = /;\s*(?:#{REGULAR_PARAMETER}|#{EXTENDED_PARAMETER})\s*/
42
+ RFC2183 = /^#{CONDISP}(#{DISPPARM})+$/i
12
43
 
44
+ class Parser
13
45
  BUFSIZE = 1_048_576
14
46
  TEXT_PLAIN = "text/plain"
15
47
  TEMPFILE_FACTORY = lambda { |filename, content_type|
16
- extension = ::File.extname(filename.gsub("\0", '%00'))[0, 129]
17
-
18
- Tempfile.new(["RackMultipart", extension])
48
+ Tempfile.new(["RackMultipart", ::File.extname(filename.gsub("\0", '%00'))])
19
49
  }
20
50
 
21
- BOUNDARY_REGEX = /\A([^\n]*(?:\n|\Z))/
22
-
23
- BOUNDARY_START_LIMIT = 16 * 1024
24
- private_constant :BOUNDARY_START_LIMIT
25
-
26
- MIME_HEADER_BYTESIZE_LIMIT = 64 * 1024
27
- private_constant :MIME_HEADER_BYTESIZE_LIMIT
28
-
29
- env_int = lambda do |key, val|
30
- if str_val = ENV[key]
31
- begin
32
- val = Integer(str_val, 10)
33
- rescue ArgumentError
34
- raise ArgumentError, "non-integer value provided for environment variable #{key}"
35
- end
36
- end
37
-
38
- val
39
- end
40
-
41
- BUFFERED_UPLOAD_BYTESIZE_LIMIT = env_int.call("RACK_MULTIPART_BUFFERED_UPLOAD_BYTESIZE_LIMIT", 16 * 1024 * 1024)
42
- private_constant :BUFFERED_UPLOAD_BYTESIZE_LIMIT
43
-
44
- bytesize_limit = env_int.call("RACK_MULTIPART_PARSER_BYTESIZE_LIMIT", 10 * 1024 * 1024 * 1024)
45
- PARSER_BYTESIZE_LIMIT = bytesize_limit > 0 ? bytesize_limit : nil
46
- private_constant :PARSER_BYTESIZE_LIMIT
47
-
48
51
  class BoundedIO # :nodoc:
49
52
  def initialize(io, content_length)
50
53
  @io = io
@@ -66,16 +69,12 @@ module Rack
66
69
  if str
67
70
  @cursor += str.bytesize
68
71
  else
69
- # Raise an error for mismatching Content-Length and actual contents
72
+ # Raise an error for mismatching content-length and actual contents
70
73
  raise EOFError, "bad content body"
71
74
  end
72
75
 
73
76
  str
74
77
  end
75
-
76
- def rewind
77
- @io.rewind
78
- end
79
78
  end
80
79
 
81
80
  MultipartInfo = Struct.new :params, :tmp_files
@@ -85,15 +84,7 @@ module Rack
85
84
  return unless content_type
86
85
  data = content_type.match(MULTIPART)
87
86
  return unless data
88
-
89
- unless data[1].empty?
90
- raise EOFError, "whitespace between boundary parameter name and equal sign"
91
- end
92
- if data.post_match =~ /boundary\s*=/i
93
- raise EOFError, "multiple boundary parameters found in multipart content type"
94
- end
95
-
96
- data[2]
87
+ data[1]
97
88
  end
98
89
 
99
90
  def self.parse(io, content_length, content_type, tmpfile, bufsize, qp)
@@ -102,22 +93,17 @@ module Rack
102
93
  boundary = parse_boundary content_type
103
94
  return EMPTY unless boundary
104
95
 
105
- if PARSER_BYTESIZE_LIMIT && content_length && content_length > PARSER_BYTESIZE_LIMIT
106
- raise EOFError, "multipart Content-Length #{content_length} exceeds limit of #{PARSER_BYTESIZE_LIMIT} bytes"
96
+ if boundary.length > 70
97
+ # RFC 1521 Section 7.2.1 imposes a 70 character maximum for the boundary.
98
+ # Most clients use no more than 55 characters.
99
+ raise Error, "multipart boundary size too large (#{boundary.length} characters)"
107
100
  end
108
101
 
109
102
  io = BoundedIO.new(io, content_length) if content_length
110
- outbuf = String.new
111
103
 
112
104
  parser = new(boundary, tmpfile, bufsize, qp)
113
- parser.on_read io.read(bufsize, outbuf)
114
-
115
- loop do
116
- break if parser.state == :DONE
117
- parser.on_read io.read(bufsize, outbuf)
118
- end
105
+ parser.parse(io)
119
106
 
120
- io.rewind
121
107
  parser.result
122
108
  end
123
109
 
@@ -180,7 +166,7 @@ module Rack
180
166
 
181
167
  @mime_parts[mime_index] = klass.new(body, head, filename, content_type, name)
182
168
 
183
- check_part_limits
169
+ check_open_files
184
170
  end
185
171
 
186
172
  def on_mime_body(mime_index, content)
@@ -192,23 +178,13 @@ module Rack
192
178
 
193
179
  private
194
180
 
195
- def check_part_limits
196
- file_limit = Utils.multipart_file_limit
197
- part_limit = Utils.multipart_total_part_limit
198
-
199
- if file_limit && file_limit > 0
200
- if @open_files >= file_limit
181
+ def check_open_files
182
+ if Utils.multipart_part_limit > 0
183
+ if @open_files >= Utils.multipart_part_limit
201
184
  @mime_parts.each(&:close)
202
185
  raise MultipartPartLimitError, 'Maximum file multiparts in content reached'
203
186
  end
204
187
  end
205
-
206
- if part_limit && part_limit > 0
207
- if @mime_parts.size >= part_limit
208
- @mime_parts.each(&:close)
209
- raise MultipartTotalPartLimitError, 'Maximum total multiparts in content reached'
210
- end
211
- end
212
188
  end
213
189
  end
214
190
 
@@ -217,42 +193,46 @@ module Rack
217
193
  def initialize(boundary, tempfile, bufsize, query_parser)
218
194
  @query_parser = query_parser
219
195
  @params = query_parser.make_params
220
- @boundary = "--#{boundary}"
221
196
  @bufsize = bufsize
222
197
 
223
- @full_boundary = @boundary
224
- @end_boundary = @boundary + '--'
225
198
  @state = :FAST_FORWARD
226
199
  @mime_index = 0
227
- @body_retained = nil
228
- @retained_size = 0
229
- @total_bytes_read = (0 if PARSER_BYTESIZE_LIMIT)
230
200
  @collector = Collector.new tempfile
231
201
 
232
202
  @sbuf = StringScanner.new("".dup)
233
- @body_regex = /(?:#{EOL})?#{Regexp.quote(@boundary)}(?:#{EOL}|--)/m
234
- @end_boundary_size = boundary.bytesize + 6 # (-- at start, -- at finish, EOL at end)
235
- @rx_max_size = EOL.size + @boundary.bytesize + [EOL.size, '--'.size].max
203
+ @body_regex = /(?:#{EOL}|\A)--#{Regexp.quote(boundary)}(?:#{EOL}|--)/m
204
+ @rx_max_size = boundary.bytesize + 6 # (\r\n-- at start, either \r\n or -- at finish)
236
205
  @head_regex = /(.*?#{EOL})#{EOL}/m
237
206
  end
238
207
 
239
- def on_read(content)
240
- handle_empty_content!(content)
241
- if @total_bytes_read
242
- @total_bytes_read += content.bytesize
243
- if @total_bytes_read > PARSER_BYTESIZE_LIMIT
244
- raise EOFError, "multipart upload exceeds limit of #{PARSER_BYTESIZE_LIMIT} bytes"
245
- end
208
+ def parse(io)
209
+ outbuf = String.new
210
+ read_data(io, outbuf)
211
+
212
+ loop do
213
+ status =
214
+ case @state
215
+ when :FAST_FORWARD
216
+ handle_fast_forward
217
+ when :CONSUME_TOKEN
218
+ handle_consume_token
219
+ when :MIME_HEAD
220
+ handle_mime_head
221
+ when :MIME_BODY
222
+ handle_mime_body
223
+ else # when :DONE
224
+ return
225
+ end
226
+
227
+ read_data(io, outbuf) if status == :want_read
246
228
  end
247
- @sbuf.concat content
248
- run_parser
249
229
  end
250
230
 
251
231
  def result
252
232
  @collector.each do |part|
253
233
  part.get_data do |data|
254
234
  tag_multipart_encoding(part.filename, part.content_type, part.name, data)
255
- @query_parser.normalize_params(@params, part.name, data, @query_parser.param_depth_limit)
235
+ @query_parser.normalize_params(@params, part.name, data)
256
236
  end
257
237
  end
258
238
  MultipartInfo.new @params.to_params_hash, @collector.find_all(&:file?).map(&:body)
@@ -260,40 +240,38 @@ module Rack
260
240
 
261
241
  private
262
242
 
263
- def run_parser
264
- loop do
265
- case @state
266
- when :FAST_FORWARD
267
- break if handle_fast_forward == :want_read
268
- when :CONSUME_TOKEN
269
- break if handle_consume_token == :want_read
270
- when :MIME_HEAD
271
- break if handle_mime_head == :want_read
272
- when :MIME_BODY
273
- break if handle_mime_body == :want_read
274
- when :DONE
275
- break
276
- end
277
- end
243
+ def dequote(str) # From WEBrick::HTTPUtils
244
+ ret = (/\A"(.*)"\Z/ =~ str) ? $1 : str.dup
245
+ ret.gsub!(/\\(.)/, "\\1")
246
+ ret
278
247
  end
279
248
 
280
- def handle_fast_forward
281
- tok = consume_boundary
282
-
283
- if tok == :END_BOUNDARY && @sbuf.pos == @end_boundary_size && @sbuf.eos?
284
- # stop parsing a buffer if a buffer is only an end boundary.
285
- @state = :DONE
286
- elsif tok
287
- @state = :MIME_HEAD
288
- else
289
- raise EOFError, "bad content body" if @sbuf.rest_size >= @bufsize
290
-
291
- # We raise if we don't find the multipart boundary, to avoid unbounded memory
292
- # buffering. Note that the actual limit is the higher of 16KB and the buffer size (1MB by default)
293
- raise EOFError, "multipart boundary not found within limit" if @sbuf.string.bytesize > BOUNDARY_START_LIMIT
249
+ def read_data(io, outbuf)
250
+ content = io.read(@bufsize, outbuf)
251
+ handle_empty_content!(content)
252
+ @sbuf.concat(content)
253
+ end
294
254
 
295
- # no boundary found, keep reading data
296
- return :want_read
255
+ # This handles the initial parser state. We read until we find the starting
256
+ # boundary, then we can transition to the next state. If we find the ending
257
+ # boundary, this is an invalid multipart upload, but keep scanning for opening
258
+ # boundary in that case. If no boundary found, we need to keep reading data
259
+ # and retry. It's highly unlikely the initial read will not consume the
260
+ # boundary. The client would have to deliberately craft a response
261
+ # with the opening boundary beyond the buffer size for that to happen.
262
+ def handle_fast_forward
263
+ while true
264
+ case consume_boundary
265
+ when :BOUNDARY
266
+ # found opening boundary, transition to next state
267
+ @state = :MIME_HEAD
268
+ return
269
+ when :END_BOUNDARY
270
+ # invalid multipart upload, but retry for opening boundary
271
+ else
272
+ # no boundary found, keep reading data
273
+ return :want_read
274
+ end
297
275
  end
298
276
  end
299
277
 
@@ -312,7 +290,7 @@ module Rack
312
290
  head = @sbuf[1]
313
291
  content_type = head[MULTIPART_CONTENT_TYPE, 1]
314
292
  if name = head[MULTIPART_CONTENT_DISPOSITION, 1]
315
- name = Rack::Auth::Digest::Params::dequote(name)
293
+ name = dequote(name)
316
294
  else
317
295
  name = head[MULTIPART_CONTENT_ID, 1]
318
296
  end
@@ -323,30 +301,16 @@ module Rack
323
301
  name = filename || "#{content_type || TEXT_PLAIN}[]".dup
324
302
  end
325
303
 
326
- # Mime part head data is retained for both TempfilePart and BufferPart
327
- # for the entireity of the parse, even though it isn't used for BufferPart.
328
- update_retained_size(head.bytesize)
329
-
330
- # If a filename is given, a TempfilePart will be used, so the body will
331
- # not be buffered in memory. However, if a filename is not given, a BufferPart
332
- # will be used, and the body will be buffered in memory.
333
- @body_retained = !filename
334
-
335
304
  @collector.on_mime_head @mime_index, head, filename, content_type, name
336
305
  @state = :MIME_BODY
337
306
  else
338
- # We raise if the mime part header is too large, to avoid unbounded memory
339
- # buffering. Note that the actual limit is the higher of 64KB and the buffer size (1MB by default)
340
- raise EOFError, "multipart mime part header too large" if @sbuf.rest.bytesize > MIME_HEADER_BYTESIZE_LIMIT
341
-
342
- return :want_read
307
+ :want_read
343
308
  end
344
309
  end
345
310
 
346
311
  def handle_mime_body
347
312
  if (body_with_boundary = @sbuf.check_until(@body_regex)) # check but do not advance the pointer yet
348
313
  body = body_with_boundary.sub(/#{@body_regex}\z/m, '') # remove the boundary from the string
349
- update_retained_size(body.bytesize) if @body_retained
350
314
  @collector.on_mime_body @mime_index, body
351
315
  @sbuf.pos += body.length + 2 # skip \r\n after the content
352
316
  @state = :CONSUME_TOKEN
@@ -355,9 +319,7 @@ module Rack
355
319
  # Save what we have so far
356
320
  if @rx_max_size < @sbuf.rest_size
357
321
  delta = @sbuf.rest_size - @rx_max_size
358
- body = @sbuf.peek(delta)
359
- update_retained_size(body.bytesize) if @body_retained
360
- @collector.on_mime_body @mime_index, body
322
+ @collector.on_mime_body @mime_index, @sbuf.peek(delta)
361
323
  @sbuf.pos += delta
362
324
  @sbuf.string = @sbuf.rest
363
325
  end
@@ -365,26 +327,16 @@ module Rack
365
327
  end
366
328
  end
367
329
 
368
- def full_boundary; @full_boundary; end
369
-
370
- def update_retained_size(size)
371
- @retained_size += size
372
- if @retained_size > BUFFERED_UPLOAD_BYTESIZE_LIMIT
373
- raise EOFError, "multipart data over retained size limit"
374
- end
375
- end
376
-
377
330
  # Scan until the we find the start or end of the boundary.
378
331
  # If we find it, return the appropriate symbol for the start or
379
332
  # end of the boundary. If we don't find the start or end of the
380
333
  # boundary, clear the buffer and return nil.
381
334
  def consume_boundary
382
- while read_buffer = @sbuf.scan_until(BOUNDARY_REGEX)
383
- case read_buffer.strip
384
- when full_boundary then return :BOUNDARY
385
- when @end_boundary then return :END_BOUNDARY
386
- end
387
- return if @sbuf.eos?
335
+ if read_buffer = @sbuf.scan_until(@body_regex)
336
+ read_buffer.end_with?(EOL) ? :BOUNDARY : :END_BOUNDARY
337
+ else
338
+ @sbuf.terminate
339
+ nil
388
340
  end
389
341
  end
390
342
 
@@ -394,10 +346,10 @@ module Rack
394
346
  when RFC2183
395
347
  params = Hash[*head.scan(DISPPARM).flat_map(&:compact)]
396
348
 
397
- if filename = params['filename']
398
- filename = $1 if filename =~ /^"(.*)"$/
399
- elsif filename = params['filename*']
349
+ if filename = params['filename*']
400
350
  encoding, _, filename = filename.split("'", 3)
351
+ elsif filename = params['filename']
352
+ filename = $1 if filename =~ /^"(.*)"$/
401
353
  end
402
354
  when BROKEN
403
355
  filename = $1
@@ -424,6 +376,7 @@ module Rack
424
376
  end
425
377
 
426
378
  CHARSET = "charset"
379
+ deprecate_constant :CHARSET
427
380
 
428
381
  def tag_multipart_encoding(filename, content_type, name, body)
429
382
  name = name.to_s
@@ -444,7 +397,13 @@ module Rack
444
397
  k.strip!
445
398
  v.strip!
446
399
  v = v[1..-2] if v.start_with?('"') && v.end_with?('"')
447
- encoding = Encoding.find v if k == CHARSET
400
+ if k == "charset"
401
+ encoding = begin
402
+ Encoding.find v
403
+ rescue ArgumentError
404
+ Encoding::BINARY
405
+ end
406
+ end
448
407
  end
449
408
  end
450
409
  end
@@ -455,7 +414,7 @@ module Rack
455
414
 
456
415
  def handle_empty_content!(content)
457
416
  if content.nil? || content.empty?
458
- raise EOFError
417
+ raise EmptyContentError
459
418
  end
460
419
  end
461
420
  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