rack 2.2.19 → 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.

Potentially problematic release.


This version of rack might be problematic. Click here for more details.

Files changed (85) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +134 -154
  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 +9 -3
  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 +15 -17
  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 +121 -139
  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 -89
  50. data/lib/rack/recursive.rb +2 -0
  51. data/lib/rack/reloader.rb +0 -2
  52. data/lib/rack/request.rb +189 -91
  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 +31 -26
  57. data/lib/rack/show_exceptions.rb +15 -2
  58. data/lib/rack/show_status.rb +17 -7
  59. data/lib/rack/static.rb +9 -10
  60. data/lib/rack/tempfile_reaper.rb +15 -4
  61. data/lib/rack/urlmap.rb +4 -2
  62. data/lib/rack/utils.rb +212 -202
  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,45 +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
51
  class BoundedIO # :nodoc:
45
52
  def initialize(io, content_length)
46
53
  @io = io
@@ -62,16 +69,12 @@ module Rack
62
69
  if str
63
70
  @cursor += str.bytesize
64
71
  else
65
- # Raise an error for mismatching Content-Length and actual contents
72
+ # Raise an error for mismatching content-length and actual contents
66
73
  raise EOFError, "bad content body"
67
74
  end
68
75
 
69
76
  str
70
77
  end
71
-
72
- def rewind
73
- @io.rewind
74
- end
75
78
  end
76
79
 
77
80
  MultipartInfo = Struct.new :params, :tmp_files
@@ -90,18 +93,17 @@ module Rack
90
93
  boundary = parse_boundary content_type
91
94
  return EMPTY unless boundary
92
95
 
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)"
100
+ end
101
+
93
102
  io = BoundedIO.new(io, content_length) if content_length
94
- outbuf = String.new
95
103
 
96
104
  parser = new(boundary, tmpfile, bufsize, qp)
97
- parser.on_read io.read(bufsize, outbuf)
98
-
99
- loop do
100
- break if parser.state == :DONE
101
- parser.on_read io.read(bufsize, outbuf)
102
- end
105
+ parser.parse(io)
103
106
 
104
- io.rewind
105
107
  parser.result
106
108
  end
107
109
 
@@ -164,7 +166,7 @@ module Rack
164
166
 
165
167
  @mime_parts[mime_index] = klass.new(body, head, filename, content_type, name)
166
168
 
167
- check_part_limits
169
+ check_open_files
168
170
  end
169
171
 
170
172
  def on_mime_body(mime_index, content)
@@ -176,23 +178,13 @@ module Rack
176
178
 
177
179
  private
178
180
 
179
- def check_part_limits
180
- file_limit = Utils.multipart_file_limit
181
- part_limit = Utils.multipart_total_part_limit
182
-
183
- if file_limit && file_limit > 0
184
- 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
185
184
  @mime_parts.each(&:close)
186
185
  raise MultipartPartLimitError, 'Maximum file multiparts in content reached'
187
186
  end
188
187
  end
189
-
190
- if part_limit && part_limit > 0
191
- if @mime_parts.size >= part_limit
192
- @mime_parts.each(&:close)
193
- raise MultipartTotalPartLimitError, 'Maximum total multiparts in content reached'
194
- end
195
- end
196
188
  end
197
189
  end
198
190
 
@@ -201,35 +193,46 @@ module Rack
201
193
  def initialize(boundary, tempfile, bufsize, query_parser)
202
194
  @query_parser = query_parser
203
195
  @params = query_parser.make_params
204
- @boundary = "--#{boundary}"
205
196
  @bufsize = bufsize
206
197
 
207
- @full_boundary = @boundary
208
- @end_boundary = @boundary + '--'
209
198
  @state = :FAST_FORWARD
210
199
  @mime_index = 0
211
- @body_retained = nil
212
- @retained_size = 0
213
200
  @collector = Collector.new tempfile
214
201
 
215
202
  @sbuf = StringScanner.new("".dup)
