rack 2.2.21 → 3.1.19

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 (86) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +459 -73
  3. data/CONTRIBUTING.md +63 -55
  4. data/MIT-LICENSE +1 -1
  5. data/README.md +363 -0
  6. data/SPEC.rdoc +204 -131
  7. data/lib/rack/auth/abstract/handler.rb +3 -1
  8. data/lib/rack/auth/abstract/request.rb +3 -1
  9. data/lib/rack/auth/basic.rb +1 -3
  10. data/lib/rack/bad_request.rb +8 -0
  11. data/lib/rack/body_proxy.rb +21 -3
  12. data/lib/rack/builder.rb +102 -69
  13. data/lib/rack/cascade.rb +2 -3
  14. data/lib/rack/common_logger.rb +22 -17
  15. data/lib/rack/conditional_get.rb +18 -15
  16. data/lib/rack/constants.rb +67 -0
  17. data/lib/rack/content_length.rb +12 -16
  18. data/lib/rack/content_type.rb +8 -5
  19. data/lib/rack/deflater.rb +40 -26
  20. data/lib/rack/directory.rb +9 -3
  21. data/lib/rack/etag.rb +17 -23
  22. data/lib/rack/events.rb +4 -0
  23. data/lib/rack/files.rb +15 -17
  24. data/lib/rack/head.rb +9 -8
  25. data/lib/rack/headers.rb +238 -0
  26. data/lib/rack/lint.rb +840 -644
  27. data/lib/rack/lock.rb +2 -5
  28. data/lib/rack/logger.rb +3 -0
  29. data/lib/rack/method_override.rb +5 -1
  30. data/lib/rack/mime.rb +14 -5
  31. data/lib/rack/mock.rb +1 -300
  32. data/lib/rack/mock_request.rb +161 -0
  33. data/lib/rack/mock_response.rb +153 -0
  34. data/lib/rack/multipart/generator.rb +7 -5
  35. data/lib/rack/multipart/parser.rb +216 -102
  36. data/lib/rack/multipart/uploaded_file.rb +4 -0
  37. data/lib/rack/multipart.rb +53 -40
  38. data/lib/rack/null_logger.rb +9 -0
  39. data/lib/rack/query_parser.rb +80 -102
  40. data/lib/rack/recursive.rb +2 -0
  41. data/lib/rack/reloader.rb +0 -2
  42. data/lib/rack/request.rb +263 -126
  43. data/lib/rack/response.rb +151 -66
  44. data/lib/rack/rewindable_input.rb +24 -5
  45. data/lib/rack/runtime.rb +7 -6
  46. data/lib/rack/sendfile.rb +35 -30
  47. data/lib/rack/show_exceptions.rb +21 -4
  48. data/lib/rack/show_status.rb +17 -7
  49. data/lib/rack/static.rb +8 -8
  50. data/lib/rack/tempfile_reaper.rb +15 -4
  51. data/lib/rack/urlmap.rb +3 -1
  52. data/lib/rack/utils.rb +236 -237
  53. data/lib/rack/version.rb +1 -9
  54. data/lib/rack.rb +13 -89
  55. metadata +13 -39
  56. data/README.rdoc +0 -355
  57. data/Rakefile +0 -130
  58. data/bin/rackup +0 -5
  59. data/contrib/rack.png +0 -0
  60. data/contrib/rack.svg +0 -150
  61. data/contrib/rack_logo.svg +0 -164
  62. data/contrib/rdoc.css +0 -412
  63. data/example/lobster.ru +0 -6
  64. data/example/protectedlobster.rb +0 -16
  65. data/example/protectedlobster.ru +0 -10
  66. data/lib/rack/auth/digest/md5.rb +0 -131
  67. data/lib/rack/auth/digest/nonce.rb +0 -53
  68. data/lib/rack/auth/digest/params.rb +0 -54
  69. data/lib/rack/auth/digest/request.rb +0 -43
  70. data/lib/rack/chunked.rb +0 -117
  71. data/lib/rack/core_ext/regexp.rb +0 -14
  72. data/lib/rack/file.rb +0 -7
  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
  86. data/rack.gemspec +0 -46
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'time'
4
+
5
+ require_relative 'response'
6
+
7
+ module Rack
8
+ # Rack::MockResponse provides useful helpers for testing your apps.
9
+ # Usually, you don't create the MockResponse on your own, but use
10
+ # MockRequest.
11
+
12
+ class MockResponse < Rack::Response
13
+ begin
14
+ # Recent versions of the CGI gem may not provide `CGI::Cookie`.
15
+ require 'cgi/cookie'
16
+ Cookie = CGI::Cookie
17
+ rescue LoadError
18
+ class Cookie
19
+ attr_reader :name, :value, :path, :domain, :expires, :secure
20
+
21
+ def initialize(args)
22
+ @name = args["name"]
23
+ @value = args["value"]
24
+ @path = args["path"]
25
+ @domain = args["domain"]
26
+ @expires = args["expires"]
27
+ @secure = args["secure"]
28
+ end
29
+
30
+ def method_missing(method_name, *args, &block)
31
+ @value.send(method_name, *args, &block)
32
+ end
33
+ # :nocov:
34
+ ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
35
+ # :nocov:
36
+
37
+ def respond_to_missing?(method_name, include_all = false)
38
+ @value.respond_to?(method_name, include_all) || super
39
+ end
40
+ end
41
+ end
42
+
43
+ class << self
44
+ alias [] new
45
+ end
46
+
47
+ # Headers
48
+ attr_reader :original_headers, :cookies
49
+
50
+ # Errors
51
+ attr_accessor :errors
52
+
53
+ def initialize(status, headers, body, errors = nil)
54
+ @original_headers = headers
55
+
56
+ if errors
57
+ @errors = errors.string if errors.respond_to?(:string)
58
+ else
59
+ @errors = ""
60
+ end
61
+
62
+ super(body, status, headers)
63
+
64
+ @cookies = parse_cookies_from_header
65
+ buffered_body!
66
+ end
67
+
68
+ def =~(other)
69
+ body =~ other
70
+ end
71
+
72
+ def match(other)
73
+ body.match other
74
+ end
75
+
76
+ def body
77
+ return @buffered_body if defined?(@buffered_body)
78
+
79
+ # FIXME: apparently users of MockResponse expect the return value of
80
+ # MockResponse#body to be a string. However, the real response object
81
+ # returns the body as a list.
82
+ #
83
+ # See spec_showstatus.rb:
84
+ #
85
+ # should "not replace existing messages" do
86
+ # ...
87
+ # res.body.should == "foo!"
88
+ # end
89
+ buffer = @buffered_body = String.new
90
+
91
+ @body.each do |chunk|
92
+ buffer << chunk
93
+ end
94
+
95
+ return buffer
96
+ end
97
+
98
+ def empty?
99
+ [201, 204, 304].include? status
100
+ end
101
+
102
+ def cookie(name)
103
+ cookies.fetch(name, nil)
104
+ end
105
+
106
+ private
107
+
108
+ def parse_cookies_from_header
109
+ cookies = Hash.new
110
+ set_cookie_header = headers['set-cookie']
111
+ if set_cookie_header && !set_cookie_header.empty?
112
+ Array(set_cookie_header).each do |cookie|
113
+ cookie_name, cookie_filling = cookie.split('=', 2)
114
+ cookie_attributes = identify_cookie_attributes cookie_filling
115
+ parsed_cookie = Cookie.new(
116
+ 'name' => cookie_name.strip,
117
+ 'value' => cookie_attributes.fetch('value'),
118
+ 'path' => cookie_attributes.fetch('path', nil),
119
+ 'domain' => cookie_attributes.fetch('domain', nil),
120
+ 'expires' => cookie_attributes.fetch('expires', nil),
121
+ 'secure' => cookie_attributes.fetch('secure', false)
122
+ )
123
+ cookies.store(cookie_name, parsed_cookie)
124
+ end
125
+ end
126
+ cookies
127
+ end
128
+
129
+ def identify_cookie_attributes(cookie_filling)
130
+ cookie_bits = cookie_filling.split(';')
131
+ cookie_attributes = Hash.new
132
+ cookie_attributes.store('value', Array(cookie_bits[0].strip))
133
+ cookie_bits.drop(1).each do |bit|
134
+ if bit.include? '='
135
+ cookie_attribute, attribute_value = bit.split('=', 2)
136
+ cookie_attributes.store(cookie_attribute.strip.downcase, attribute_value.strip)
137
+ end
138
+ if bit.include? 'secure'
139
+ cookie_attributes.store('secure', true)
140
+ end
141
+ end
142
+
143
+ if cookie_attributes.key? 'max-age'
144
+ cookie_attributes.store('expires', Time.now + cookie_attributes['max-age'].to_i)
145
+ elsif cookie_attributes.key? 'expires'
146
+ cookie_attributes.store('expires', Time.httpdate(cookie_attributes['expires']))
147
+ end
148
+
149
+ cookie_attributes
150
+ end
151
+
152
+ end
153
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'uploaded_file'
4
+
3
5
  module Rack
