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