rack 2.2.25 → 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 -206
  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 +122 -193
  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,56 +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
- LINE_END = /\n/
24
- private_constant :LINE_END
25
-
26
- BOUNDARY_START_LIMIT = 16 * 1024
27
- private_constant :BOUNDARY_START_LIMIT
28
-
29
- MIME_HEADER_BYTESIZE_LIMIT = 64 * 1024
30
-
31
- OBS_UNFOLD = /\r\n([ \t])/
32
-
33
- private_constant :OBS_UNFOLD
34
- private_constant :MIME_HEADER_BYTESIZE_LIMIT
35
-
36
- env_int = lambda do |key, val|
37
- if str_val = ENV[key]
38
- begin
39
- val = Integer(str_val, 10)
40
- rescue ArgumentError
41
- raise ArgumentError, "non-integer value provided for environment variable #{key}"
42
- end
43
- end
44
-
45
- val
46
- end
47
-
48
- BUFFERED_UPLOAD_BYTESIZE_LIMIT = env_int.call("RACK_MULTIPART_BUFFERED_UPLOAD_BYTESIZE_LIMIT", 16 * 1024 * 1024)
49
- private_constant :BUFFERED_UPLOAD_BYTESIZE_LIMIT
50
-
51
- bytesize_limit = env_int.call("RACK_MULTIPART_PARSER_BYTESIZE_LIMIT", 10 * 1024 * 1024 * 1024)
52
- PARSER_BYTESIZE_LIMIT = bytesize_limit > 0 ? bytesize_limit : nil
53
- private_constant :PARSER_BYTESIZE_LIMIT
54
-
55
51
  class BoundedIO # :nodoc:
56
52
  def initialize(io, content_length)
57
53
  @io = io
@@ -73,16 +69,12 @@ module Rack
73
69
  if str
74
70
  @cursor += str.bytesize
75
71
  else
76
- # Raise an error for mismatching Content-Length and actual contents
72
+ # Raise an error for mismatching content-length and actual contents
77
73
  raise EOFError, "bad content body"
78
74
  end
79
75
 
80
76
  str
81
77
  end
82
-
83
- def rewind
84
- @io.rewind
85
- end
86
78
  end
87
79
 
88
80
  MultipartInfo = Struct.new :params, :tmp_files
@@ -92,19 +84,7 @@ module Rack
92
84
  return unless content_type
93
85
  data = content_type.match(MULTIPART)
94
86
  return unless data
95
-
96
- unless data[1].empty?
97
- raise EOFError, "whitespace between boundary parameter name and equal sign"
98
- end
99
- # Count across the whole header rather than only after the match: the
100
- # lazy prefix in MULTIPART can skip a first boundary parameter whose
101
- # value is empty or starts with a quote, semicolon, or comma. Also
102
- # count RFC 2231 boundary* parameters, which some parsers prefer.
103
- if content_type.scan(/boundary(?:\*\d*\*?)?\s*=/i).size > 1
104
- raise EOFError, "multiple boundary parameters found in multipart content type"
105
- end
106
-
107
- data[2]
87
+ data[1]
108
88
  end
109
89
 
110
90
  def self.parse(io, content_length, content_type, tmpfile, bufsize, qp)
@@ -113,22 +93,17 @@ module Rack
113
93
  boundary = parse_boundary content_type
114
94
  return EMPTY unless boundary
115
95
 
116
- if PARSER_BYTESIZE_LIMIT && content_length && content_length > PARSER_BYTESIZE_LIMIT
117
- 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)"
118
100
  end
119
101
 
120
102
  io = BoundedIO.new(io, content_length) if content_length
121
- outbuf = String.new
122
103
 
123
104
  parser = new(boundary, tmpfile, bufsize, qp)
124
- parser.on_read io.read(bufsize, outbuf)
105
+ parser.parse(io)
125
106
 
