rack 2.1.0 → 3.1.0

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 (88) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +377 -16
  3. data/CONTRIBUTING.md +144 -0
  4. data/MIT-LICENSE +1 -1
  5. data/README.md +328 -0
  6. data/SPEC.rdoc +365 -0
  7. data/lib/rack/auth/abstract/handler.rb +3 -1
  8. data/lib/rack/auth/abstract/request.rb +2 -2
  9. data/lib/rack/auth/basic.rb +4 -7
  10. data/lib/rack/bad_request.rb +8 -0
  11. data/lib/rack/body_proxy.rb +34 -12
  12. data/lib/rack/builder.rb +162 -59
  13. data/lib/rack/cascade.rb +24 -10
  14. data/lib/rack/common_logger.rb +43 -28
  15. data/lib/rack/conditional_get.rb +30 -25
  16. data/lib/rack/constants.rb +66 -0
  17. data/lib/rack/content_length.rb +10 -16
  18. data/lib/rack/content_type.rb +9 -7
  19. data/lib/rack/deflater.rb +78 -50
  20. data/lib/rack/directory.rb +86 -63
  21. data/lib/rack/etag.rb +14 -22
  22. data/lib/rack/events.rb +18 -17
  23. data/lib/rack/files.rb +99 -61
  24. data/lib/rack/head.rb +8 -9
  25. data/lib/rack/headers.rb +238 -0
  26. data/lib/rack/lint.rb +868 -642
  27. data/lib/rack/lock.rb +2 -6
  28. data/lib/rack/logger.rb +3 -0
  29. data/lib/rack/media_type.rb +9 -4
  30. data/lib/rack/method_override.rb +6 -2
  31. data/lib/rack/mime.rb +14 -5
  32. data/lib/rack/mock.rb +1 -253
  33. data/lib/rack/mock_request.rb +171 -0
  34. data/lib/rack/mock_response.rb +124 -0
  35. data/lib/rack/multipart/generator.rb +15 -8
  36. data/lib/rack/multipart/parser.rb +238 -107
  37. data/lib/rack/multipart/uploaded_file.rb +17 -7
  38. data/lib/rack/multipart.rb +54 -42
  39. data/lib/rack/null_logger.rb +9 -0
  40. data/lib/rack/query_parser.rb +87 -105
  41. data/lib/rack/recursive.rb +3 -1
  42. data/lib/rack/reloader.rb +0 -4
  43. data/lib/rack/request.rb +366 -135
  44. data/lib/rack/response.rb +186 -68
  45. data/lib/rack/rewindable_input.rb +24 -6
  46. data/lib/rack/runtime.rb +8 -7
  47. data/lib/rack/sendfile.rb +29 -27
  48. data/lib/rack/show_exceptions.rb +27 -12
  49. data/lib/rack/show_status.rb +21 -13
  50. data/lib/rack/static.rb +19 -12
  51. data/lib/rack/tempfile_reaper.rb +14 -5
  52. data/lib/rack/urlmap.rb +5 -6
  53. data/lib/rack/utils.rb +274 -260
  54. data/lib/rack/version.rb +21 -0
  55. data/lib/rack.rb +18 -103
  56. metadata +25 -52
  57. data/README.rdoc +0 -262
  58. data/Rakefile +0 -123
  59. data/SPEC +0 -263
  60. data/bin/rackup +0 -5
  61. data/contrib/rack.png +0 -0
  62. data/contrib/rack.svg +0 -150
  63. data/contrib/rack_logo.svg +0 -164
  64. data/contrib/rdoc.css +0 -412
  65. data/example/lobster.ru +0 -6
  66. data/example/protectedlobster.rb +0 -16
  67. data/example/protectedlobster.ru +0 -10
  68. data/lib/rack/auth/digest/md5.rb +0 -131
  69. data/lib/rack/auth/digest/nonce.rb +0 -54
  70. data/lib/rack/auth/digest/params.rb +0 -54
  71. data/lib/rack/auth/digest/request.rb +0 -43
  72. data/lib/rack/chunked.rb +0 -92
  73. data/lib/rack/core_ext/regexp.rb +0 -14
  74. data/lib/rack/file.rb +0 -8
  75. data/lib/rack/handler/cgi.rb +0 -62
  76. data/lib/rack/handler/fastcgi.rb +0 -102
  77. data/lib/rack/handler/lsws.rb +0 -63
  78. data/lib/rack/handler/scgi.rb +0 -73
  79. data/lib/rack/handler/thin.rb +0 -38
  80. data/lib/rack/handler/webrick.rb +0 -122
  81. data/lib/rack/handler.rb +0 -104
  82. data/lib/rack/lobster.rb +0 -72
  83. data/lib/rack/server.rb +0 -467
  84. data/lib/rack/session/abstract/id.rb +0 -528
  85. data/lib/rack/session/cookie.rb +0 -205
  86. data/lib/rack/session/memcache.rb +0 -10
  87. data/lib/rack/session/pool.rb +0 -85
  88. data/rack.gemspec +0 -44