4
6
  module Multipart
5
7
  class Generator
@@ -74,12 +76,12 @@ module Rack
74
76
 
75
77
  def content_for_tempfile(io, file, name)
76
78
  length = ::File.stat(file.path).size if file.path
77
- filename = "; filename=\"#{Utils.escape(file.original_filename)}\"" if file.original_filename
79
+ filename = "; filename=\"#{Utils.escape_path(file.original_filename)}\""
78
80
  <<-EOF
79
81
  --#{MULTIPART_BOUNDARY}\r
80
- Content-Disposition: form-data; name="#{name}"#{filename}\r
81
- Content-Type: #{file.content_type}\r
82
- #{"Content-Length: #{length}\r\n" if length}\r
82
+ content-disposition: form-data; name="#{name}"#{filename}\r
83
+ content-type: #{file.content_type}\r
84
+ #{"content-length: #{length}\r\n" if length}\r
83
85
  #{io.read}\r
84
86
  EOF
85
87
  end
@@ -87,7 +89,7 @@ EOF
87
89
  def content_for_other(file, name)
88
90
  <<-EOF
89
91
  --#{MULTIPART_BOUNDARY}\r
90
- Content-Disposition: form-data; name="#{name}"\r
92
+ content-disposition: form-data; name="#{name}"\r
91
93
  \r