126
- loop do
127
- break if parser.state == :DONE
128
- parser.on_read io.read(bufsize, outbuf)
129
- end
130
-
131
- io.rewind
132
107
  parser.result
133
108
  end
134
109
 
@@ -191,7 +166,7 @@ module Rack
191
166
 
192
167
  @mime_parts[mime_index] = klass.new(body, head, filename, content_type, name)
193
168
 
194
- check_part_limits
169
+ check_open_files
195
170
  end
196
171
 
197
172
  def on_mime_body(mime_index, content)
@@ -203,23 +178,13 @@ module Rack
203
178
 
204
179
  private
205
180
 
206
- def check_part_limits
207
- file_limit = Utils.multipart_file_limit
208
- part_limit = Utils.multipart_total_part_limit
209
-
210
- if file_limit && file_limit > 0
211
- 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
212
184
  @mime_parts.each(&:close)
213
185
  raise MultipartPartLimitError, 'Maximum file multiparts in content reached'
214
186
  end
215
187
  end
216
-
217
- if part_limit && part_limit > 0
218
- if @mime_parts.size >= part_limit
219
- @mime_parts.each(&:close)
220
- raise MultipartTotalPartLimitError, 'Maximum total multiparts in content reached'
221
- end
222
- end
223
188
  end
224
189
  end
225
190
 
@@ -228,42 +193,46 @@ module Rack
228
193
  def initialize(boundary, tempfile, bufsize, query_parser)
229
194
  @query_parser = query_parser
230
195
  @params = query_parser.make_params
231
- @boundary = "--#{boundary}"
232
196
  @bufsize = bufsize
233
197
 
234
- @full_boundary = @boundary
235
- @end_boundary = @boundary + '--'
236
198
  @state = :FAST_FORWARD
237
199
  @mime_index = 0
238
- @body_retained = nil
239
- @retained_size = 0
240
- @total_bytes_read = (0 if PARSER_BYTESIZE_LIMIT)
241
200
  @collector = Collector.new tempfile
242
201
 
243
202
  @sbuf = StringScanner.new("".dup)
