rack 2.2.22 → 3.2.6

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 (87) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +588 -71
  3. data/CONTRIBUTING.md +63 -55
  4. data/MIT-LICENSE +1 -1
  5. data/README.md +384 -0
  6. data/SPEC.rdoc +243 -277
  7. data/lib/rack/auth/abstract/handler.rb +3 -1
  8. data/lib/rack/auth/abstract/request.rb +5 -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 +108 -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 +20 -16
  16. data/lib/rack/constants.rb +68 -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 +10 -4
  21. data/lib/rack/etag.rb +17 -23
  22. data/lib/rack/events.rb +25 -6
  23. data/lib/rack/files.rb +16 -18
  24. data/lib/rack/head.rb +8 -8
  25. data/lib/rack/headers.rb +238 -0
  26. data/lib/rack/lint.rb +817 -648
  27. data/lib/rack/lock.rb +2 -5
  28. data/lib/rack/media_type.rb +6 -7
  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 +156 -0
  34. data/lib/rack/multipart/generator.rb +7 -5
  35. data/lib/rack/multipart/parser.rb +282 -101
  36. data/lib/rack/multipart/uploaded_file.rb +45 -4
  37. data/lib/rack/multipart.rb +53 -40
  38. data/lib/rack/null_logger.rb +9 -0
  39. data/lib/rack/query_parser.rb +116 -121
  40. data/lib/rack/recursive.rb +2 -0
  41. data/lib/rack/reloader.rb +0 -2
  42. data/lib/rack/request.rb +272 -144
  43. data/lib/rack/response.rb +151 -66
  44. data/lib/rack/rewindable_input.rb +27 -5
  45. data/lib/rack/runtime.rb +7 -6
  46. data/lib/rack/sendfile.rb +37 -32
  47. data/lib/rack/show_exceptions.rb +25 -6
  48. data/lib/rack/show_status.rb +17 -9
  49. data/lib/rack/static.rb +15 -11
  50. data/lib/rack/tempfile_reaper.rb +15 -4
  51. data/lib/rack/urlmap.rb +3 -1
  52. data/lib/rack/utils.rb +326 -244
  53. data/lib/rack/version.rb +3 -15
  54. data/lib/rack.rb +13 -90
  55. metadata +15 -41
  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/logger.rb +0 -20
  82. data/lib/rack/server.rb +0 -466
  83. data/lib/rack/session/abstract/id.rb +0 -523
  84. data/lib/rack/session/cookie.rb +0 -203
  85. data/lib/rack/session/memcache.rb +0 -10
  86. data/lib/rack/session/pool.rb +0 -90
  87. data/rack.gemspec +0 -46
@@ -2,14 +2,55 @@
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
12
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
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(\s*)=\"?([^\";,]+)\"?|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
+ # Rack::Multipart::Parser handles parsing of multipart/form-data requests.
42
+ #
43
+ # File Parameter Contents
44
+ #
45
+ # When processing file uploads, the parser returns a hash containing
46
+ # information about uploaded files. For +file+ parameters, the hash includes:
47
+ #
48
+ # * +:filename+ - The original filename, already URL decoded by the parser
49
+ # * +:type+ - The content type of the uploaded file
50
+ # * +:name+ - The parameter name from the form
51
+ # * +:tempfile+ - A Tempfile object containing the uploaded data
52
+ # * +:head+ - The raw header content for this part
53
+ class Parser
13
54
  BUFSIZE = 1_048_576
14
55
  TEXT_PLAIN = "text/plain"
15
56
  TEMPFILE_FACTORY = lambda { |filename, content_type|
@@ -18,8 +59,6 @@ module Rack
18
59
  Tempfile.new(["RackMultipart", extension])
19
60
  }
20
61
 
21
- BOUNDARY_REGEX = /\A([^\n]*(?:\n|\Z))/
22
-
23
62
  BOUNDARY_START_LIMIT = 16 * 1024
24
63
  private_constant :BOUNDARY_START_LIMIT
25
64
 
@@ -41,6 +80,13 @@ module Rack
41
80
  BUFFERED_UPLOAD_BYTESIZE_LIMIT = env_int.call("RACK_MULTIPART_BUFFERED_UPLOAD_BYTESIZE_LIMIT", 16 * 1024 * 1024)