@@ -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
+ return @buffered_body if defined?(@buffered_body)
49
+
50
+ # FIXME: apparently users of MockResponse expect the return value of
51
+ # MockResponse#body to be a string. However, the real response object
52
+ # returns the body as a list.
53
+ #
54
+ # See spec_showstatus.rb:
55
+ #
56
+ # should "not replace existing messages" do
57
+ # ...
58
+ # res.body.should == "foo!"
59
+ # end
60
+ buffer = @buffered_body = String.new
61
+
62
+ @body.each do |chunk|
63
+ buffer << chunk
64
+ end
65
+
66
+ return buffer
67
+ end
68
+
69
+ def empty?
70
+ [201, 204, 304].include? status
71
+ end
72
+
73
+ def cookie(name)
74
+ cookies.fetch(name, nil)
75
+ end
76
+
77
+ private
78
+
79
+ def parse_cookies_from_header
80
+ cookies = Hash.new
81
+ if headers.has_key? 'set-cookie'
82
+ set_cookie_header = headers.fetch('set-cookie')
83
+ Array(set_cookie_header).each do |cookie|
84
+ cookie_name, cookie_filling = cookie.split('=', 2)
85
+ cookie_attributes = identify_cookie_attributes cookie_filling
86
+ parsed_cookie = CGI::Cookie.new(
87
+ 'name' => cookie_name.strip,
88
+ 'value' => cookie_attributes.fetch('value'),
89
+ 'path' => cookie_attributes.fetch('path', nil),
90
+ 'domain' => cookie_attributes.fetch('domain', nil),
91
+ 'expires' => cookie_attributes.fetch('expires', nil),
92
+ 'secure' => cookie_attributes.fetch('secure', false)
93
+ )
94
+ cookies.store(cookie_name, parsed_cookie)
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
@@ -17,9 +19,13 @@ module Rack
17
19
 
18
20
  flattened_params.map do |name, file|
19
21
  if file.respond_to?(:original_filename)
20
- ::File.open(file.path, 'rb') do |f|
21
- f.set_encoding(Encoding::BINARY)
22
- content_for_tempfile(f, file, name)
22
+ if file.path
23
+ ::File.open(file.path, 'rb') do |f|
24
+ f.set_encoding(Encoding::BINARY)
25
+ content_for_tempfile(f, file, name)
26
+ end
27
+ else
28
+ content_for_tempfile(file, file, name)
23
29
  end
24
30
  else
25
31
  content_for_other(file, name)
@@ -69,12 +75,13 @@ module Rack
69
75
  end
70
76
 
71
77
  def content_for_tempfile(io, file, name)
78
+ length = ::File.stat(file.path).size if file.path
79
+ filename = "; filename=\"#{Utils.escape_path(file.original_filename)}\""
72
80
  <<-EOF
73
81
  --#{MULTIPART_BOUNDARY}\r
74
- Content-Disposition: form-data; name="#{name}"; filename="#{Utils.escape(file.original_filename)}"\r
75
- Content-Type: #{file.content_type}\r
76
- Content-Length: #{::File.stat(file.path).size}\r
77
- \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
78
85
  #{io.read}\r
