rack 2.2.23 → 3.2.5

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 +554 -83
  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 +240 -123
  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 +11 -15
  50. data/lib/rack/tempfile_reaper.rb +15 -4
  51. data/lib/rack/urlmap.rb +3 -1
  52. data/lib/rack/utils.rb +234 -275
  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=\"?([^\";,]+)\"?|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,10 +80,6 @@ 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
 
44
- bytesize_limit = env_int.call("RACK_MULTIPART_PARSER_BYTESIZE_LIMIT", 10 * 1024 * 1024 * 1024)
45
- PARSER_BYTESIZE_LIMIT = bytesize_limit > 0 ? bytesize_limit : nil
46
- private_constant :PARSER_BYTESIZE_LIMIT
47
-
48
83
  class BoundedIO # :nodoc:
49
84
  def initialize(io, content_length)
50
85
  @io = io
@@ -66,16 +101,12 @@ module Rack
66
101
  if str
67
102
  @cursor += str.bytesize
68
103
  else
69
- # Raise an error for mismatching Content-Length and actual contents
104
+ # Raise an error for mismatching content-length and actual contents
70
105
  raise EOFError, "bad content body"
71
106
  end
72
107
 
73
108
  str
74
109
  end
75
-
76
- def rewind
77
- @io.rewind
78
- end
79
110
  end
80
111
 
81
112
  MultipartInfo = Struct.new :params, :tmp_files
@@ -85,15 +116,7 @@ module Rack
85
116
  return unless content_type
86
117
  data = content_type.match(MULTIPART)
87
118
  return unless data
88
-
89
- unless data[1].empty?
90
- raise EOFError, "whitespace between boundary parameter name and equal sign"
91
- end
92
- if data.post_match =~ /boundary\s*=/i
93
- raise EOFError, "multiple boundary parameters found in multipart content type"
94
- end
95
-
96
- data[2]
119
+ data[1]
97
120
  end
98
121
 
99
122
  def self.parse(io, content_length, content_type, tmpfile, bufsize, qp)
@@ -102,22 +125,17 @@ module Rack
102
125
  boundary = parse_boundary content_type
103
126
  return EMPTY unless boundary
104
127
 
105
- if PARSER_BYTESIZE_LIMIT && content_length && content_length > PARSER_BYTESIZE_LIMIT
106
- raise EOFError, "multipart Content-Length #{content_length} exceeds limit of #{PARSER_BYTESIZE_LIMIT} bytes"
128
+ if boundary.length > 70
129
+ # RFC 1521 Section 7.2.1 imposes a 70 character maximum for the boundary.
130
+ # Most clients use no more than 55 characters.
131
+ raise BoundaryTooLongError, "multipart boundary size too large (#{boundary.length} characters)"
107
132
  end
108
133
 
109
134
  io = BoundedIO.new(io, content_length) if content_length
110
- outbuf = String.new
111
135
 
112
136
  parser = new(boundary, tmpfile, bufsize, qp)
113
- parser.on_read io.read(bufsize, outbuf)
114
-
115
- loop do
116
- break if parser.state == :DONE
117
- parser.on_read io.read(bufsize, outbuf)
118
- end
137
+ parser.parse(io)
119
138
 
120
- io.rewind
121
139
  parser.result
122
140
  end
123
141
 
@@ -217,42 +235,51 @@ module Rack
217
235
  def initialize(boundary, tempfile, bufsize, query_parser)
218
236
  @query_parser = query_parser
219
237
  @params = query_parser.make_params
220
- @boundary = "--#{boundary}"
221
238
  @bufsize = bufsize
222
239
 
223
- @full_boundary = @boundary
224
- @end_boundary = @boundary + '--'
225
240
  @state = :FAST_FORWARD
226
241
  @mime_index = 0
227
242
  @body_retained = nil
228
243
  @retained_size = 0