42
81
  private_constant :BUFFERED_UPLOAD_BYTESIZE_LIMIT
43
82
 
83
+ bytesize_limit = env_int.call("RACK_MULTIPART_PARSER_BYTESIZE_LIMIT", 10 * 1024 * 1024 * 1024)
84
+ PARSER_BYTESIZE_LIMIT = bytesize_limit > 0 ? bytesize_limit : nil
85
+ private_constant :PARSER_BYTESIZE_LIMIT
86
+
87
+ CONTENT_DISPOSITION_QUOTED_ESCAPES_LIMIT = env_int.call("RACK_MULTIPART_CONTENT_DISPOSITION_QUOTED_ESCAPES_LIMIT", 8 * 1024)
88
+ private_constant :CONTENT_DISPOSITION_QUOTED_ESCAPES_LIMIT
89
+
44
90
  class BoundedIO # :nodoc:
45
91
  def initialize(io, content_length)
46
92
  @io = io
@@ -62,16 +108,12 @@ module Rack
62
108
  if str
63
109
  @cursor += str.bytesize
64
110
  else
65
- # Raise an error for mismatching Content-Length and actual contents
111
+ # Raise an error for mismatching content-length and actual contents
66
112
  raise EOFError, "bad content body"
67
113
  end
68
114
 
69
115
  str
70
116
  end
71
-
72
- def rewind
73
- @io.rewind
74
- end
75
117
  end
76
118
 
77
119
  MultipartInfo = Struct.new :params, :tmp_files
@@ -81,7 +123,15 @@ module Rack
81
123
  return unless content_type
82
124
  data = content_type.match(MULTIPART)
83
125
  return unless data
84
- data[1]
126
+
127
+ unless data[1].empty?
128
+ raise Error, "whitespace between boundary parameter name and equal sign"
129
+ end
130
+ if data.post_match.match?(/boundary\s*=/i)
131
+ raise BoundaryTooLongError, "multiple boundary parameters found in multipart content type"
132
+ end
133
+
134
+ data[2]
85
135
  end
86
136
 
87
137
  def self.parse(io, content_length, content_type, tmpfile, bufsize, qp)
@@ -90,18 +140,21 @@ module Rack
90
140
  boundary = parse_boundary content_type
91
141
  return EMPTY unless boundary
92
142
 
143
+ if PARSER_BYTESIZE_LIMIT && content_length && content_length > PARSER_BYTESIZE_LIMIT
144
+ raise Error, "multipart Content-Length #{content_length} exceeds limit of #{PARSER_BYTESIZE_LIMIT} bytes"
145
+ end
146
+
147
+ if boundary.length > 70
148
+ # RFC 1521 Section 7.2.1 imposes a 70 character maximum for the boundary.
149
+ # Most clients use no more than 55 characters.
150
+ raise BoundaryTooLongError, "multipart boundary size too large (#{boundary.length} characters)"
151
+ end
152
+
93
153
  io = BoundedIO.new(io, content_length) if content_length
94
- outbuf = String.new
95
154
 
96
155
  parser = new(boundary, tmpfile, bufsize, qp)
97
- parser.on_read io.read(bufsize, outbuf)
156
+ parser.parse(io)
98
157
 
99
- loop do
100
- break if parser.state == :DONE
101
- parser.on_read io.read(bufsize, outbuf)
102
- end
103
-
104
- io.rewind
105
158
  parser.result
106
159
  end
107
160
 
@@ -201,35 +254,54 @@ module Rack
201
254
  def initialize(boundary, tempfile, bufsize, query_parser)
202
255
  @query_parser = query_parser
203
256
  @params = query_parser.make_params
204
- @boundary = "--#{boundary}"
205
257
  @bufsize = bufsize
206
258
 
207
- @full_boundary = @boundary
208
- @end_boundary = @boundary + '--'
209
259
  @state = :FAST_FORWARD
210
260
  @mime_index = 0
211
261
  @body_retained = nil
212
262
  @retained_size = 0