244
- @body_regex = /(?:#{EOL})?#{Regexp.quote(@boundary)}(?:#{EOL}|--)/m
245
- @end_boundary_size = boundary.bytesize + 6 # (-- at start, -- at finish, EOL at end)
246
- @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)
247
205
  @head_regex = /(.*?#{EOL})#{EOL}/m
248
206
  end
249
207
 
250
- def on_read(content)
251
- handle_empty_content!(content)
252
- if @total_bytes_read
253
- @total_bytes_read += content.bytesize
254
- if @total_bytes_read > PARSER_BYTESIZE_LIMIT
255
- raise EOFError, "multipart upload exceeds limit of #{PARSER_BYTESIZE_LIMIT} bytes"
256
- 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
257
228
  end
258
- @sbuf.concat content
259
- run_parser
260
229
  end
261
230
 
262
231
  def result
263
232
  @collector.each do |part|
264
233
  part.get_data do |data|
265
234
  tag_multipart_encoding(part.filename, part.content_type, part.name, data)
266
- @query_parser.normalize_params(@params, part.name, data, @query_parser.param_depth_limit)
235
+ @query_parser.normalize_params(@params, part.name, data)
267
236
  end
268
237
  end
269
238
  MultipartInfo.new @params.to_params_hash, @collector.find_all(&:file?).map(&:body)
@@ -271,40 +240,38 @@ module Rack
271
240
 
272
241
  private
273
242
 
274
- def run_parser
275
- loop do
276
- case @state
277
- when :FAST_FORWARD
278
- break if handle_fast_forward == :want_read
279
- when :CONSUME_TOKEN
280
- break if handle_consume_token == :want_read
281
- when :MIME_HEAD
282
- break if handle_mime_head == :want_read
283
- when :MIME_BODY
284
- break if handle_mime_body == :want_read
285
- when :DONE
286
- break
287
- end
288
- end
243
+ def dequote(str) # From WEBrick::HTTPUtils
244
+ ret = (/\A"(.*)"\Z/ =~ str) ? $1 : str.dup
245
+ ret.gsub!(/\\(.)/, "\\1")
246
+ ret
289
247
  end
290
248
 
291
- def handle_fast_forward
292
- tok = consume_boundary
293
-
294
- if tok == :END_BOUNDARY && @sbuf.pos == @end_boundary_size && @sbuf.eos?
295
- # stop parsing a buffer if a buffer is only an end boundary.
296
- @state = :DONE
297
- elsif tok
298
- @state = :MIME_HEAD
299
- else
300
- raise EOFError, "bad content body" if @sbuf.rest_size >= @bufsize
301
-
302
- # We raise if we don't find the multipart boundary, to avoid unbounded memory
303
- # buffering. Note that the actual limit is the higher of 16KB and the buffer size (1MB by default)
304
- 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
305
254
 
306
- # no boundary found, keep reading data
307
- 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
308
275
  end
309
276
  end
310
277
 
@@ -321,47 +288,29 @@ module Rack
321
288
  def handle_mime_head
322
289
  if @sbuf.scan_until(@head_regex)
323
290
  head = @sbuf[1]
324
- # Implement OBS unfolding (RFC 5322 Section 2.2.3). Done on a copy
325
- # because content type, disposition and filename are extracted by regex.
326
- unfolded_head = head.gsub(OBS_UNFOLD, '\\1')
327
- content_type = unfolded_head[MULTIPART_CONTENT_TYPE, 1]
328
- if name = unfolded_head[MULTIPART_CONTENT_DISPOSITION, 1]
329
- name = Rack::Auth::Digest::Params::dequote(name)
291
+ content_type = head[MULTIPART_CONTENT_TYPE, 1]
292
+ if name = head[MULTIPART_CONTENT_DISPOSITION, 1]
293
+ name = dequote(name)
330
294
  else
331
- name = unfolded_head[MULTIPART_CONTENT_ID, 1]
295
+ name = head[MULTIPART_CONTENT_ID, 1]
332
296
  end
333
297
 
334
- filename = get_filename(unfolded_head)
298
+ filename = get_filename(head)
335
299
 
336
300
  if name.nil? || name.empty?
337
301
  name = filename || "#{content_type || TEXT_PLAIN}[]".dup
338
302
  end
339
303
 
340
- # Mime part head data is retained for both TempfilePart and BufferPart
341
- # for the entireity of the parse, even though it isn't used for BufferPart.
342
- update_retained_size(head.bytesize)
343
-
344
- # If a filename is given, a TempfilePart will be used, so the body will
345
- # not be buffered in memory. However, if a filename is not given, a BufferPart
346
- # will be used, and the body will be buffered in memory.
347
- @body_retained = !filename
348
-
349
304
  @collector.on_mime_head @mime_index, head, filename, content_type, name
350
305
  @state = :MIME_BODY
351
306
  else
352
- # We raise if the mime part header is too large, to avoid unbounded memory
353
- # buffering. Note that the actual limit is the higher of 64KB and the buffer size (1MB by default)
354
- raise EOFError, "multipart mime part header too large" if @sbuf.rest.bytesize > MIME_HEADER_BYTESIZE_LIMIT
355
-
356
- @sbuf.string = @sbuf.rest
357
- return :want_read
307
+ :want_read
358
308
  end
359
309
  end
360
310
 
361
311
  def handle_mime_body
362
312
  if (body_with_boundary = @sbuf.check_until(@body_regex)) # check but do not advance the pointer yet
363
313
  body = body_with_boundary.sub(/#{@body_regex}\z/m, '') # remove the boundary from the string
364
- update_retained_size(body.bytesize) if @body_retained
365
314
  @collector.on_mime_body @mime_index, body
366
315
  @sbuf.pos += body.length + 2 # skip \r\n after the content
367
316
  @state = :CONSUME_TOKEN
@@ -370,51 +319,24 @@ module Rack
370
319
  # Save what we have so far
371
320
  if @rx_max_size < @sbuf.rest_size
372
321
  delta = @sbuf.rest_size - @rx_max_size
373
- body = @sbuf.peek(delta)
374
- update_retained_size(body.bytesize) if @body_retained
375
- @collector.on_mime_body @mime_index, body
322
+ @collector.on_mime_body @mime_index, @sbuf.peek(delta)
376
323
  @sbuf.pos += delta
324
+ @sbuf.string = @sbuf.rest
377
325
  end
378
- @sbuf.string = @sbuf.rest
379
326
  :want_read
380
327
  end
381
328
  end
382
329
 
383
- def full_boundary; @full_boundary; end
384
-
385
- def update_retained_size(size)
386
- @retained_size += size
387
- if @retained_size > BUFFERED_UPLOAD_BYTESIZE_LIMIT
388
- raise EOFError, "multipart data over retained size limit"
389
- end
390
- end
391
-
392
330
  # Scan until the we find the start or end of the boundary.
393
331
  # If we find it, return the appropriate symbol for the start or
394
332
  # end of the boundary. If we don't find the start or end of the
395
333
  # boundary, clear the buffer and return nil.
396
334
  def consume_boundary
397
- while read_buffer = scan_line
398
- case read_buffer.strip
399
- when full_boundary then return :BOUNDARY
400
- when @end_boundary then return :END_BOUNDARY
401
- end
402
- return if @sbuf.eos?
403
- end
404
- end
405
-
406
- # Return the next line of the buffer including its trailing "\n", or the
407
- # rest of the buffer if it contains no "\n", and advance past it. This is
408
- # equivalent to `@sbuf.scan_until(BOUNDARY_REGEX)`, but searching for the
409
- # "\n" directly avoids the regexp engine's backtracking stack, which grows
410
- # by one entry per byte matched by `[^\n]*` (tens of MB for a 1 MB line).
411
- def scan_line
412
- if line = @sbuf.scan_until(LINE_END)
413
- line
335
+ if read_buffer = @sbuf.scan_until(@body_regex)
336
+ read_buffer.end_with?(EOL) ? :BOUNDARY : :END_BOUNDARY
414
337
  else
415
- line = @sbuf.rest
416
338
  @sbuf.terminate
417
- line
339
+ nil
418
340
  end
419
341
  end
420
342
 
@@ -424,10 +346,10 @@ module Rack
424
346
  when RFC2183
425
347
  params = Hash[*head.scan(DISPPARM).flat_map(&:compact)]
426
348
 
427
- if filename = params['filename']
428
- filename = $1 if filename =~ /^"(.*)"$/
429
- elsif filename = params['filename*']
349
+ if filename = params['filename*']
430
350
  encoding, _, filename = filename.split("'", 3)
351
+ elsif filename = params['filename']
352
+ filename = $1 if filename =~ /^"(.*)"$/
431
353
  end
432
354
  when BROKEN
433
355
  filename = $1
@@ -454,6 +376,7 @@ module Rack
454
376
  end
455
377
 
456
378
  CHARSET = "charset"
379
+ deprecate_constant :CHARSET
457
380
 
458
381
  def tag_multipart_encoding(filename, content_type, name, body)
459
382
  name = name.to_s
@@ -474,7 +397,13 @@ module Rack
474
397
  k.strip!
475
398
  v.strip!
476
399
  v = v[1..-2] if v.start_with?('"') && v.end_with?('"')
477
- 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
478
407
  end
479
408
  end
480
409
  end
@@ -485,7 +414,7 @@ module Rack
485
414
 
486
415
  def handle_empty_content!(content)
487
416
  if content.nil? || content.empty?
488
- raise EOFError
417
+ raise EmptyContentError
489
418
  end
490
419
  end
491
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