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

@@ -3,51 +3,46 @@
3
3
  require 'strscan'
4
4
 
5
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 MultipartTotalPartLimitError < StandardError; end
14
+ class MultipartTotalPartLimitError < StandardError
15
+ include BadRequest
16
+ end
12
17
 
13
18
  # Use specific error class when parsing multipart request
14
19
  # that ends early.
15
- class EmptyContentError < ::EOFError; end
20
+ class EmptyContentError < ::EOFError
21
+ include BadRequest
22
+ end
16
23
 
17
24
  # Base class for multipart exceptions that do not subclass from
18
25
  # other exception classes for backwards compatibility.
19
- class Error < StandardError; end
26
+ class BoundaryTooLongError < StandardError
27
+ include BadRequest
28
+ end
29
+
30
+ # Prefer to use the BoundaryTooLongError class or Rack::BadRequest.
31
+ Error = BoundaryTooLongError
20
32
 
21
33
  EOL = "\r\n"
22
34
  MULTIPART = %r|\Amultipart/.*boundary=\"?([^\";,]+)\"?|ni
23
- TOKEN = /[^\s()<>,;:\\"\/\[\]?=]+/
24
- CONDISP = /Content-Disposition:\s*#{TOKEN}\s*/i
25
- VALUE = /"(?:\\"|[^"])*"|#{TOKEN}/
26
- BROKEN = /^#{CONDISP}.*;\s*filename=(#{VALUE})/i
27
35
  MULTIPART_CONTENT_TYPE = /Content-Type: (.*)#{EOL}/ni
28
- MULTIPART_CONTENT_DISPOSITION = /Content-Disposition:[^:]*;\s*name=(#{VALUE})/ni
36
+ MULTIPART_CONTENT_DISPOSITION = /Content-Disposition:(.*)(?=#{EOL}(\S|\z))/ni
29
37
  MULTIPART_CONTENT_ID = /Content-ID:\s*([^#{EOL}]*)/ni
30
- # Updated definitions from RFC 2231
31
- ATTRIBUTE_CHAR = %r{[^ \x00-\x1f\x7f)(><@,;:\\"/\[\]?='*%]}
32
- ATTRIBUTE = /#{ATTRIBUTE_CHAR}+/
33
- SECTION = /\*[0-9]+/
34
- REGULAR_PARAMETER_NAME = /#{ATTRIBUTE}#{SECTION}?/
35
- REGULAR_PARAMETER = /(#{REGULAR_PARAMETER_NAME})=(#{VALUE})/
36
- EXTENDED_OTHER_NAME = /#{ATTRIBUTE}\*[1-9][0-9]*\*/
37
- EXTENDED_OTHER_VALUE = /%[0-9a-fA-F]{2}|#{ATTRIBUTE_CHAR}/
38
- EXTENDED_OTHER_PARAMETER = /(#{EXTENDED_OTHER_NAME})=(#{EXTENDED_OTHER_VALUE}*)/
39
- EXTENDED_INITIAL_NAME = /#{ATTRIBUTE}(?:\*0)?\*/
40
- EXTENDED_INITIAL_VALUE = /[a-zA-Z0-9\-]*'[a-zA-Z0-9\-]*'#{EXTENDED_OTHER_VALUE}*/
41
- EXTENDED_INITIAL_PARAMETER = /(#{EXTENDED_INITIAL_NAME})=(#{EXTENDED_INITIAL_VALUE})/
42
- EXTENDED_PARAMETER = /#{EXTENDED_INITIAL_PARAMETER}|#{EXTENDED_OTHER_PARAMETER}/
43
- DISPPARM = /;\s*(?:#{REGULAR_PARAMETER}|#{EXTENDED_PARAMETER})\s*/
44
- RFC2183 = /^#{CONDISP}(#{DISPPARM})+$/i
45
38
 
46
39
  class Parser
47
40
  BUFSIZE = 1_048_576
48
41
  TEXT_PLAIN = "text/plain"
49
42
  TEMPFILE_FACTORY = lambda { |filename, content_type|
50
- Tempfile.new(["RackMultipart", ::File.extname(filename.gsub("\0", '%00'))])
43
+ extension = ::File.extname(filename.gsub("\0", '%00'))[0, 129]
44
+
45
+ Tempfile.new(["RackMultipart", extension])
51
46
  }
52
47
 
53
48
  class BoundedIO # :nodoc:
@@ -98,7 +93,7 @@ module Rack
98
93
  if boundary.length > 70
99
94
  # RFC 1521 Section 7.2.1 imposes a 70 character maximum for the boundary.
100
95
  # Most clients use no more than 55 characters.
101
- raise Error, "multipart boundary size too large (#{boundary.length} characters)"
96
+ raise BoundaryTooLongError, "multipart boundary size too large (#{boundary.length} characters)"
102
97
  end
103
98
 
104
99
  io = BoundedIO.new(io, content_length) if content_length
@@ -213,6 +208,7 @@ module Rack
213
208
 
214
209
  @sbuf = StringScanner.new("".dup)
215
210
  @body_regex = /(?:#{EOL}|\A)--#{Regexp.quote(boundary)}(?:#{EOL}|--)/m
211
+ @body_regex_at_end = /#{@body_regex}\z/m
216
212
  @end_boundary_size = boundary.bytesize + 4 # (-- at start, -- at finish)
217
213
  @rx_max_size = boundary.bytesize + 6 # (\r\n-- at start, either \r\n or -- at finish)
218
214
  @head_regex = /(.*?#{EOL})#{EOL}/m
@@ -305,17 +301,102 @@ module Rack
305
301
  end
306
302
  end
307
303
 
304
+ CONTENT_DISPOSITION_MAX_PARAMS = 16
305
+ CONTENT_DISPOSITION_MAX_BYTES = 1536
308
306
  def handle_mime_head
309
307
  if @sbuf.scan_until(@head_regex)
310
308
  head = @sbuf[1]
311
309
  content_type = head[MULTIPART_CONTENT_TYPE, 1]
312
- if name = head[MULTIPART_CONTENT_DISPOSITION, 1]
313
- name = 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
314
388
  else
315
389
  name = head[MULTIPART_CONTENT_ID, 1]
316
390
  end
317
391
 
318
- 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
319
400
 
320
401
  if name.nil? || name.empty?
321
402
  name = filename || "#{content_type || TEXT_PLAIN}[]".dup
@@ -330,7 +411,7 @@ module Rack
330
411
 
331
412
  def handle_mime_body
332
413
  if (body_with_boundary = @sbuf.check_until(@body_regex)) # check but do not advance the pointer yet
333
- body = body_with_boundary.sub(/#{@body_regex}\z/m, '') # remove the boundary from the string
414
+ body = body_with_boundary.sub(@body_regex_at_end, '') # remove the boundary from the string
334
415
  @collector.on_mime_body @mime_index, body
335
416
  @sbuf.pos += body.length + 2 # skip \r\n after the content
336
417
  @state = :CONSUME_TOKEN
@@ -360,39 +441,14 @@ module Rack
360
441
  end
361
442
  end
362
443
 
363
- def get_filename(head)
364
- filename = nil
365
- case head
366
- when RFC2183
367
- params = Hash[*head.scan(DISPPARM).flat_map(&:compact)]
368
-
369
- if filename = params['filename*']
370
- encoding, _, filename = filename.split("'", 3)
371
- elsif filename = params['filename']
372
- filename = $1 if filename =~ /^"(.*)"$/
373
- end
374
- when BROKEN
375
- filename = $1
376
- filename = $1 if filename =~ /^"(.*)"$/
377
- end
378
-
379
- return unless filename
380
-
444
+ def normalize_filename(filename)
381
445
  if filename.scan(/%.?.?/).all? { |s| /%[0-9a-fA-F]{2}/.match?(s) }
382
446
  filename = Utils.unescape_path(filename)
383
447
  end
384
448
 
385
449
  filename.scrub!
386
450
 
387
- if filename !~ /\\[^\\"]/
388
- filename = filename.gsub(/\\(.)/, '\1')
389
- end
390
-
391
- if encoding
392
- filename.force_encoding ::Encoding.find(encoding)
393
- end
394
-
395
- filename
451
+ filename.split(/[\/\\]/).last || String.new
396
452
  end
397
453
 
398
454
  CHARSET = "charset"
@@ -418,11 +474,7 @@ module Rack
418
474
  v.strip!
419
475
  v = v[1..-2] if v.start_with?('"') && v.end_with?('"')
420
476
  if k == "charset"
421
- encoding = begin
422
- Encoding.find v
423
- rescue ArgumentError
424
- Encoding::BINARY
425
- end
477
+ encoding = find_encoding(v)
426
478
  end
427
479
  end
428
480
  end
@@ -432,6 +484,15 @@ module Rack
432
484
  body.force_encoding(encoding)
433
485
  end
434
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
+
435
496
  def handle_empty_content!(content)
436
497
  if content.nil? || content.empty?
437
498
  raise EmptyContentError
@@ -6,6 +6,8 @@ require_relative 'utils'
6
6
  require_relative 'multipart/parser'
7
7
  require_relative 'multipart/generator'
8
8
 
9
+ require_relative 'bad_request'
10
+
9
11
  module Rack
10
12
  # A multipart form data parser, adapted from IOWA.
11
13
  #
@@ -13,9 +15,40 @@ module Rack
13
15
  module Multipart
14
16
  MULTIPART_BOUNDARY = "AaB03x"
15
17
 
18
+ class MissingInputError < StandardError
19
+ include BadRequest
20
+ end
21
+
22
+ # Accumulator for multipart form data, conforming to the QueryParser API.
23
+ # In future, the Parser could return the pair list directly, but that would
24
+ # change its API.
25
+ class ParamList # :nodoc:
26
+ def self.make_params
27
+ new
28
+ end
29
+
30
+ def self.normalize_params(params, key, value)
31
+ params << [key, value]
32
+ end
33
+
34
+ def initialize
35
+ @pairs = []
36
+ end
37
+
38
+ def <<(pair)
39
+ @pairs << pair
40
+ end
41
+
42
+ def to_params_hash
43
+ @pairs
44
+ end
45
+ end
46
+
16
47
  class << self
17
48
  def parse_multipart(env, params = Rack::Utils.default_query_parser)
18
- io = env[RACK_INPUT]
49
+ unless io = env[RACK_INPUT]
50
+ raise MissingInputError, "Missing input stream!"
51
+ end
19
52
 
20
53
  if content_length = env['CONTENT_LENGTH']
21
54
  content_length = content_length.to_i
@@ -1,69 +1,41 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'bad_request'
3
4
  require 'uri'
4
5
 
5
6
  module Rack
6
7
  class QueryParser
7
- DEFAULT_SEP = /[&] */n
8
- COMMON_SEP = { ";" => /[;] */n, ";," => /[;,] */n, "&" => /[&] */n }
8
+ DEFAULT_SEP = /& */n
9
+ COMMON_SEP = { ";" => /; */n, ";," => /[;,] */n, "&" => /& */n }
9
10
 
10
11
  # ParameterTypeError is the error that is raised when incoming structural
11
12
  # parameters (parsed by parse_nested_query) contain conflicting types.
12
- class ParameterTypeError < TypeError; end
13
+ class ParameterTypeError < TypeError
14
+ include BadRequest
15
+ end
13
16
 
14
17
  # InvalidParameterError is the error that is raised when incoming structural
15
18
  # parameters (parsed by parse_nested_query) contain invalid format or byte
16
19
  # sequence.
17
- class InvalidParameterError < ArgumentError; end
18
-
19
- # QueryLimitError is for errors raised when the query provided exceeds one
20
- # of the query parser limits.
21
- class QueryLimitError < RangeError
20
+ class InvalidParameterError < ArgumentError
21
+ include BadRequest
22
22
  end
23
23
 
24
- # ParamsTooDeepError is the old name for the error that is raised when params
25
- # are recursively nested over the specified limit. Make it the same as
26
- # as QueryLimitError, so that code that rescues ParamsTooDeepError error
27
- # to handle bad query strings also now handles other limits.
28
- ParamsTooDeepError = QueryLimitError
29
-
30
- def self.make_default(_key_space_limit=(not_deprecated = true; nil), param_depth_limit, **options)
31
- unless not_deprecated
32
- warn("`first argument `key_space limit` is deprecated and no longer has an effect. Please call with only one argument, which will be required in a future version of Rack", uplevel: 1)
33
- end
34
-
35
- new Params, param_depth_limit, **options
24
+ # ParamsTooDeepError is the error that is raised when params are recursively
25
+ # nested over the specified limit.
26
+ class ParamsTooDeepError < RangeError
27
+ include BadRequest
36
28
  end
37
29
 
38
- attr_reader :param_depth_limit
39
-
40
- env_int = lambda do |key, val|
41
- if str_val = ENV[key]
42
- begin
43
- val = Integer(str_val, 10)
44
- rescue ArgumentError
45
- raise ArgumentError, "non-integer value provided for environment variable #{key}"
46
- end
47
- end
48
-
49
- val
30
+ def self.make_default(param_depth_limit)
31
+ new Params, param_depth_limit
50
32
  end
51
33
 
52
- BYTESIZE_LIMIT = env_int.call("RACK_QUERY_PARSER_BYTESIZE_LIMIT", 4194304)
53
- private_constant :BYTESIZE_LIMIT
54
-
55
- PARAMS_LIMIT = env_int.call("RACK_QUERY_PARSER_PARAMS_LIMIT", 4096)
56
- private_constant :PARAMS_LIMIT
57
-
58
- def initialize(params_class, _key_space_limit=(not_deprecated = true; nil), param_depth_limit, bytesize_limit: BYTESIZE_LIMIT, params_limit: PARAMS_LIMIT)
59
- unless not_deprecated
60
- warn("`second argument `key_space limit` is deprecated and no longer has an effect. Please call with only two arguments, which will be required in a future version of Rack", uplevel: 1)
61
- end
34
+ attr_reader :param_depth_limit
62
35
 
36
+ def initialize(params_class, param_depth_limit)
63
37
  @params_class = params_class
64
38
  @param_depth_limit = param_depth_limit
65
- @bytesize_limit = bytesize_limit
66
- @params_limit = params_limit
67
39
  end
68
40
 
69
41
  # Stolen from Mongrel, with some small modifications:
@@ -75,7 +47,7 @@ module Rack
75
47
 
76
48
  params = make_params
77
49
 
78
- check_query_string(qs, separator).split(separator ? (COMMON_SEP[separator] || /[#{separator}] */n) : DEFAULT_SEP).each do |p|
50
+ (qs || '').split(separator ? (COMMON_SEP[separator] || /[#{separator}] */n) : DEFAULT_SEP).each do |p|
79
51
  next if p.empty?
80
52
  k, v = p.split('=', 2).map!(&unescaper)
81
53
 
@@ -102,7 +74,7 @@ module Rack
102
74
  params = make_params
103
75
 
104
76
  unless qs.nil? || qs.empty?
105
- check_query_string(qs, separator).split(separator ? (COMMON_SEP[separator] || /[#{separator}] */n) : DEFAULT_SEP).each do |p|
77
+ (qs || '').split(separator ? (COMMON_SEP[separator] || /[#{separator}] */n) : DEFAULT_SEP).each do |p|
106
78
  k, v = p.split('=', 2).map! { |s| unescape(s) }
107
79
 
108
80
  _normalize_params(params, k, v, 0)
@@ -217,79 +189,11 @@ module Rack
217
189
  true
218
190
  end
219
191
 
220
- def check_query_string(qs, sep)
221
- if qs
222
- if qs.bytesize > @bytesize_limit
223
- raise QueryLimitError, "total query size (#{qs.bytesize}) exceeds limit (#{@bytesize_limit})"
224
- end
225
-
226
- if (param_count = qs.count(sep.is_a?(String) ? sep : '&')) >= @params_limit
227
- raise QueryLimitError, "total number of query parameters (#{param_count+1}) exceeds limit (#{@params_limit})"
228
- end
229
-
230
- qs
231
- else
232
- ''
233
- end
234
- end
235
-
236
192
  def unescape(string, encoding = Encoding::UTF_8)
237
193
  URI.decode_www_form_component(string, encoding)
238
194
  end
239
195
 
240
- class Params
241
- def initialize
242
- @size = 0
243
- @params = {}
244
- end
245
-
246
- def [](key)
247
- @params[key]
248
- end
249
-
250
- def []=(key, value)
251
- @params[key] = value
252
- end
253
-
254
- def key?(key)
255
- @params.key?(key)
256
- end
257
-
258
- # Recursively unwraps nested `Params` objects and constructs an object
259
- # of the same shape, but using the objects' internal representations
260
- # (Ruby hashes) in place of the objects. The result is a hash consisting
261
- # purely of Ruby primitives.
262
- #
263
- # Mutation warning!
264
- #
265
- # 1. This method mutates the internal representation of the `Params`
266
- # objects in order to save object allocations.
267
- #
268
- # 2. The value you get back is a reference to the internal hash
269
- # representation, not a copy.
270
- #
271
- # 3. Because the `Params` object's internal representation is mutable
272
- # through the `#[]=` method, it is not thread safe. The result of
273
- # getting the hash representation while another thread is adding a
274
- # key to it is non-deterministic.
275
- #
276
- def to_h
277
- @params.each do |key, value|
278
- case value
279
- when self
280
- # Handle circular references gracefully.
281
- @params[key] = @params
282
- when Params
283
- @params[key] = value.to_h
284
- when Array
285
- value.map! { |v| v.kind_of?(Params) ? v.to_h : v }
286
- else
287
- # Ignore anything that is not a `Params` object or
288
- # a collection that can contain one.
289
- end
290
- end
291
- @params
292
- end
196
+ class Params < Hash
293
197
  alias_method :to_params_hash, :to_h
294
198
  end
295
199
  end
data/lib/rack/request.rb CHANGED
@@ -482,9 +482,14 @@ module Rack
482
482
 
483
483
  # Returns the data received in the query string.
484
484
  def GET
485
- if get_header(RACK_REQUEST_QUERY_STRING) == query_string
485
+ rr_query_string = get_header(RACK_REQUEST_QUERY_STRING)
486
+ query_string = self.query_string
487
+ if rr_query_string == query_string
486
488
  get_header(RACK_REQUEST_QUERY_HASH)
487
489
  else
490
+ if rr_query_string
491
+ warn "query string used for GET parsing different from current query string. Starting in Rack 3.2, Rack will used the cached GET value instead of parsing the current query string.", uplevel: 1
492
+ end
488
493
  query_hash = parse_query(query_string, '&')
489
494
  set_header(RACK_REQUEST_QUERY_STRING, query_string)
490
495
  set_header(RACK_REQUEST_QUERY_HASH, query_hash)
@@ -505,9 +510,12 @@ module Rack
505
510
 
506
511
  # If the form hash was already memoized:
507
512
  if form_hash = get_header(RACK_REQUEST_FORM_HASH)
513
+ form_input = get_header(RACK_REQUEST_FORM_INPUT)
508
514
  # And it was memoized from the same input:
509
- if get_header(RACK_REQUEST_FORM_INPUT).equal?(rack_input)
515
+ if form_input.equal?(rack_input)
510
516
  return form_hash
517
+ elsif form_input
518
+ warn "input stream used for POST parsing different from current input stream. Starting in Rack 3.2, Rack will used the cached POST value instead of parsing the current input stream.", uplevel: 1
511
519
  end
512
520
  end
513
521
 
@@ -516,7 +524,10 @@ module Rack
516
524
  set_header RACK_REQUEST_FORM_INPUT, nil
517
525
  set_header(RACK_REQUEST_FORM_HASH, {})
518
526
  elsif form_data? || parseable_data?
519
- unless set_header(RACK_REQUEST_FORM_HASH, parse_multipart)
527
+ if pairs = Rack::Multipart.parse_multipart(env, Rack::Multipart::ParamList)
528
+ set_header RACK_REQUEST_FORM_PAIRS, pairs
529
+ set_header RACK_REQUEST_FORM_HASH, expand_param_pairs(pairs)
530
+ else
520
531
  form_vars = get_header(RACK_INPUT).read
521
532
 
522
533
  # Fix for Safari Ajax postings that always append \0
@@ -605,24 +616,10 @@ module Rack
605
616
  Rack::Request.ip_filter.call(ip)
606
617
  end
607
618
 
608
- # shortcut for <tt>request.params[key]</tt>
609
- def [](key)
610
- warn("Request#[] is deprecated and will be removed in a future version of Rack. Please use request.params[] instead", uplevel: 1)
611
-
612
- params[key.to_s]
613
- end
614
-
615
- # shortcut for <tt>request.params[key] = value</tt>
616
- #
617
- # Note that modifications will not be persisted in the env. Use update_param or delete_param if you want to destructively modify params.
618
- def []=(key, value)
619
- warn("Request#[]= is deprecated and will be removed in a future version of Rack. Please use request.params[]= instead", uplevel: 1)
620
-
621
- params[key.to_s] = value
622
- end
623
-
624
619
  # like Hash#values_at
625
620
  def values_at(*keys)
621
+ warn("Request#values_at is deprecated and will be removed in a future version of Rack. Please use request.params.values_at instead", uplevel: 1)
622
+
626
623
  keys.map { |key| params[key] }
627
624
  end
628
625
 
@@ -645,8 +642,8 @@ module Rack
645
642
  end
646
643
 
647
644
  def parse_http_accept_header(header)
648
- header.to_s.split(",").each(&:strip!).map do |part|
649
- attribute, parameters = part.split(";", 2).each(&:strip!)
645
+ header.to_s.split(/\s*,\s*/).map do |part|
646
+ attribute, parameters = part.split(/\s*;\s*/, 2)
650
647
  quality = 1.0
651
648
  if parameters and /\Aq=([\d.]+)/ =~ parameters
652
649
  quality = $1.to_f
@@ -672,6 +669,16 @@ module Rack
672
669
  Rack::Multipart.extract_multipart(self, query_parser)
673
670
  end
674
671
 
672
+ def expand_param_pairs(pairs, query_parser = query_parser())
673
+ params = query_parser.make_params
674
+
675
+ pairs.each do |k, v|
676
+ query_parser.normalize_params(params, k, v)
677
+ end
678
+
679
+ params.to_params_hash
680
+ end
681
+
675
682
  def split_header(value)
676
683
  value ? value.strip.split(/[,\s]+/) : []
677
684
  end