92
94
  #{file}\r
93
95
  EOF
@@ -2,14 +2,43 @@
2
2
 
3
3
  require 'strscan'
4
4
 
5
+ require_relative '../utils'
6
+ require_relative '../bad_request'
7
+
5
8
  module Rack
6
9
  module Multipart
7
- class MultipartPartLimitError < Errno::EMFILE; end
8
- class MultipartTotalPartLimitError < StandardError; end
10
+ class MultipartPartLimitError < Errno::EMFILE
11
+ include BadRequest
12
+ end
9
13
 
10
- class Parser
11
- (require_relative '../core_ext/regexp'; using ::Rack::RegexpExtensions) if RUBY_VERSION < '2.4'
14
+ class MultipartTotalPartLimitError < StandardError
15
+ include BadRequest
16
+ end
17
+
18
+ # Use specific error class when parsing multipart request
19
+ # that ends early.
20
+ class EmptyContentError < ::EOFError
21
+ include BadRequest
22
+ end
23
+
24
+ # Base class for multipart exceptions that do not subclass from
25
+ # other exception classes for backwards compatibility.
26
+ class BoundaryTooLongError < StandardError
27
+ include BadRequest
28
+ end
12
29
 
30
+ # Prefer to use the BoundaryTooLongError class or Rack::BadRequest.
31
+ Error = BoundaryTooLongError
32
+
33
+ EOL = "\r\n"
34
+ FWS = /[ \t]+(?:\r\n[ \t]+)?/ # whitespace with optional folding
35
+ HEADER_VALUE = "(?:[^\r\n]|\r\n[ \t])*" # anything but a non-folding CRLF
36
+ MULTIPART = %r|\Amultipart/.*boundary=\"?([^\";,]+)\"?|ni
37
+ MULTIPART_CONTENT_TYPE = /^Content-Type:#{FWS}?(#{HEADER_VALUE})/ni
38
+ MULTIPART_CONTENT_DISPOSITION = /^Content-Disposition:#{FWS}?(#{HEADER_VALUE})/ni
39
+ MULTIPART_CONTENT_ID = /^Content-ID:#{FWS}?(#{HEADER_VALUE})/ni
40
+
41
+ class Parser
13
42
  BUFSIZE = 1_048_576