216
- @body_regex = /(?:#{EOL})?#{Regexp.quote(@boundary)}(?:#{EOL}|--)/m
217
- @end_boundary_size = boundary.bytesize + 6 # (-- at start, -- at finish, EOL at end)
218
- @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)
219
205
  @head_regex = /(.*?#{EOL})#{EOL}/m
220
206
  end
221
207
 
222
- def on_read(content)
223
- handle_empty_content!(content)
224
- @sbuf.concat content
225
- run_parser
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
228
+ end
226
229
  end
227
230
 
228
231
  def result
229
232
  @collector.each do |part|
230
233
  part.get_data do |data|
231
234
  tag_multipart_encoding(part.filename, part.content_type, part.name, data)
232
- @query_parser.normalize_params(@params, part.name, data, @query_parser.param_depth_limit)
235
+ @query_parser.normalize_params(@params, part.name, data)
233
236
  end
234
237
  end
235
238
  MultipartInfo.new @params.to_params_hash, @collector.find_all(&:file?).map(&:body)
@@ -237,40 +240,38 @@ module Rack
237
240
 
238
241
  private
239
242
 
240
- def run_parser
241
- loop do
242
- case @state
243
- when :FAST_FORWARD
244
- break if handle_fast_forward == :want_read
245
- when :CONSUME_TOKEN
246
- break if handle_consume_token == :want_read
247
- when :MIME_HEAD
248
- break if handle_mime_head == :want_read
249
- when :MIME_BODY
250
- break if handle_mime_body == :want_read
251
- when :DONE
252
- break
253
- end
254
- end
243
+ def dequote(str) # From WEBrick::HTTPUtils
244
+ ret = (/\A"(.*)"\Z/ =~ str) ? $1 : str.dup
245
+ ret.gsub!(/\\(.)/, "\\1")
246
+ ret
255
247
  end
256
248
 
257
- def handle_fast_forward
258
- tok = consume_boundary
259
-
260
- if tok == :END_BOUNDARY && @sbuf.pos == @end_boundary_size && @sbuf.eos?
261
- # stop parsing a buffer if a buffer is only an end boundary.
262
- @state = :DONE
263
- elsif tok
264
- @state = :MIME_HEAD
265
- else
266
- raise EOFError, "bad content body" if @sbuf.rest_size >= @bufsize
267
-
268
- # We raise if we don't find the multipart boundary, to avoid unbounded memory
269
- # buffering. Note that the actual limit is the higher of 16KB and the buffer size (1MB by default)
270
- 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
271
254
 
272
- # no boundary found, keep reading data
273
- 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
274
275
  end
275
276
  end
276
277
 
@@ -289,7 +290,7 @@ module Rack
289
290
  head = @sbuf[1]
290
291
  content_type = head[MULTIPART_CONTENT_TYPE, 1]
291
292
  if name = head[MULTIPART_CONTENT_DISPOSITION, 1]
292
- name = Rack::Auth::Digest::Params::dequote(name)
293
+ name = dequote(name)
293
294
  else
294
295
  name = head[MULTIPART_CONTENT_ID, 1]
295
296
  end
@@ -300,30 +301,16 @@ module Rack
300
301
  name = filename || "#{content_type || TEXT_PLAIN}[]".dup
301
302
  end
302
303
 
303
- # Mime part head data is retained for both TempfilePart and BufferPart
304
- # for the entireity of the parse, even though it isn't used for BufferPart.
305
- update_retained_size(head.bytesize)
306
-
307
- # If a filename is given, a TempfilePart will be used, so the body will
308
- # not be buffered in memory. However, if a filename is not given, a BufferPart
309
- # will be used, and the body will be buffered in memory.
310
- @body_retained = !filename
311
-
312
304
  @collector.on_mime_head @mime_index, head, filename, content_type, name
313
305
  @state = :MIME_BODY
314
306
  else
315
- # We raise if the mime part header is too large, to avoid unbounded memory
316
- # buffering. Note that the actual limit is the higher of 64KB and the buffer size (1MB by default)
317
- raise EOFError, "multipart mime part header too large" if @sbuf.string.bytesize > MIME_HEADER_BYTESIZE_LIMIT
318
-
319
- return :want_read
307
+ :want_read
320
308
  end
321
309
  end
322
310
 
323
311
  def handle_mime_body
324
312
  if (body_with_boundary = @sbuf.check_until(@body_regex)) # check but do not advance the pointer yet
325
313
  body = body_with_boundary.sub(/#{@body_regex}\z/m, '') # remove the boundary from the string
326
- update_retained_size(body.bytesize) if @body_retained
327
314
  @collector.on_mime_body @mime_index, body
328
315
  @sbuf.pos += body.length + 2 # skip \r\n after the content
329
316
  @state = :CONSUME_TOKEN
@@ -332,9 +319,7 @@ module Rack
332
319
  # Save what we have so far
333
320
  if @rx_max_size < @sbuf.rest_size
334
321
  delta = @sbuf.rest_size - @rx_max_size
335
- body = @sbuf.peek(delta)
336
- update_retained_size(body.bytesize) if @body_retained
337
- @collector.on_mime_body @mime_index, body
322
+ @collector.on_mime_body @mime_index, @sbuf.peek(delta)
338
323
  @sbuf.pos += delta
339
324
  @sbuf.string = @sbuf.rest
340
325
  end
@@ -342,26 +327,16 @@ module Rack
342
327
  end
343
328
  end
344
329
 
345
- def full_boundary; @full_boundary; end
346
-
347
- def update_retained_size(size)
348
- @retained_size += size
349
- if @retained_size > BUFFERED_UPLOAD_BYTESIZE_LIMIT
350
- raise EOFError, "multipart data over retained size limit"
351
- end
352
- end
353
-
354
330
  # Scan until the we find the start or end of the boundary.
355
331
  # If we find it, return the appropriate symbol for the start or
356
332
  # end of the boundary. If we don't find the start or end of the
357
333
  # boundary, clear the buffer and return nil.
358
334
  def consume_boundary
359
- while read_buffer = @sbuf.scan_until(BOUNDARY_REGEX)
360
- case read_buffer.strip
361
- when full_boundary then return :BOUNDARY
362
- when @end_boundary then return :END_BOUNDARY
363
- end
364
- 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
365
340
  end
366
341
  end
367
342
 
@@ -371,10 +346,10 @@ module Rack
371
346
  when RFC2183
372
347
  params = Hash[*head.scan(DISPPARM).flat_map(&:compact)]
373
348
 
374
- if filename = params['filename']
375
- filename = $1 if filename =~ /^"(.*)"$/
376
- elsif filename = params['filename*']
349
+ if filename = params['filename*']
377
350
  encoding, _, filename = filename.split("'", 3)
351
+ elsif filename = params['filename']
352
+ filename = $1 if filename =~ /^"(.*)"$/
378
353
  end
379
354
  when BROKEN
380
355
  filename = $1
@@ -401,6 +376,7 @@ module Rack
401
376
  end
402
377
 
403
378
  CHARSET = "charset"
379
+ deprecate_constant :CHARSET
404
380
 
405
381
  def tag_multipart_encoding(filename, content_type, name, body)
406
382
  name = name.to_s
@@ -421,7 +397,13 @@ module Rack
421
397
  k.strip!
422
398
  v.strip!
423
399
  v = v[1..-2] if v.start_with?('"') && v.end_with?('"')
424
- 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
425
407
  end
426
408
  end
427
409
  end
@@ -432,7 +414,7 @@ module Rack
432
414
 
433
415
  def handle_empty_content!(content)
434
416
  if content.nil? || content.empty?
435
- raise EOFError
417
+ raise EmptyContentError
436
418
  end
437
419
  end
438
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
 
@@ -1,64 +1,44 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'constants'
4
+ require_relative 'utils'
5
+
3
6
  require_relative 'multipart/parser'
7
+ require_relative 'multipart/generator'
4
8
 
5
9
  module Rack
6
10
  # A multipart form data parser, adapted from IOWA.
7
11
  #
8
12
  # Usually, Rack::Request#POST takes care of calling this.
9
13
  module Multipart
10
- autoload :UploadedFile, 'rack/multipart/uploaded_file'
11
- autoload :Generator, 'rack/multipart/generator'
12
-
13
- EOL = "\r\n"
14
14
  MULTIPART_BOUNDARY = "AaB03x"
15
- MULTIPART = %r|\Amultipart/.*boundary=\"?([^\";,]+)\"?|ni
16
- TOKEN = /[^\s()<>,;:\\"\/\[\]?=]+/
17
- CONDISP = /Content-Disposition:\s*#{TOKEN}\s*/i
18
- VALUE = /"(?:\\"|[^"])*"|#{TOKEN}/
19
- BROKEN = /^#{CONDISP}.*;\s*filename=(#{VALUE})/i
20
- MULTIPART_CONTENT_TYPE = /Content-Type: (.*)#{EOL}/ni
21
- MULTIPART_CONTENT_DISPOSITION = /Content-Disposition:[^:]*;\s*name=(#{VALUE})/ni
22
- MULTIPART_CONTENT_ID = /Content-ID:\s*([^#{EOL}]*)/ni
23
- # Updated definitions from RFC 2231
24
- ATTRIBUTE_CHAR = %r{[^ \x00-\x1f\x7f)(><@,;:\\"/\[\]?='*%]}
25
- ATTRIBUTE = /#{ATTRIBUTE_CHAR}+/
26
- SECTION = /\*[0-9]+/
27
- REGULAR_PARAMETER_NAME = /#{ATTRIBUTE}#{SECTION}?/
28
- REGULAR_PARAMETER = /(#{REGULAR_PARAMETER_NAME})=(#{VALUE})/
29
- EXTENDED_OTHER_NAME = /#{ATTRIBUTE}\*[1-9][0-9]*\*/
30
- EXTENDED_OTHER_VALUE = /%[0-9a-fA-F]{2}|#{ATTRIBUTE_CHAR}/
31
- EXTENDED_OTHER_PARAMETER = /(#{EXTENDED_OTHER_NAME})=(#{EXTENDED_OTHER_VALUE}*)/
32
- EXTENDED_INITIAL_NAME = /#{ATTRIBUTE}(?:\*0)?\*/
33
- EXTENDED_INITIAL_VALUE = /[a-zA-Z0-9\-]*'[a-zA-Z0-9\-]*'#{EXTENDED_OTHER_VALUE}*/
34
- EXTENDED_INITIAL_PARAMETER = /(#{EXTENDED_INITIAL_NAME})=(#{EXTENDED_INITIAL_VALUE})/
35
- EXTENDED_PARAMETER = /#{EXTENDED_INITIAL_PARAMETER}|#{EXTENDED_OTHER_PARAMETER}/
36
- DISPPARM = /;\s*(?:#{REGULAR_PARAMETER}|#{EXTENDED_PARAMETER})\s*/
37
- RFC2183 = /^#{CONDISP}(#{DISPPARM})+$/i
38
15
 
39
16
  class << self
40
17
  def parse_multipart(env, params = Rack::Utils.default_query_parser)
41
- extract_multipart Rack::Request.new(env), params
42
- end
18
+ io = env[RACK_INPUT]
19
+
20
+ if content_length = env['CONTENT_LENGTH']
21
+ content_length = content_length.to_i
22
+ end
43
23
 
44
- def extract_multipart(req, params = Rack::Utils.default_query_parser)
45
- io = req.get_header(RACK_INPUT)
46
- io.rewind
47
- content_length = req.content_length
48
- content_length = content_length.to_i if content_length
24
+ content_type = env['CONTENT_TYPE']
49
25
 
50
- tempfile = req.get_header(RACK_MULTIPART_TEMPFILE_FACTORY) || Parser::TEMPFILE_FACTORY
51
- bufsize = req.get_header(RACK_MULTIPART_BUFFER_SIZE) || Parser::BUFSIZE
26
+ tempfile = env[RACK_MULTIPART_TEMPFILE_FACTORY] || Parser::TEMPFILE_FACTORY
27
+ bufsize = env[RACK_MULTIPART_BUFFER_SIZE] || Parser::BUFSIZE
52
28
 
53
- info = Parser.parse io, content_length, req.get_header('CONTENT_TYPE'), tempfile, bufsize, params
54
- req.set_header(RACK_TEMPFILES, info.tmp_files)
55
- info.params
29
+ info = Parser.parse(io, content_length, content_type, tempfile, bufsize, params)
30
+ env[RACK_TEMPFILES] = info.tmp_files
31
+
32
+ return info.params
33
+ end
34
+
35
+ def extract_multipart(request, params = Rack::Utils.default_query_parser)
36
+ parse_multipart(request.env)
56
37
  end
57
38
 
58
39
  def build_multipart(params, first = true)
59
40
  Generator.new(params, first).dump
60
41
  end
61
42
  end
62
-
63
43
  end
64
44
  end