79
86
  EOF
80
87
  end
@@ -82,7 +89,7 @@ EOF
82
89
  def content_for_other(file, name)
83
90
  <<-EOF
84
91
  --#{MULTIPART_BOUNDARY}\r
85
- Content-Disposition: form-data; name="#{name}"\r
92
+ content-disposition: form-data; name="#{name}"\r
86
93
  \r
87
94
  #{file}\r
88
95
  EOF
@@ -1,23 +1,49 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'rack/utils'
4
3
  require 'strscan'
5
- require 'rack/core_ext/regexp'
4
+
5
+ require_relative '../utils'
6
+ require_relative '../bad_request'
6
7
 
7
8
  module Rack
8
9
  module Multipart
9
- class MultipartPartLimitError < Errno::EMFILE; end
10
+ class MultipartPartLimitError < Errno::EMFILE
11
+ include BadRequest
12
+ end
10
13
 
11
- class Parser
12
- using ::Rack::RegexpExtensions
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
29
+
30
+ # Prefer to use the BoundaryTooLongError class or Rack::BadRequest.
31
+ Error = BoundaryTooLongError
13
32
 
33
+ EOL = "\r\n"
34
+ MULTIPART = %r|\Amultipart/.*boundary=\"?([^\";,]+)\"?|ni
35
+ MULTIPART_CONTENT_TYPE = /Content-Type: (.*)#{EOL}/ni
36
+ MULTIPART_CONTENT_DISPOSITION = /Content-Disposition:(.*)(?=#{EOL}(\S|\z))/ni
37
+ MULTIPART_CONTENT_ID = /Content-ID:\s*([^#{EOL}]*)/ni
38
+
39
+ class Parser
14
40
  BUFSIZE = 1_048_576
15
41
  TEXT_PLAIN = "text/plain"
16
42
  TEMPFILE_FACTORY = lambda { |filename, content_type|
17
- Tempfile.new(["RackMultipart", ::File.extname(filename.gsub("\0", '%00'))])
18
- }
43
+ extension = ::File.extname(filename.gsub("\0", '%00'))[0, 129]
19
44
 
20
- BOUNDARY_REGEX = /\A([^\n]*(?:\n|\Z))/
45
+ Tempfile.new(["RackMultipart", extension])
46
+ }
21
47
 
22
48
  class BoundedIO # :nodoc:
23
49
  def initialize(io, content_length)
@@ -40,16 +66,12 @@ module Rack
40
66
  if str
41
67
  @cursor += str.bytesize
42
68
  else
43
- # Raise an error for mismatching Content-Length and actual contents
69
+ # Raise an error for mismatching content-length and actual contents
44
70
  raise EOFError, "bad content body"
45
71
  end
46
72
 
47
73
  str
48
74
  end
49
-
50
- def rewind
51
- @io.rewind
52
- end
53
75
  end
54
76
 
55
77
  MultipartInfo = Struct.new :params, :tmp_files
@@ -68,18 +90,17 @@ module Rack
68
90
  boundary = parse_boundary content_type
69
91
  return EMPTY unless boundary
70
92
 
93
+ if boundary.length > 70
94
+ # RFC 1521 Section 7.2.1 imposes a 70 character maximum for the boundary.
95
+ # Most clients use no more than 55 characters.
96
+ raise BoundaryTooLongError, "multipart boundary size too large (#{boundary.length} characters)"
97
+ end
98
+
71
99
  io = BoundedIO.new(io, content_length) if content_length
72
- outbuf = String.new
73
100
 
74
101
  parser = new(boundary, tmpfile, bufsize, qp)
75
- parser.on_read io.read(bufsize, outbuf)
102
+ parser.parse(io)
76
103
 
77
- loop do
78
- break if parser.state == :DONE
79
- parser.on_read io.read(bufsize, outbuf)
80
- end
81
-
82
- io.rewind
83
104
  parser.result
84
105
  end
85
106
 
@@ -101,12 +122,6 @@ module Rack
101
122
 
102
123
  data = { filename: fn, type: content_type,
103
124
  name: name, tempfile: body, head: head }
104
- elsif !filename && content_type && body.is_a?(IO)
105
- body.rewind
106
-
107
- # Generic multipart cases, not coming from a form
108
- data = { type: content_type,
109
- name: name, tempfile: body, head: head }
110
125
  end
111
126
 
112
127
  yield data
@@ -125,7 +140,7 @@ module Rack
125
140
 
126
141
  include Enumerable
127
142
 
128
- def initialize tempfile
143
+ def initialize(tempfile)
129
144
  @tempfile = tempfile
130
145
  @mime_parts = []
131
146
  @open_files = 0
@@ -135,7 +150,7 @@ module Rack
135
150
  @mime_parts.each { |part| yield part }
136
151
  end
137
152
 
138
- def on_mime_head mime_index, head, filename, content_type, name
153
+ def on_mime_head(mime_index, head, filename, content_type, name)
139
154
  if filename
140
155
  body = @tempfile.call(filename, content_type)
141
156
  body.binmode if body.respond_to?(:binmode)
@@ -148,25 +163,35 @@ module Rack
148
163
 
149
164
  @mime_parts[mime_index] = klass.new(body, head, filename, content_type, name)
150
165
 
151
- check_open_files
166
+ check_part_limits
152
167
  end
153
168
 
154
- def on_mime_body mime_index, content
169
+ def on_mime_body(mime_index, content)
155
170
  @mime_parts[mime_index].body << content
156
171
  end
157
172
 
158
- def on_mime_finish mime_index
173
+ def on_mime_finish(mime_index)
159
174
  end
160
175
 
161
176
  private
162
177
 
163
- def check_open_files
164
- if Utils.multipart_part_limit > 0
165
- if @open_files >= Utils.multipart_part_limit
178
+ def check_part_limits
179
+ file_limit = Utils.multipart_file_limit
180
+ part_limit = Utils.multipart_total_part_limit
181
+
182
+ if file_limit && file_limit > 0
183
+ if @open_files >= file_limit
166
184
  @mime_parts.each(&:close)
167
185
  raise MultipartPartLimitError, 'Maximum file multiparts in content reached'
168
186
  end
169
187
  end
188
+
189
+ if part_limit && part_limit > 0
190
+ if @mime_parts.size >= part_limit
191
+ @mime_parts.each(&:close)
192
+ raise MultipartTotalPartLimitError, 'Maximum total multiparts in content reached'
193
+ end
194
+ end
170
195
  end
171
196
  end
172
197
 
@@ -175,32 +200,48 @@ module Rack
175
200
  def initialize(boundary, tempfile, bufsize, query_parser)
176
201
  @query_parser = query_parser
177
202
  @params = query_parser.make_params
178
- @boundary = "--#{boundary}"
179
203
  @bufsize = bufsize
180
204
 
181
- @full_boundary = @boundary
182
- @end_boundary = @boundary + '--'
183
205
  @state = :FAST_FORWARD
184
206
  @mime_index = 0
185
207
  @collector = Collector.new tempfile
186
208
 
187
209
  @sbuf = StringScanner.new("".dup)
188
- @body_regex = /(.*?)(#{EOL})?#{Regexp.quote(@boundary)}(#{EOL}|--)/m
189
- @rx_max_size = EOL.size + @boundary.bytesize + [EOL.size, '--'.size].max
210
+ @body_regex = /(?:#{EOL}|\A)--#{Regexp.quote(boundary)}(?:#{EOL}|--)/m
211
+ @body_regex_at_end = /#{@body_regex}\z/m
212
+ @end_boundary_size = boundary.bytesize + 4 # (-- at start, -- at finish)
213
+ @rx_max_size = boundary.bytesize + 6 # (\r\n-- at start, either \r\n or -- at finish)
190
214
  @head_regex = /(.*?#{EOL})#{EOL}/m
191
215
  end
192
216
 
193
- def on_read content
194
- handle_empty_content!(content)
195
- @sbuf.concat content
196
- run_parser
217
+ def parse(io)
218
+ outbuf = String.new
219
+ read_data(io, outbuf)
220
+
221
+ loop do
222
+ status =
223
+ case @state
224
+ when :FAST_FORWARD
225
+ handle_fast_forward
226
+ when :CONSUME_TOKEN
227
+ handle_consume_token
228
+ when :MIME_HEAD
229
+ handle_mime_head
230
+ when :MIME_BODY
231
+ handle_mime_body
232
+ else # when :DONE
233
+ return
234
+ end
235
+
236
+ read_data(io, outbuf) if status == :want_read
237
+ end
197
238
  end
198
239
 
199
240
  def result
200
241
  @collector.each do |part|
201
242
  part.get_data do |data|
202
243
  tag_multipart_encoding(part.filename, part.content_type, part.name, data)
203
- @query_parser.normalize_params(@params, part.name, data, @query_parser.param_depth_limit)
244
+ @query_parser.normalize_params(@params, part.name, data)
204
245
  end
205
246
  end
206
247
  MultipartInfo.new @params.to_params_hash, @collector.find_all(&:file?).map(&:body)
@@ -208,29 +249,45 @@ module Rack
208
249
 
209
250
  private
210
251
 
211
- def run_parser
212
- loop do
213
- case @state
214
- when :FAST_FORWARD
215
- break if handle_fast_forward == :want_read
216
- when :CONSUME_TOKEN
217
- break if handle_consume_token == :want_read
218
- when :MIME_HEAD
219
- break if handle_mime_head == :want_read
220
- when :MIME_BODY
221
- break if handle_mime_body == :want_read
222
- when :DONE
223
- break
224
- end
225
- end
252
+ def dequote(str) # From WEBrick::HTTPUtils
253
+ ret = (/\A"(.*)"\Z/ =~ str) ? $1 : str.dup
254
+ ret.gsub!(/\\(.)/, "\\1")
255
+ ret
226
256
  end
227
257
 
258
+ def read_data(io, outbuf)
259
+ content = io.read(@bufsize, outbuf)
260
+ handle_empty_content!(content)
261
+ @sbuf.concat(content)
262
+ end
263
+
264
+ # This handles the initial parser state. We read until we find the starting
265
+ # boundary, then we can transition to the next state. If we find the ending
266
+ # boundary, this is an invalid multipart upload, but keep scanning for opening
267
+ # boundary in that case. If no boundary found, we need to keep reading data
268
+ # and retry. It's highly unlikely the initial read will not consume the
269
+ # boundary. The client would have to deliberately craft a response
270
+ # with the opening boundary beyond the buffer size for that to happen.
228
271
  def handle_fast_forward
229
- if consume_boundary
230
- @state = :MIME_HEAD
231
- else
232
- raise EOFError, "bad content body" if @sbuf.rest_size >= @bufsize
233
- :want_read
272
+ while true
273
+ case consume_boundary
274
+ when :BOUNDARY
275
+ # found opening boundary, transition to next state
276
+ @state = :MIME_HEAD
277
+ return
278
+ when :END_BOUNDARY
279
+ # invalid multipart upload
280
+ if @sbuf.pos == @end_boundary_size && @sbuf.rest == EOL
281
+ # stop parsing a buffer if a buffer is only an end boundary.
282
+ @state = :DONE
283
+ return
284
+ end
285
+
286
+ # retry for opening boundary
287
+ else
288
+ # no boundary found, keep reading data
289
+ return :want_read
290
+ end
234
291
  end
235
292
  end
236
293
 
@@ -244,17 +301,102 @@ module Rack
244
301
  end
245
302
  end
246
303
 
304
+ CONTENT_DISPOSITION_MAX_PARAMS = 16
305
+ CONTENT_DISPOSITION_MAX_BYTES = 1536
247
306
  def handle_mime_head
248
307
  if @sbuf.scan_until(@head_regex)
249
308
  head = @sbuf[1]
250
309
  content_type = head[MULTIPART_CONTENT_TYPE, 1]
251
- if name = head[MULTIPART_CONTENT_DISPOSITION, 1]
252
- name = Rack::Auth::Digest::Params::dequote(name)
310
+ if (disposition = head[MULTIPART_CONTENT_DISPOSITION, 1]) &&
311
+ disposition.bytesize <= CONTENT_DISPOSITION_MAX_BYTES
312
+
313
+ # ignore actual content-disposition value (should always be form-data)
314
+ i = disposition.index(';')
315
+ disposition.slice!(0, i+1)
316
+ param = nil
317
+ num_params = 0
318
+
319
+ # Parse parameter list
320
+ while i = disposition.index('=')
321
+ # Only parse up to max parameters, to avoid potential denial of service
322
+ num_params += 1
323
+ break if num_params > CONTENT_DISPOSITION_MAX_PARAMS
324
+
325
+ # Found end of parameter name, ensure forward progress in loop
326
+ param = disposition.slice!(0, i+1)
327
+
328
+ # Remove ending equals and preceding whitespace from parameter name
329
+ param.chomp!('=')
330
+ param.lstrip!
331
+
332
+ if disposition[0] == '"'
333
+ # Parameter value is quoted, parse it, handling backslash escapes
334
+ disposition.slice!(0, 1)
335
+ value = String.new
336
+
337
+ while i = disposition.index(/(["\\])/)
338
+ c = $1
339
+
340
+ # Append all content until ending quote or escape
341
+ value << disposition.slice!(0, i)
342
+
343
+ # Remove either backslash or ending quote,
344
+ # ensures forward progress in loop
345
+ disposition.slice!(0, 1)
346
+
347
+ # stop parsing parameter value if found ending quote
348
+ break if c == '"'
349
+
350
+ escaped_char = disposition.slice!(0, 1)
351
+ if param == 'filename' && escaped_char != '"'
352
+ # Possible IE uploaded filename, append both escape backslash and value
353
+ value << c << escaped_char
354
+ else
355
+ # Other only append escaped value
356
+ value << escaped_char
357
+ end
358
+ end
359
+ else
360
+ if i = disposition.index(';')
361
+ # Parameter value unquoted (which may be invalid), value ends at semicolon
362
+ value = disposition.slice!(0, i)
363
+ else
364
+ # If no ending semicolon, assume remainder of line is value and stop
365
+ # parsing
366
+ disposition.strip!
367
+ value = disposition
368
+ disposition = ''
369
+ end
370
+ end
371
+
372
+ case param
373
+ when 'name'
374
+ name = value
375
+ when 'filename'
376
+ filename = value
377
+ when 'filename*'
378
+ filename_star = value
379
+ # else
380
+ # ignore other parameters
381
+ end
382
+
383
+ # skip trailing semicolon, to proceed to next parameter
384
+ if i = disposition.index(';')
385
+ disposition.slice!(0, i+1)
386
+ end
387
+ end
253
388
  else
254
389
  name = head[MULTIPART_CONTENT_ID, 1]
255
390
  end
256
391
 
257
- filename = get_filename(head)
392
+ if filename_star
393
+ encoding, _, filename = filename_star.split("'", 3)
394
+ filename = normalize_filename(filename || '')
395
+ filename.force_encoding(find_encoding(encoding))
396
+ elsif filename
397
+ filename = $1 if filename =~ /^"(.*)"$/
398
+ filename = normalize_filename(filename)
399
+ end
258
400
 
259
401
  if name.nil? || name.empty?
260
402
  name = filename || "#{content_type || TEXT_PLAIN}[]".dup
@@ -268,8 +410,8 @@ module Rack
268
410
  end
269
411
 
270
412
  def handle_mime_body
271
- if @sbuf.check_until(@body_regex) # check but do not advance the pointer yet
272
- body = @sbuf[1]
413
+ if (body_with_boundary = @sbuf.check_until(@body_regex)) # check but do not advance the pointer yet
414
+ body = body_with_boundary.sub(@body_regex_at_end, '') # remove the boundary from the string
273
415
  @collector.on_mime_body @mime_index, body
274
416
  @sbuf.pos += body.length + 2 # skip \r\n after the content
275
417
  @state = :CONSUME_TOKEN
@@ -286,53 +428,31 @@ module Rack
286
428
  end
287
429
  end
288
430
 
289
- def full_boundary; @full_boundary; end
290
-
431
+ # Scan until the we find the start or end of the boundary.
432
+ # If we find it, return the appropriate symbol for the start or
433
+ # end of the boundary. If we don't find the start or end of the
434
+ # boundary, clear the buffer and return nil.
291
435
  def consume_boundary
292
- while read_buffer = @sbuf.scan_until(BOUNDARY_REGEX)
293
- case read_buffer.strip
294
- when full_boundary then return :BOUNDARY
295
- when @end_boundary then return :END_BOUNDARY
296
- end
297
- return if @sbuf.eos?
436
+ if read_buffer = @sbuf.scan_until(@body_regex)
437
+ read_buffer.end_with?(EOL) ? :BOUNDARY : :END_BOUNDARY
438
+ else
439
+ @sbuf.terminate
440
+ nil
298
441
  end
299
442
  end
300
443
 
301
- def get_filename(head)
302
- filename = nil
303
- case head
304
- when RFC2183
305
- params = Hash[*head.scan(DISPPARM).flat_map(&:compact)]
306
-
307
- if filename = params['filename']
308
- filename = $1 if filename =~ /^"(.*)"$/
309
- elsif filename = params['filename*']
310
- encoding, _, filename = filename.split("'", 3)
311
- end
312
- when BROKEN_QUOTED, BROKEN_UNQUOTED
313
- filename = $1
314
- end
315
-
316
- return unless filename
317
-
444
+ def normalize_filename(filename)
318
445
  if filename.scan(/%.?.?/).all? { |s| /%[0-9a-fA-F]{2}/.match?(s) }
319
446
  filename = Utils.unescape_path(filename)
320
447
  end
321
448
 
322
449
  filename.scrub!
323
450
 
324
- if filename !~ /\\[^\\"]/
325
- filename = filename.gsub(/\\(.)/, '\1')
326
- end
327
-
328
- if encoding
329
- filename.force_encoding ::Encoding.find(encoding)
330
- end
331
-
332
- filename
451
+ filename.split(/[\/\\]/).last || String.new
333
452
  end
334
453
 
335
454
  CHARSET = "charset"
455
+ deprecate_constant :CHARSET
336
456
 
337
457
  def tag_multipart_encoding(filename, content_type, name, body)
338
458
  name = name.to_s
@@ -347,13 +467,15 @@ module Rack
347
467
  type_subtype = list.first
348
468
  type_subtype.strip!
349
469
  if TEXT_PLAIN == type_subtype
350
- rest = list.drop 1
470
+ rest = list.drop 1
351
471
  rest.each do |param|
352
472
  k, v = param.split('=', 2)
353
473
  k.strip!
354
474
  v.strip!
355
475
  v = v[1..-2] if v.start_with?('"') && v.end_with?('"')
356
- encoding = Encoding.find v if k == CHARSET
476
+ if k == "charset"
477
+ encoding = find_encoding(v)
478
+ end
357
479
  end
358
480
  end
359
481
  end
@@ -362,9 +484,18 @@ module Rack
362
484
  body.force_encoding(encoding)
363
485
  end
364
486
 
487
+ # Return the related Encoding object. However, because
488
+ # enc is submitted by the user, it may be invalid, so
489
+ # use a binary encoding in that case.
490
+ def find_encoding(enc)
491
+ Encoding.find enc
492
+ rescue ArgumentError
493
+ Encoding::BINARY
494
+ end
495
+
365
496
  def handle_empty_content!(content)
366
497
  if content.nil? || content.empty?
367
- raise EOFError
498
+ raise EmptyContentError
368
499
  end
369
500
  end
370
501
  end