14
43
  TEXT_PLAIN = "text/plain"
15
44
  TEMPFILE_FACTORY = lambda { |filename, content_type|
@@ -18,8 +47,6 @@ module Rack
18
47
  Tempfile.new(["RackMultipart", extension])
19
48
  }
20
49
 
21
- BOUNDARY_REGEX = /\A([^\n]*(?:\n|\Z))/
22
-
23
50
  BOUNDARY_START_LIMIT = 16 * 1024
24
51
  private_constant :BOUNDARY_START_LIMIT
25
52
 
@@ -62,16 +89,12 @@ module Rack
62
89
  if str
63
90
  @cursor += str.bytesize
64
91
  else
65
- # Raise an error for mismatching Content-Length and actual contents
92
+ # Raise an error for mismatching content-length and actual contents
66
93
  raise EOFError, "bad content body"
67
94
  end
68
95
 
69
96
  str
70
97
  end
71
-
72
- def rewind
73
- @io.rewind
74
- end
75
98
  end
76
99
 
77
100
  MultipartInfo = Struct.new :params, :tmp_files
@@ -90,18 +113,17 @@ module Rack
90
113
  boundary = parse_boundary content_type
91
114
  return EMPTY unless boundary
92
115
 
116
+ if boundary.length > 70
117
+ # RFC 1521 Section 7.2.1 imposes a 70 character maximum for the boundary.
118
+ # Most clients use no more than 55 characters.
119
+ raise BoundaryTooLongError, "multipart boundary size too large (#{boundary.length} characters)"
120
+ end
121
+
93
122
  io = BoundedIO.new(io, content_length) if content_length
94
- outbuf = String.new
95
123
 
96
124
  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
125
+ parser.parse(io)
103
126
 
104
- io.rewind
105
127
  parser.result
106
128
  end
107
129
 
@@ -201,11 +223,8 @@ module Rack
201
223
  def initialize(boundary, tempfile, bufsize, query_parser)
202
224
  @query_parser = query_parser
203
225
  @params = query_parser.make_params
204
- @boundary = "--#{boundary}"
205
226
  @bufsize = bufsize
206
227
 
207
- @full_boundary = @boundary
208
- @end_boundary = @boundary + '--'
209
228
  @state = :FAST_FORWARD
210
229
  @mime_index = 0
211
230
  @body_retained = nil
@@ -213,23 +232,41 @@ module Rack
213
232
  @collector = Collector.new tempfile
214
233
 
215
234
  @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