229
- @total_bytes_read = (0 if PARSER_BYTESIZE_LIMIT)
230
244
  @collector = Collector.new tempfile
231
245
 
232
246
  @sbuf = StringScanner.new("".dup)
233
- @body_regex = /(?:#{EOL})?#{Regexp.quote(@boundary)}(?:#{EOL}|--)/m
234
- @end_boundary_size = boundary.bytesize + 6 # (-- at start, -- at finish, EOL at end)
235
- @rx_max_size = EOL.size + @boundary.bytesize + [EOL.size, '--'.size].max
247
+ @body_regex = /(?:#{EOL}|\A)--#{Regexp.quote(boundary)}(?:#{EOL}|--)/m
248
+ @body_regex_at_end = /#{@body_regex}\z/m
249
+ @end_boundary_size = boundary.bytesize + 4 # (-- at start, -- at finish)
250
+ @rx_max_size = boundary.bytesize + 6 # (\r\n-- at start, either \r\n or -- at finish)
236
251
  @head_regex = /(.*?#{EOL})#{EOL}/m
237
252
  end
238
253
 
239
- def on_read(content)
240
- handle_empty_content!(content)
241
- if @total_bytes_read
242
- @total_bytes_read += content.bytesize
243
- if @total_bytes_read > PARSER_BYTESIZE_LIMIT
244
- raise EOFError, "multipart upload exceeds limit of #{PARSER_BYTESIZE_LIMIT} bytes"
245
- end
254
+ def parse(io)
255
+ outbuf = String.new
256
+ read_data(io, outbuf)
257
+
258
+ loop do
259
+ status =
260
+ case @state
261
+ when :FAST_FORWARD
262
+ handle_fast_forward
263
+ when :CONSUME_TOKEN
264
+ handle_consume_token
265
+ when :MIME_HEAD
266
+ handle_mime_head
267
+ when :MIME_BODY
268
+ handle_mime_body
269
+ else # when :DONE
270
+ return
271
+ end
272
+
273
+ read_data(io, outbuf) if status == :want_read
246
274
  end
247
- @sbuf.concat content
248
- run_parser
249
275
  end
250
276
 
251
277
  def result
252
278
  @collector.each do |part|
253
279
  part.get_data do |data|
254
280
  tag_multipart_encoding(part.filename, part.content_type, part.name, data)
255
- @query_parser.normalize_params(@params, part.name, data, @query_parser.param_depth_limit)
281
+ name, data = handle_dummy_encoding(part.name, data)
282
+ @query_parser.normalize_params(@params, name, data)
256
283
  end
257
284
  end
258
285
  MultipartInfo.new @params.to_params_hash, @collector.find_all(&:file?).map(&:body)
@@ -260,40 +287,43 @@ module Rack
260
287
 
261
288
  private
262
289
 
263
- def run_parser
264
- loop do
265
- case @state
266
- when :FAST_FORWARD
267
- break if handle_fast_forward == :want_read
268
- when :CONSUME_TOKEN
269
- break if handle_consume_token == :want_read
270
- when :MIME_HEAD
271
- break if handle_mime_head == :want_read
272
- when :MIME_BODY
273
- break if handle_mime_body == :want_read
274
- when :DONE
275
- break
276
- end
277
- end
290
+ def read_data(io, outbuf)
291
+ content = io.read(@bufsize, outbuf)
292
+ handle_empty_content!(content)
293
+ @sbuf.concat(content)
278
294
  end
279
295
 
296
+ # This handles the initial parser state. We read until we find the starting
297
+ # boundary, then we can transition to the next state. If we find the ending
298
+ # boundary, this is an invalid multipart upload, but keep scanning for opening
299
+ # boundary in that case. If no boundary found, we need to keep reading data
300
+ # and retry. It's highly unlikely the initial read will not consume the
301
+ # boundary. The client would have to deliberately craft a response
302
+ # with the opening boundary beyond the buffer size for that to happen.
280
303
  def handle_fast_forward
281
- tok = consume_boundary
282
-
283
- if tok == :END_BOUNDARY && @sbuf.pos == @end_boundary_size && @sbuf.eos?
284
- # stop parsing a buffer if a buffer is only an end boundary.
285
- @state = :DONE
286
- elsif tok
287
- @state = :MIME_HEAD
288
- else
289
- raise EOFError, "bad content body" if @sbuf.rest_size >= @bufsize
304
+ while true
305
+ case consume_boundary
306
+ when :BOUNDARY
307
+ # found opening boundary, transition to next state
308
+ @state = :MIME_HEAD
309
+ return
310
+ when :END_BOUNDARY
311
+ # invalid multipart upload
312
+ if @sbuf.pos == @end_boundary_size && @sbuf.rest == EOL
313
+ # stop parsing a buffer if a buffer is only an end boundary.
314
+ @state = :DONE
315
+ return
316
+ end
290
317
 
291
- # We raise if we don't find the multipart boundary, to avoid unbounded memory
292
- # buffering. Note that the actual limit is the higher of 16KB and the buffer size (1MB by default)
293
- raise EOFError, "multipart boundary not found within limit" if @sbuf.string.bytesize > BOUNDARY_START_LIMIT
318
+ # retry for opening boundary
319
+ else
320
+ # We raise if we don't find the multipart boundary, to avoid unbounded memory
321
+ # buffering. Note that the actual limit is the higher of 16KB and the buffer size (1MB by default)
322
+ raise Error, "multipart boundary not found within limit" if @sbuf.string.bytesize > BOUNDARY_START_LIMIT
294
323
 
295
- # no boundary found, keep reading data
296
- return :want_read
324
+ # no boundary found, keep reading data
325
+ return :want_read
326
+ end
297
327
  end
298
328
  end
299
329
 
@@ -307,17 +337,101 @@ module Rack
307
337
  end
308
338
  end
309
339
 
340
+ CONTENT_DISPOSITION_MAX_PARAMS = 16
341
+ CONTENT_DISPOSITION_MAX_BYTES = 1536
310
342
  def handle_mime_head
311
343
  if @sbuf.scan_until(@head_regex)
312
344
  head = @sbuf[1]
313
345
  content_type = head[MULTIPART_CONTENT_TYPE, 1]
314
- if name = head[MULTIPART_CONTENT_DISPOSITION, 1]
315
- name = Rack::Auth::Digest::Params::dequote(name)
346
+ if (disposition = head[MULTIPART_CONTENT_DISPOSITION, 1]) &&
347
+ disposition.bytesize <= CONTENT_DISPOSITION_MAX_BYTES
348
+
349
+ # ignore actual content-disposition value (should always be form-data)
350
+ i = disposition.index(';')
351
+ disposition.slice!(0, i+1)
352
+ param = nil
353
+ num_params = 0
354
+
355
+ # Parse parameter list
356
+ while i = disposition.index('=')
357
+ # Only parse up to max parameters, to avoid potential denial of service
358
+ num_params += 1
359
+ break if num_params > CONTENT_DISPOSITION_MAX_PARAMS
360
+
361
+ # Found end of parameter name, ensure forward progress in loop
362
+ param = disposition.slice!(0, i+1)
363
+
364
+ # Remove ending equals and preceding whitespace from parameter name
365
+ param.chomp!('=')
366
+ param.lstrip!
367
+
368
+ if disposition[0] == '"'
369
+ # Parameter value is quoted, parse it, handling backslash escapes
370
+ disposition.slice!(0, 1)
371
+ value = String.new
372
+
373
+ while i = disposition.index(/(["\\])/)
374
+ c = $1
375
+
376
+ # Append all content until ending quote or escape
377
+ value << disposition.slice!(0, i)
378
+
379
+ # Remove either backslash or ending quote,
380
+ # ensures forward progress in loop
381
+ disposition.slice!(0, 1)
382
+
383
+ # stop parsing parameter value if found ending quote
384
+ break if c == '"'
385
+
386
+ escaped_char = disposition.slice!(0, 1)
387
+ if param == 'filename' && escaped_char != '"'
388
+ # Possible IE uploaded filename, append both escape backslash and value
389
+ value << c << escaped_char
390
+ else
391
+ # Other only append escaped value
392
+ value << escaped_char
393
+ end
394
+ end
395
+ else
396
+ if i = disposition.index(';')
397
+ # Parameter value unquoted (which may be invalid), value ends at semicolon
398
+ value = disposition.slice!(0, i)
399
+ else
400
+ # If no ending semicolon, assume remainder of line is value and stop
401
+ # parsing
402
+ disposition.strip!
403
+ value = disposition
404
+ disposition = ''
405
+ end
406
+ end
407
+
408
+ case param
409
+ when 'name'
410
+ name = value
411
+ when 'filename'
412
+ filename = value
413
+ when 'filename*'
414
+ filename_star = value
415
+ # else
416
+ # ignore other parameters
417
+ end
418
+
419
+ # skip trailing semicolon, to proceed to next parameter
420
+ if i = disposition.index(';')
421
+ disposition.slice!(0, i+1)
422
+ end
423
+ end
316
424
  else
317
425
  name = head[MULTIPART_CONTENT_ID, 1]
318
426
  end
319
427
 
320
- filename = get_filename(head)
428
+ if filename_star
429
+ encoding, _, filename = filename_star.split("'", 3)
430
+ filename = normalize_filename(filename || '')
431
+ filename.force_encoding(find_encoding(encoding))
432
+ elsif filename
433
+ filename = normalize_filename(filename)
434
+ end
321
435
 
322
436
  if name.nil? || name.empty?
323
437
  name = filename || "#{content_type || TEXT_PLAIN}[]".dup
@@ -337,7 +451,7 @@ module Rack
337
451
  else
338
452
  # We raise if the mime part header is too large, to avoid unbounded memory
339
453
  # buffering. Note that the actual limit is the higher of 64KB and the buffer size (1MB by default)
340
- raise EOFError, "multipart mime part header too large" if @sbuf.rest.bytesize > MIME_HEADER_BYTESIZE_LIMIT
454
+ raise Error, "multipart mime part header too large" if @sbuf.rest.bytesize > MIME_HEADER_BYTESIZE_LIMIT
341
455
 
342
456
  return :want_read
343
457
  end
@@ -345,7 +459,7 @@ module Rack
345
459
 
346
460
  def handle_mime_body
347
461
  if (body_with_boundary = @sbuf.check_until(@body_regex)) # check but do not advance the pointer yet
348
- body = body_with_boundary.sub(/#{@body_regex}\z/m, '') # remove the boundary from the string
462
+ body = body_with_boundary.sub(@body_regex_at_end, '') # remove the boundary from the string
349
463
  update_retained_size(body.bytesize) if @body_retained
350
464
  @collector.on_mime_body @mime_index, body
351
465
  @sbuf.pos += body.length + 2 # skip \r\n after the content
@@ -365,12 +479,10 @@ module Rack
365
479
  end
366
480
  end
367
481
 
368
- def full_boundary; @full_boundary; end
369
-
370
482
  def update_retained_size(size)
371
483
  @retained_size += size
372
484
  if @retained_size > BUFFERED_UPLOAD_BYTESIZE_LIMIT
373
- raise EOFError, "multipart data over retained size limit"
485
+ raise Error, "multipart data over retained size limit"
374
486
  end
375
487
  end
376
488
 
@@ -379,51 +491,26 @@ module Rack
379
491
  # end of the boundary. If we don't find the start or end of the
380
492
  # boundary, clear the buffer and return nil.
381
493
  def consume_boundary
382
- while read_buffer = @sbuf.scan_until(BOUNDARY_REGEX)
383
- case read_buffer.strip
384
- when full_boundary then return :BOUNDARY
385
- when @end_boundary then return :END_BOUNDARY
386
- end
387
- return if @sbuf.eos?
494
+ if read_buffer = @sbuf.scan_until(@body_regex)
495
+ read_buffer.end_with?(EOL) ? :BOUNDARY : :END_BOUNDARY
496
+ else
497
+ @sbuf.terminate
498
+ nil
388
499
  end
389
500
  end
390
501
 
391
- def get_filename(head)
392
- filename = nil
393
- case head
394
- when RFC2183
395
- params = Hash[*head.scan(DISPPARM).flat_map(&:compact)]
396
-
397
- if filename = params['filename']
398
- filename = $1 if filename =~ /^"(.*)"$/
399
- elsif filename = params['filename*']
400
- encoding, _, filename = filename.split("'", 3)
401
- end
402
- when BROKEN
403
- filename = $1
404
- filename = $1 if filename =~ /^"(.*)"$/
405
- end
406
-
407
- return unless filename
408
-
502
+ def normalize_filename(filename)
409
503
  if filename.scan(/%.?.?/).all? { |s| /%[0-9a-fA-F]{2}/.match?(s) }
410
504
  filename = Utils.unescape_path(filename)
411
505
  end
412
506
 
413
507
  filename.scrub!
414
508
 
415
- if filename !~ /\\[^\\"]/
416
- filename = filename.gsub(/\\(.)/, '\1')
417
- end
418
-
419
- if encoding
420
- filename.force_encoding ::Encoding.find(encoding)
421
- end
422
-
423
- filename
509
+ filename.split(/[\/\\]/).last || String.new
424
510
  end
425
511
 
426
512
  CHARSET = "charset"
513
+ deprecate_constant :CHARSET
427
514
 
428
515
  def tag_multipart_encoding(filename, content_type, name, body)
429
516
  name = name.to_s
@@ -444,7 +531,9 @@ module Rack
444
531
  k.strip!
445
532
  v.strip!
446
533
  v = v[1..-2] if v.start_with?('"') && v.end_with?('"')
447
- encoding = Encoding.find v if k == CHARSET
534
+ if k == "charset"
535
+ encoding = find_encoding(v)
536
+ end
448
537
  end
449
538
  end
450
539
  end
@@ -453,9 +542,37 @@ module Rack
453
542
  body.force_encoding(encoding)
454
543
  end
455
544
 
545
+ # Return the related Encoding object. However, because
546
+ # enc is submitted by the user, it may be invalid, so
547
+ # use a binary encoding in that case.
548
+ def find_encoding(enc)
549
+ Encoding.find enc
550
+ rescue ArgumentError
551
+ Encoding::BINARY
552
+ end
553
+
554
+ REENCODE_DUMMY_ENCODINGS = {
555
+ # ISO-2022-JP is a legacy but still widely used encoding in Japan
556
+ # Here we convert ISO-2022-JP to UTF-8 so that it can be handled.
557
+ Encoding::ISO_2022_JP => true
558
+
559
+ # Other dummy encodings are rarely used and have not been supported yet.
560
+ # Adding support for them will require careful considerations.
561
+ }
562
+
563
+ def handle_dummy_encoding(name, body)
564
+ # A string object with a 'dummy' encoding does not have full functionality and can cause errors.
565
+ # So here we covert it to UTF-8 so that it can be handled properly.
566
+ if name.encoding.dummy? && REENCODE_DUMMY_ENCODINGS[name.encoding]
567
+ name = name.encode(Encoding::UTF_8)
568
+ body = body.encode(Encoding::UTF_8)
569
+ end
570
+ return name, body
571
+ end
572
+
456
573
  def handle_empty_content!(content)
457
574
  if content.nil? || content.empty?
458
- raise EOFError
575
+ raise EmptyContentError
459
576
  end
460
577
  end
461
578
  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