263
+ @total_bytes_read = (0 if PARSER_BYTESIZE_LIMIT)
264
+ @content_disposition_quoted_escapes = 0
213
265
  @collector = Collector.new tempfile
214
266
 
215
267
  @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
268
+ @body_regex = /(?:#{EOL}|\A)--#{Regexp.quote(boundary)}(?:#{EOL}|--)/m
269
+ @body_regex_at_end = /#{@body_regex}\z/m
270
+ @end_boundary_size = boundary.bytesize + 4 # (-- at start, -- at finish)
271
+ @rx_max_size = boundary.bytesize + 6 # (\r\n-- at start, either \r\n or -- at finish)
219
272
  @head_regex = /(.*?#{EOL})#{EOL}/m
220
273
  end
221
274
 
222
- def on_read(content)
223
- handle_empty_content!(content)
224
- @sbuf.concat content
225
- run_parser
275
+ def parse(io)
276
+ @total_bytes_read &&= nil if io.is_a?(BoundedIO)
277
+ outbuf = String.new
278
+ read_data(io, outbuf)
279
+
280
+ loop do
281
+ status =
282
+ case @state
283
+ when :FAST_FORWARD
284
+ handle_fast_forward
285
+ when :CONSUME_TOKEN
286
+ handle_consume_token
287
+ when :MIME_HEAD
288
+ handle_mime_head
289
+ when :MIME_BODY
290
+ handle_mime_body
291
+ else # when :DONE
292
+ return
293
+ end
294
+
295
+ read_data(io, outbuf) if status == :want_read
296
+ end
226
297
  end
227
298
 
228
299
  def result
229
300
  @collector.each do |part|
230
301
  part.get_data do |data|
231
302
  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)
303
+ name, data = handle_dummy_encoding(part.name, data)
304
+ @query_parser.normalize_params(@params, name, data)
233
305
  end
234
306
  end
235
307
  MultipartInfo.new @params.to_params_hash, @collector.find_all(&:file?).map(&:body)
@@ -237,40 +309,49 @@ module Rack
237
309
 
238
310
  private
239
311
 
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
312
+ def read_data(io, outbuf)
313
+ content = io.read(@bufsize, outbuf)
314
+ handle_empty_content!(content)
315
+ if @total_bytes_read
316
+ @total_bytes_read += content.bytesize
317
+ if @total_bytes_read > PARSER_BYTESIZE_LIMIT
318
+ raise Error, "multipart upload exceeds limit of #{PARSER_BYTESIZE_LIMIT} bytes"
253
319
  end
254
320
  end
321
+ @sbuf.concat(content)
255
322
  end
256
323
 
324
+ # This handles the initial parser state. We read until we find the starting
325
+ # boundary, then we can transition to the next state. If we find the ending
326
+ # boundary, this is an invalid multipart upload, but keep scanning for opening
327
+ # boundary in that case. If no boundary found, we need to keep reading data
328
+ # and retry. It's highly unlikely the initial read will not consume the
329
+ # boundary. The client would have to deliberately craft a response
330
+ # with the opening boundary beyond the buffer size for that to happen.
257
331
  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
332
+ while true
333
+ case consume_boundary
334
+ when :BOUNDARY
335
+ # found opening boundary, transition to next state
336
+ @state = :MIME_HEAD
337
+ return
338
+ when :END_BOUNDARY
339
+ # invalid multipart upload
340
+ if @sbuf.pos == @end_boundary_size && @sbuf.rest == EOL
341
+ # stop parsing a buffer if a buffer is only an end boundary.
342
+ @state = :DONE
343
+ return
344
+ end
267
345
 
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
346
+ # retry for opening boundary
347
+ else
348
+ # We raise if we don't find the multipart boundary, to avoid unbounded memory
349
+ # buffering. Note that the actual limit is the higher of 16KB and the buffer size (1MB by default)
350
+ raise Error, "multipart boundary not found within limit" if @sbuf.string.bytesize > BOUNDARY_START_LIMIT
271
351
 
272
- # no boundary found, keep reading data
273
- return :want_read
352
+ # no boundary found, keep reading data
353
+ return :want_read
354
+ end
274
355
  end
275
356
  end
276
357
 
@@ -284,17 +365,114 @@ module Rack
284
365
  end
285
366
  end
286
367
 
368
+ CONTENT_DISPOSITION_MAX_PARAMS = 16
369
+ CONTENT_DISPOSITION_MAX_BYTES = 1536
370
+ OBS_UNFOLD = /\r\n([ \t])/
371
+ private_constant :OBS_UNFOLD
372
+
287
373
  def handle_mime_head
288
374
  if @sbuf.scan_until(@head_regex)
289
375
  head = @sbuf[1]
290
376
  content_type = head[MULTIPART_CONTENT_TYPE, 1]
291
- if name = head[MULTIPART_CONTENT_DISPOSITION, 1]
292
- name = Rack::Auth::Digest::Params::dequote(name)
377
+ content_type.gsub!(OBS_UNFOLD, '\1') if content_type
378
+
379
+ if (disposition = head[MULTIPART_CONTENT_DISPOSITION, 1]) &&
380
+ disposition.bytesize <= CONTENT_DISPOSITION_MAX_BYTES
381
+
382
+ # Implement OBS unfolding (RFC 5322 Section 2.2.3)
383
+ disposition.gsub!(OBS_UNFOLD, '\1')
384
+
385
+ # ignore actual content-disposition value (should always be form-data)
386
+ i = disposition.index(';')
387
+ disposition.slice!(0, i+1)
388
+ param = nil
389
+ num_params = 0
390
+
391
+ # Parse parameter list
392
+ while i = disposition.index('=')
393
+ # Only parse up to max parameters, to avoid potential denial of service
394
+ num_params += 1
395
+ break if num_params > CONTENT_DISPOSITION_MAX_PARAMS
396
+
397
+ # Found end of parameter name, ensure forward progress in loop
398
+ param = disposition.slice!(0, i+1)
399
+
400
+ # Remove ending equals and preceding whitespace from parameter name
401
+ param.chomp!('=')
402
+ param.lstrip!
403
+
404
+ if disposition[0] == '"'
405
+ # Parameter value is quoted, parse it, handling backslash escapes
406
+ disposition.slice!(0, 1)
407
+ value = String.new
408
+
409
+ while i = disposition.index(/(["\\])/)
410
+ c = $1
411
+
412
+ # Append all content until ending quote or escape
413
+ value << disposition.slice!(0, i)
414
+
415
+ # Remove either backslash or ending quote,
416
+ # ensures forward progress in loop
417
+ disposition.slice!(0, 1)
418
+
419
+ # stop parsing parameter value if found ending quote
420
+ break if c == '"'
421
+
422
+ @content_disposition_quoted_escapes += 1
423
+ if @content_disposition_quoted_escapes > CONTENT_DISPOSITION_QUOTED_ESCAPES_LIMIT
424
+ raise Error, "number of quoted escapes during content disposition parsing exceeds limit"
425
+ end
426
+
427
+ escaped_char = disposition.slice!(0, 1)
428
+ if param == 'filename' && escaped_char != '"'
429
+ # Possible IE uploaded filename, append both escape backslash and value
430
+ value << c << escaped_char
431
+ else
432
+ # Other only append escaped value
433
+ value << escaped_char
434
+ end
435
+ end
436
+ else
437
+ if i = disposition.index(';')
438
+ # Parameter value unquoted (which may be invalid), value ends at semicolon
439
+ value = disposition.slice!(0, i)
440
+ else
441
+ # If no ending semicolon, assume remainder of line is value and stop
442
+ # parsing
443
+ disposition.strip!
444
+ value = disposition
445
+ disposition = ''
446
+ end
447
+ end
448
+
449
+ case param
450
+ when 'name'
451
+ name = value
452
+ when 'filename'
453
+ filename = value
454
+ when 'filename*'
455
+ filename_star = value
456
+ # else
457
+ # ignore other parameters
458
+ end
459
+
460
+ # skip trailing semicolon, to proceed to next parameter
461
+ if i = disposition.index(';')
462
+ disposition.slice!(0, i+1)
463
+ end
464
+ end
293
465
  else
294
466
  name = head[MULTIPART_CONTENT_ID, 1]
295
467
  end
296
468
 
297
- filename = get_filename(head)
469
+ if filename_star
470
+ encoding, _, filename = filename_star.split("'", 3)
471
+ filename = normalize_filename(filename || '')
472
+ filename.force_encoding(find_encoding(encoding))
473
+ elsif filename
474
+ filename = normalize_filename(filename)
475
+ end
298
476
 
299
477
  if name.nil? || name.empty?
300
478
  name = filename || "#{content_type || TEXT_PLAIN}[]".dup
@@ -314,7 +492,7 @@ module Rack
314
492
  else
315
493
  # We raise if the mime part header is too large, to avoid unbounded memory
316
494
  # 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
495
+ raise Error, "multipart mime part header too large" if @sbuf.rest.bytesize > MIME_HEADER_BYTESIZE_LIMIT
318
496
 
319
497
  return :want_read
320
498
  end
@@ -322,7 +500,7 @@ module Rack
322
500
 
323
501
  def handle_mime_body
324
502
  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
503
+ body = body_with_boundary.sub(@body_regex_at_end, '') # remove the boundary from the string
326
504
  update_retained_size(body.bytesize) if @body_retained
327
505
  @collector.on_mime_body @mime_index, body
328
506
  @sbuf.pos += body.length + 2 # skip \r\n after the content
@@ -342,12 +520,10 @@ module Rack
342
520
  end
343
521
  end
344
522
 
345
- def full_boundary; @full_boundary; end
346
-
347
523
  def update_retained_size(size)
348
524
  @retained_size += size
349
525
  if @retained_size > BUFFERED_UPLOAD_BYTESIZE_LIMIT
350
- raise EOFError, "multipart data over retained size limit"
526
+ raise Error, "multipart data over retained size limit"
351
527
  end
352
528
  end
353
529
 
@@ -356,51 +532,26 @@ module Rack
356
532
  # end of the boundary. If we don't find the start or end of the
357
533
  # boundary, clear the buffer and return nil.
358
534
  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?
535
+ if read_buffer = @sbuf.scan_until(@body_regex)
536
+ read_buffer.end_with?(EOL) ? :BOUNDARY : :END_BOUNDARY
537
+ else
538
+ @sbuf.terminate
539
+ nil
365
540
  end
366
541
  end
367
542
 
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
-
543
+ def normalize_filename(filename)
386
544
  if filename.scan(/%.?.?/).all? { |s| /%[0-9a-fA-F]{2}/.match?(s) }
387
545
  filename = Utils.unescape_path(filename)
388
546
  end
389
547
 
390
548
  filename.scrub!
391
549
 
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
550
+ filename.split(/[\/\\]/).last || String.new
401
551
  end
402
552
 
403
553
  CHARSET = "charset"
554
+ deprecate_constant :CHARSET
404
555
 
405
556
  def tag_multipart_encoding(filename, content_type, name, body)
406
557
  name = name.to_s
@@ -421,7 +572,9 @@ module Rack
421
572
  k.strip!
422
573
  v.strip!
423
574
  v = v[1..-2] if v.start_with?('"') && v.end_with?('"')
424
- encoding = Encoding.find v if k == CHARSET
575
+ if k == "charset"
576
+ encoding = find_encoding(v)
577
+ end
425
578
  end
426
579
  end
427
580
  end
@@ -430,9 +583,37 @@ module Rack
430
583
  body.force_encoding(encoding)
431
584
  end
432
585
 
586
+ # Return the related Encoding object. However, because
587
+ # enc is submitted by the user, it may be invalid, so
588
+ # use a binary encoding in that case.
589
+ def find_encoding(enc)
590
+ Encoding.find enc
591
+ rescue ArgumentError
592
+ Encoding::BINARY
593
+ end
594
+
595
+ REENCODE_DUMMY_ENCODINGS = {
596
+ # ISO-2022-JP is a legacy but still widely used encoding in Japan
597
+ # Here we convert ISO-2022-JP to UTF-8 so that it can be handled.
598
+ Encoding::ISO_2022_JP => true
599
+
600
+ # Other dummy encodings are rarely used and have not been supported yet.
601
+ # Adding support for them will require careful considerations.
602
+ }
603
+
604
+ def handle_dummy_encoding(name, body)
605
+ # A string object with a 'dummy' encoding does not have full functionality and can cause errors.
606
+ # So here we covert it to UTF-8 so that it can be handled properly.
607
+ if name.encoding.dummy? && REENCODE_DUMMY_ENCODINGS[name.encoding]
608
+ name = name.encode(Encoding::UTF_8)
609
+ body = body.encode(Encoding::UTF_8)
610
+ end
611
+ return name, body
612
+ end
613
+
433
614
  def handle_empty_content!(content)
434
615
  if content.nil? || content.empty?
435
- raise EOFError
616
+ raise EmptyContentError
436
617
  end
437
618
  end
438
619
  end
@@ -1,14 +1,51 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'tempfile'
4
+ require 'fileutils'
5
+
3
6
  module Rack
4
7
  module Multipart
8
+ # Despite the misleading name, UploadedFile is designed for use for
9
+ # preparing multipart file upload bodies, generally for use in tests.
10
+ # It is not designed for and should not be used for handling uploaded
11
+ # files (there is no need for that, since Rack's multipart parser
12
+ # already creates Tempfiles for that). Using this with non-trusted
13
+ # filenames can create a security vulnerability.
14
+ #
15
+ # You should only use this class if you plan on passing the instances
16
+ # to Rack::MockRequest for use in creating multipart request bodies.
17
+ #
18
+ # UploadedFile delegates most methods to the tempfile it contains.
5
19
  class UploadedFile
6
- # The filename, *not* including the path, of the "uploaded" file
20
+ # The provided name of the file. This generally is the basename of
21
+ # path provided during initialization, but it can contain slashes if they
22
+ # were present in the filename argument when the instance was created.
7
23
  attr_reader :original_filename
8
24
 
9
- # The content type of the "uploaded" file
25
+ # The content type of the instance.
10
26
  attr_accessor :content_type
11
27
 
28
+ # Create a new UploadedFile. For backwards compatibility, this accepts
29
+ # both positional and keyword versions of the same arguments:
30
+ #
31
+ # filepath/path :: The path to the file
32
+ # ct/content_type :: The content_type of the file
33
+ # bin/binary :: Whether to set binmode on the file before copying data into it.
34
+ #
35
+ # If both positional and keyword arguments are present, the keyword arguments
36
+ # take precedence.
37
+ #
38
+ # The following keyword-only arguments are also accepted:
39
+ #
40
+ # filename :: Override the filename to use for the file. This is so the
41
+ # filename for the upload does not need to match the basename of
42
+ # the file path. This should not contain slashes, unless you are
43
+ # trying to test how an application handles invalid filenames in
44
+ # multipart upload bodies.
45
+ # io :: Use the given IO-like instance as the tempfile, instead of creating
46
+ # a Tempfile instance. This is useful for building multipart file
47
+ # upload bodies without a file being present on the filesystem. If you are
48
+ # providing this, you should also provide the filename argument.
12
49
  def initialize(filepath = nil, ct = "text/plain", bin = false,
13
50
  path: filepath, content_type: ct, binary: bin, filename: nil, io: nil)
14
51
  if io
@@ -24,15 +61,19 @@ module Rack
24
61
  @content_type = content_type
25
62
  end
26
63
 
64
+ # The path of the tempfile for the instance, if the tempfile has a path.
65
+ # nil if the tempfile does not have a path.
27
66
  def path
28
67
  @tempfile.path if @tempfile.respond_to?(:path)
29
68
  end
30
69
  alias_method :local_path, :path
31
70
 
32
- def respond_to?(*args)
33
- super or @tempfile.respond_to?(*args)
71
+ # Return true if the tempfile responds to the method.
72
+ def respond_to_missing?(*args)
73
+ @tempfile.respond_to?(*args)
34
74
  end
35
75
 
76
+ # Delegate method missing calls to the tempfile.
36
77
  def method_missing(method_name, *args, &block) #:nodoc:
37
78
  @tempfile.__send__(method_name, *args, &block)
38
79
  end