235
+ @body_regex = /(?:#{EOL}|\A)--#{Regexp.quote(boundary)}(?:#{EOL}|--)/m
236
+ @body_regex_at_end = /#{@body_regex}\z/m
237
+ @end_boundary_size = boundary.bytesize + 4 # (-- at start, -- at finish)
238
+ @rx_max_size = boundary.bytesize + 6 # (\r\n-- at start, either \r\n or -- at finish)
219
239
  @head_regex = /(.*?#{EOL})#{EOL}/m
220
240
  end
221
241
 
222
- def on_read(content)
223
- handle_empty_content!(content)
224
- @sbuf.concat content
225
- run_parser
242
+ def parse(io)
243
+ outbuf = String.new
244
+ read_data(io, outbuf)
245
+
246
+ loop do
247
+ status =
248
+ case @state
249
+ when :FAST_FORWARD
250
+ handle_fast_forward
251
+ when :CONSUME_TOKEN
252
+ handle_consume_token
253
+ when :MIME_HEAD
254
+ handle_mime_head
255
+ when :MIME_BODY
256
+ handle_mime_body
257
+ else # when :DONE
258
+ return
259
+ end
260
+
261
+ read_data(io, outbuf) if status == :want_read
262
+ end
226
263
  end
227
264
 
228
265
  def result
229
266
  @collector.each do |part|
230
267
  part.get_data do |data|
231
268
  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)
269
+ @query_parser.normalize_params(@params, part.name, data)
233
270
  end
234
271
  end
235
272
  MultipartInfo.new @params.to_params_hash, @collector.find_all(&:file?).map(&:body)
@@ -237,40 +274,49 @@ module Rack
237
274
 
238
275
  private
239
276
 
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
277
+ def dequote(str) # From WEBrick::HTTPUtils
278
+ ret = (/\A"(.*)"\Z/ =~ str) ? $1 : str.dup
279
+ ret.gsub!(/\\(.)/, "\\1")
280
+ ret
255
281
  end
256
282
 
257
- def handle_fast_forward
258
- tok = consume_boundary
283
+ def read_data(io, outbuf)
284
+ content = io.read(@bufsize, outbuf)
285
+ handle_empty_content!(content)
286
+ @sbuf.concat(content)
287
+ end
259
288
 
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
289
+ # This handles the initial parser state. We read until we find the starting
290
+ # boundary, then we can transition to the next state. If we find the ending
291
+ # boundary, this is an invalid multipart upload, but keep scanning for opening
292
+ # boundary in that case. If no boundary found, we need to keep reading data
293
+ # and retry. It's highly unlikely the initial read will not consume the
294
+ # boundary. The client would have to deliberately craft a response
295
+ # with the opening boundary beyond the buffer size for that to happen.
296
+ def handle_fast_forward
297
+ while true
298
+ case consume_boundary
299
+ when :BOUNDARY
300
+ # found opening boundary, transition to next state
301
+ @state = :MIME_HEAD
302
+ return
303
+ when :END_BOUNDARY
304
+ # invalid multipart upload
305
+ if @sbuf.pos == @end_boundary_size && @sbuf.rest == EOL
306
+ # stop parsing a buffer if a buffer is only an end boundary.
307
+ @state = :DONE
308
+ return
309
+ end
267
310
 
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
311
+ # retry for opening boundary
312
+ else
313
+ # We raise if we don't find the multipart boundary, to avoid unbounded memory
314
+ # buffering. Note that the actual limit is the higher of 16KB and the buffer size (1MB by default)
315
+ raise Error, "multipart boundary not found within limit" if @sbuf.string.bytesize > BOUNDARY_START_LIMIT
271
316
 
272
- # no boundary found, keep reading data
273
- return :want_read
317
+ # no boundary found, keep reading data
318
+ return :want_read
319
+ end
274
320
  end
275
321
  end
276
322
 
@@ -284,17 +330,101 @@ module Rack
284
330
  end
285
331
  end
286
332
 
333
+ CONTENT_DISPOSITION_MAX_PARAMS = 16
334
+ CONTENT_DISPOSITION_MAX_BYTES = 1536
287
335
  def handle_mime_head
288
336
  if @sbuf.scan_until(@head_regex)
289
337
  head = @sbuf[1]
290
338
  content_type = head[MULTIPART_CONTENT_TYPE, 1]
291
- if name = head[MULTIPART_CONTENT_DISPOSITION, 1]
292
- name = Rack::Auth::Digest::Params::dequote(name)
339
+ if (disposition = head[MULTIPART_CONTENT_DISPOSITION, 1]) &&
340
+ disposition.bytesize <= CONTENT_DISPOSITION_MAX_BYTES
341
+
342
+ # ignore actual content-disposition value (should always be form-data)
343
+ i = disposition.index(';')
344
+ disposition.slice!(0, i+1)
345
+ param = nil
346
+ num_params = 0
347
+
348
+ # Parse parameter list
349
+ while i = disposition.index('=')
350
+ # Only parse up to max parameters, to avoid potential denial of service
351
+ num_params += 1
352
+ break if num_params > CONTENT_DISPOSITION_MAX_PARAMS
353
+
354
+ # Found end of parameter name, ensure forward progress in loop
355
+ param = disposition.slice!(0, i+1)
356
+
357
+ # Remove ending equals and preceding whitespace from parameter name
358
+ param.chomp!('=')
359
+ param.lstrip!
360
+
361
+ if disposition[0] == '"'
362
+ # Parameter value is quoted, parse it, handling backslash escapes
363
+ disposition.slice!(0, 1)
364
+ value = String.new
365
+
366
+ while i = disposition.index(/(["\\])/)
367
+ c = $1
368
+
369
+ # Append all content until ending quote or escape
370
+ value << disposition.slice!(0, i)
371
+
372
+ # Remove either backslash or ending quote,
373
+ # ensures forward progress in loop
374
+ disposition.slice!(0, 1)
375
+
376
+ # stop parsing parameter value if found ending quote
377
+ break if c == '"'
378
+
379
+ escaped_char = disposition.slice!(0, 1)
380
+ if param == 'filename' && escaped_char != '"'
381
+ # Possible IE uploaded filename, append both escape backslash and value
382
+ value << c << escaped_char
383
+ else
384
+ # Other only append escaped value
385
+ value << escaped_char
386
+ end
387
+ end
388
+ else
389
+ if i = disposition.index(';')
390
+ # Parameter value unquoted (which may be invalid), value ends at semicolon
391
+ value = disposition.slice!(0, i)
392
+ else
393
+ # If no ending semicolon, assume remainder of line is value and stop
394
+ # parsing
395
+ disposition.strip!
396
+ value = disposition
397
+ disposition = ''
398
+ end
399
+ end
400
+
401
+ case param
402
+ when 'name'
403
+ name = value
404
+ when 'filename'
405
+ filename = value
406
+ when 'filename*'
407
+ filename_star = value
408
+ # else
409
+ # ignore other parameters
410
+ end
411
+
412
+ # skip trailing semicolon, to proceed to next parameter
413
+ if i = disposition.index(';')
414
+ disposition.slice!(0, i+1)
415
+ end
416
+ end
293
417
  else
294
418
  name = head[MULTIPART_CONTENT_ID, 1]
295
419
  end
296
420
 
297
- filename = get_filename(head)
421
+ if filename_star
422
+ encoding, _, filename = filename_star.split("'", 3)
423
+ filename = normalize_filename(filename || '')
424
+ filename.force_encoding(find_encoding(encoding))
425
+ elsif filename
426
+ filename = normalize_filename(filename)
427
+ end
298
428
 
299
429
  if name.nil? || name.empty?
300
430
  name = filename || "#{content_type || TEXT_PLAIN}[]".dup
@@ -314,7 +444,7 @@ module Rack
314
444
  else
315
445
  # We raise if the mime part header is too large, to avoid unbounded memory
316
446
  # 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.rest.bytesize > MIME_HEADER_BYTESIZE_LIMIT
447
+ raise Error, "multipart mime part header too large" if @sbuf.rest.bytesize > MIME_HEADER_BYTESIZE_LIMIT
318
448
 
319
449
  return :want_read
320
450
  end
@@ -322,7 +452,7 @@ module Rack
322
452
 
323
453
  def handle_mime_body
324
454
  if (body_with_boundary = @sbuf.check_until(@body_regex)) # check but do not advance the pointer yet
325
- body = body_with_boundary.sub(/#{@body_regex}\z/m, '') # remove the boundary from the string
455
+ body = body_with_boundary.sub(@body_regex_at_end, '') # remove the boundary from the string
326
456
  update_retained_size(body.bytesize) if @body_retained
327
457
  @collector.on_mime_body @mime_index, body
328
458
  @sbuf.pos += body.length + 2 # skip \r\n after the content
@@ -342,12 +472,10 @@ module Rack
342
472
  end
343
473
  end
344
474
 
345
- def full_boundary; @full_boundary; end
346
-
347
475
  def update_retained_size(size)
348
476
  @retained_size += size
349
477
  if @retained_size > BUFFERED_UPLOAD_BYTESIZE_LIMIT
350
- raise EOFError, "multipart data over retained size limit"
478
+ raise Error, "multipart data over retained size limit"
351
479
  end
352
480
  end
353
481
 
@@ -356,51 +484,26 @@ module Rack
356
484
  # end of the boundary. If we don't find the start or end of the
357
485
  # boundary, clear the buffer and return nil.
358
486
  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?
487
+ if read_buffer = @sbuf.scan_until(@body_regex)
488
+ read_buffer.end_with?(EOL) ? :BOUNDARY : :END_BOUNDARY
489
+ else
490
+ @sbuf.terminate
491
+ nil
365
492
  end
366
493
  end
367
494
 
368
- def get_filename(head)
369
- filename = nil
370
- case head
371
- when RFC2183
372
- params = Hash[*head.scan(DISPPARM).flat_map(&:compact)]
373
-
374
- if filename = params['filename']
375
- filename = $1 if filename =~ /^"(.*)"$/
376
- elsif filename = params['filename*']
377
- encoding, _, filename = filename.split("'", 3)
378
- end
379
- when BROKEN
380
- filename = $1
381
- filename = $1 if filename =~ /^"(.*)"$/
382
- end
383
-
384
- return unless filename
385
-
495
+ def normalize_filename(filename)
386
496
  if filename.scan(/%.?.?/).all? { |s| /%[0-9a-fA-F]{2}/.match?(s) }
387
497
  filename = Utils.unescape_path(filename)
388
498
  end
389
499
 
390
500
  filename.scrub!
391
501
 
392
- if filename !~ /\\[^\\"]/
393
- filename = filename.gsub(/\\(.)/, '\1')
394
- end
395
-
396
- if encoding
397
- filename.force_encoding ::Encoding.find(encoding)
398
- end
399
-
400
- filename
502
+ filename.split(/[\/\\]/).last || String.new
401
503
  end
402
504
 
403
505
  CHARSET = "charset"
506
+ deprecate_constant :CHARSET
404
507
 
405
508
  def tag_multipart_encoding(filename, content_type, name, body)
406
509
  name = name.to_s
@@ -421,7 +524,9 @@ module Rack
421
524
  k.strip!
422
525
  v.strip!
423
526
  v = v[1..-2] if v.start_with?('"') && v.end_with?('"')
424
- encoding = Encoding.find v if k == CHARSET
527
+ if k == "charset"
528
+ encoding = find_encoding(v)
529
+ end
425
530
  end
426
531
  end
427
532
  end
@@ -430,9 +535,18 @@ module Rack
430
535
  body.force_encoding(encoding)
431
536
  end
432
537
 
538
+ # Return the related Encoding object. However, because
539
+ # enc is submitted by the user, it may be invalid, so
540
+ # use a binary encoding in that case.
541
+ def find_encoding(enc)
542
+ Encoding.find enc
543
+ rescue ArgumentError
544
+ Encoding::BINARY
545
+ end
546
+
433
547
  def handle_empty_content!(content)
434
548
  if content.nil? || content.empty?
435
- raise EOFError
549
+ raise EmptyContentError
436
550
  end
437
551
  end
438
552
  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