rack 3.0.5 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +90 -3
- data/CONTRIBUTING.md +11 -9
- data/README.md +34 -15
- data/SPEC.rdoc +38 -13
- data/lib/rack/auth/basic.rb +1 -2
- data/lib/rack/bad_request.rb +8 -0
- data/lib/rack/body_proxy.rb +18 -2
- data/lib/rack/builder.rb +23 -10
- data/lib/rack/cascade.rb +0 -3
- data/lib/rack/constants.rb +2 -2
- data/lib/rack/content_length.rb +0 -1
- data/lib/rack/headers.rb +86 -2
- data/lib/rack/lint.rb +116 -34
- data/lib/rack/logger.rb +2 -1
- data/lib/rack/media_type.rb +9 -4
- data/lib/rack/mime.rb +6 -5
- data/lib/rack/mock_request.rb +19 -14
- data/lib/rack/mock_response.rb +12 -14
- data/lib/rack/multipart/parser.rb +132 -63
- data/lib/rack/multipart.rb +9 -1
- data/lib/rack/query_parser.rb +36 -106
- data/lib/rack/request.rb +37 -73
- data/lib/rack/response.rb +22 -22
- data/lib/rack/sendfile.rb +1 -1
- data/lib/rack/show_exceptions.rb +6 -2
- data/lib/rack/utils.rb +66 -101
- data/lib/rack/version.rb +1 -14
- data/lib/rack.rb +11 -16
- metadata +4 -10
- data/lib/rack/auth/digest/md5.rb +0 -1
- data/lib/rack/auth/digest/nonce.rb +0 -1
- data/lib/rack/auth/digest/params.rb +0 -1
- data/lib/rack/auth/digest/request.rb +0 -1
- data/lib/rack/auth/digest.rb +0 -256
- data/lib/rack/chunked.rb +0 -120
- data/lib/rack/file.rb +0 -9
|
@@ -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
|
|
10
|
+
class MultipartPartLimitError < Errno::EMFILE
|
|
11
|
+
include BadRequest
|
|
12
|
+
end
|
|
10
13
|
|
|
11
|
-
class MultipartTotalPartLimitError < StandardError
|
|
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
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
|
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,8 @@ 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
|
|
212
|
+
@end_boundary_size = boundary.bytesize + 4 # (-- at start, -- at finish)
|
|
216
213
|
@rx_max_size = boundary.bytesize + 6 # (\r\n-- at start, either \r\n or -- at finish)
|
|
217
214
|
@head_regex = /(.*?#{EOL})#{EOL}/m
|
|
218
215
|
end
|
|
@@ -279,7 +276,14 @@ module Rack
|
|
|
279
276
|
@state = :MIME_HEAD
|
|
280
277
|
return
|
|
281
278
|
when :END_BOUNDARY
|
|
282
|
-
# invalid multipart upload
|
|
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
|
|
283
287
|
else
|
|
284
288
|
# no boundary found, keep reading data
|
|
285
289
|
return :want_read
|
|
@@ -297,17 +301,102 @@ module Rack
|
|
|
297
301
|
end
|
|
298
302
|
end
|
|
299
303
|
|
|
304
|
+
CONTENT_DISPOSITION_MAX_PARAMS = 16
|
|
305
|
+
CONTENT_DISPOSITION_MAX_BYTES = 1536
|
|
300
306
|
def handle_mime_head
|
|
301
307
|
if @sbuf.scan_until(@head_regex)
|
|
302
308
|
head = @sbuf[1]
|
|
303
309
|
content_type = head[MULTIPART_CONTENT_TYPE, 1]
|
|
304
|
-
if
|
|
305
|
-
|
|
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
|
|
306
388
|
else
|
|
307
389
|
name = head[MULTIPART_CONTENT_ID, 1]
|
|
308
390
|
end
|
|
309
391
|
|
|
310
|
-
|
|
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
|
|
311
400
|
|
|
312
401
|
if name.nil? || name.empty?
|
|
313
402
|
name = filename || "#{content_type || TEXT_PLAIN}[]".dup
|
|
@@ -322,7 +411,7 @@ module Rack
|
|
|
322
411
|
|
|
323
412
|
def handle_mime_body
|
|
324
413
|
if (body_with_boundary = @sbuf.check_until(@body_regex)) # check but do not advance the pointer yet
|
|
325
|
-
body = body_with_boundary.sub(
|
|
414
|
+
body = body_with_boundary.sub(@body_regex_at_end, '') # remove the boundary from the string
|
|
326
415
|
@collector.on_mime_body @mime_index, body
|
|
327
416
|
@sbuf.pos += body.length + 2 # skip \r\n after the content
|
|
328
417
|
@state = :CONSUME_TOKEN
|
|
@@ -352,39 +441,14 @@ module Rack
|
|
|
352
441
|
end
|
|
353
442
|
end
|
|
354
443
|
|
|
355
|
-
def
|
|
356
|
-
filename = nil
|
|
357
|
-
case head
|
|
358
|
-
when RFC2183
|
|
359
|
-
params = Hash[*head.scan(DISPPARM).flat_map(&:compact)]
|
|
360
|
-
|
|
361
|
-
if filename = params['filename*']
|
|
362
|
-
encoding, _, filename = filename.split("'", 3)
|
|
363
|
-
elsif filename = params['filename']
|
|
364
|
-
filename = $1 if filename =~ /^"(.*)"$/
|
|
365
|
-
end
|
|
366
|
-
when BROKEN
|
|
367
|
-
filename = $1
|
|
368
|
-
filename = $1 if filename =~ /^"(.*)"$/
|
|
369
|
-
end
|
|
370
|
-
|
|
371
|
-
return unless filename
|
|
372
|
-
|
|
444
|
+
def normalize_filename(filename)
|
|
373
445
|
if filename.scan(/%.?.?/).all? { |s| /%[0-9a-fA-F]{2}/.match?(s) }
|
|
374
446
|
filename = Utils.unescape_path(filename)
|
|
375
447
|
end
|
|
376
448
|
|
|
377
449
|
filename.scrub!
|
|
378
450
|
|
|
379
|
-
|
|
380
|
-
filename = filename.gsub(/\\(.)/, '\1')
|
|
381
|
-
end
|
|
382
|
-
|
|
383
|
-
if encoding
|
|
384
|
-
filename.force_encoding ::Encoding.find(encoding)
|
|
385
|
-
end
|
|
386
|
-
|
|
387
|
-
filename
|
|
451
|
+
filename.split(/[\/\\]/).last || String.new
|
|
388
452
|
end
|
|
389
453
|
|
|
390
454
|
CHARSET = "charset"
|
|
@@ -410,11 +474,7 @@ module Rack
|
|
|
410
474
|
v.strip!
|
|
411
475
|
v = v[1..-2] if v.start_with?('"') && v.end_with?('"')
|
|
412
476
|
if k == "charset"
|
|
413
|
-
encoding =
|
|
414
|
-
Encoding.find v
|
|
415
|
-
rescue ArgumentError
|
|
416
|
-
Encoding::BINARY
|
|
417
|
-
end
|
|
477
|
+
encoding = find_encoding(v)
|
|
418
478
|
end
|
|
419
479
|
end
|
|
420
480
|
end
|
|
@@ -424,6 +484,15 @@ module Rack
|
|
|
424
484
|
body.force_encoding(encoding)
|
|
425
485
|
end
|
|
426
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
|
+
|
|
427
496
|
def handle_empty_content!(content)
|
|
428
497
|
if content.nil? || content.empty?
|
|
429
498
|
raise EmptyContentError
|
data/lib/rack/multipart.rb
CHANGED
|
@@ -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,6 +15,10 @@ module Rack
|
|
|
13
15
|
module Multipart
|
|
14
16
|
MULTIPART_BOUNDARY = "AaB03x"
|
|
15
17
|
|
|
18
|
+
class MissingInputError < StandardError
|
|
19
|
+
include BadRequest
|
|
20
|
+
end
|
|
21
|
+
|
|
16
22
|
# Accumulator for multipart form data, conforming to the QueryParser API.
|
|
17
23
|
# In future, the Parser could return the pair list directly, but that would
|
|
18
24
|
# change its API.
|
|
@@ -40,7 +46,9 @@ module Rack
|
|
|
40
46
|
|
|
41
47
|
class << self
|
|
42
48
|
def parse_multipart(env, params = Rack::Utils.default_query_parser)
|
|
43
|
-
io = env[RACK_INPUT]
|
|
49
|
+
unless io = env[RACK_INPUT]
|
|
50
|
+
raise MissingInputError, "Missing input stream!"
|
|
51
|
+
end
|
|
44
52
|
|
|
45
53
|
if content_length = env['CONTENT_LENGTH']
|
|
46
54
|
content_length = content_length.to_i
|
data/lib/rack/query_parser.rb
CHANGED
|
@@ -1,78 +1,56 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative 'bad_request'
|
|
4
|
+
require 'uri'
|
|
5
|
+
|
|
3
6
|
module Rack
|
|
4
7
|
class QueryParser
|
|
5
|
-
DEFAULT_SEP =
|
|
6
|
-
COMMON_SEP = { ";" =>
|
|
8
|
+
DEFAULT_SEP = /& */n
|
|
9
|
+
COMMON_SEP = { ";" => /; */n, ";," => /[;,] */n, "&" => /& */n }
|
|
7
10
|
|
|
8
11
|
# ParameterTypeError is the error that is raised when incoming structural
|
|
9
12
|
# parameters (parsed by parse_nested_query) contain conflicting types.
|
|
10
|
-
class ParameterTypeError < TypeError
|
|
13
|
+
class ParameterTypeError < TypeError
|
|
14
|
+
include BadRequest
|
|
15
|
+
end
|
|
11
16
|
|
|
12
17
|
# InvalidParameterError is the error that is raised when incoming structural
|
|
13
18
|
# parameters (parsed by parse_nested_query) contain invalid format or byte
|
|
14
19
|
# sequence.
|
|
15
|
-
class InvalidParameterError < ArgumentError
|
|
20
|
+
class InvalidParameterError < ArgumentError
|
|
21
|
+
include BadRequest
|
|
22
|
+
end
|
|
16
23
|
|
|
17
24
|
# ParamsTooDeepError is the error that is raised when params are recursively
|
|
18
25
|
# nested over the specified limit.
|
|
19
|
-
class ParamsTooDeepError < RangeError
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
unless not_deprecated
|
|
23
|
-
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)
|
|
24
|
-
end
|
|
26
|
+
class ParamsTooDeepError < RangeError
|
|
27
|
+
include BadRequest
|
|
28
|
+
end
|
|
25
29
|
|
|
30
|
+
def self.make_default(param_depth_limit)
|
|
26
31
|
new Params, param_depth_limit
|
|
27
32
|
end
|
|
28
33
|
|
|
29
34
|
attr_reader :param_depth_limit
|
|
30
35
|
|
|
31
|
-
def initialize(params_class,
|
|
32
|
-
unless not_deprecated
|
|
33
|
-
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)
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
+
def initialize(params_class, param_depth_limit)
|
|
36
37
|
@params_class = params_class
|
|
37
38
|
@param_depth_limit = param_depth_limit
|
|
38
39
|
end
|
|
39
40
|
|
|
40
|
-
#
|
|
41
|
+
# Stolen from Mongrel, with some small modifications:
|
|
41
42
|
# Parses a query string by breaking it up at the '&'. You can also use this
|
|
42
43
|
# to parse cookies by changing the characters used in the second parameter
|
|
43
44
|
# (which defaults to '&').
|
|
44
|
-
#
|
|
45
|
-
# Returns an array of 2-element arrays, where the first element is the
|
|
46
|
-
# key and the second element is the value.
|
|
47
|
-
def split_query(qs, separator = nil, &unescaper)
|
|
48
|
-
pairs = []
|
|
49
|
-
|
|
50
|
-
if qs && !qs.empty?
|
|
51
|
-
unescaper ||= method(:unescape)
|
|
52
|
-
|
|
53
|
-
qs.split(separator ? (COMMON_SEP[separator] || /[#{separator}] */n) : DEFAULT_SEP).each do |p|
|
|
54
|
-
next if p.empty?
|
|
55
|
-
pair = p.split('=', 2).map!(&unescaper)
|
|
56
|
-
pair << nil if pair.length == 1
|
|
57
|
-
pairs << pair
|
|
58
|
-
end
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
pairs
|
|
62
|
-
rescue ArgumentError => e
|
|
63
|
-
raise InvalidParameterError, e.message, e.backtrace
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
# Parses a query string by breaking it up at the '&'. You can also use this
|
|
67
|
-
# to parse cookies by changing the characters used in the second parameter
|
|
68
|
-
# (which defaults to '&').
|
|
69
|
-
#
|
|
70
|
-
# Returns a hash where each value is a string (when a key only appears once)
|
|
71
|
-
# or an array of strings (when a key appears more than once).
|
|
72
45
|
def parse_query(qs, separator = nil, &unescaper)
|
|
46
|
+
unescaper ||= method(:unescape)
|
|
47
|
+
|
|
73
48
|
params = make_params
|
|
74
49
|
|
|
75
|
-
|
|
50
|
+
(qs || '').split(separator ? (COMMON_SEP[separator] || /[#{separator}] */n) : DEFAULT_SEP).each do |p|
|
|
51
|
+
next if p.empty?
|
|
52
|
+
k, v = p.split('=', 2).map!(&unescaper)
|
|
53
|
+
|
|
76
54
|
if cur = params[k]
|
|
77
55
|
if cur.class == Array
|
|
78
56
|
params[k] << v
|
|
@@ -84,7 +62,7 @@ module Rack
|
|
|
84
62
|
end
|
|
85
63
|
end
|
|
86
64
|
|
|
87
|
-
params.to_h
|
|
65
|
+
return params.to_h
|
|
88
66
|
end
|
|
89
67
|
|
|
90
68
|
# parse_nested_query expands a query string into structural types. Supported
|
|
@@ -95,11 +73,17 @@ module Rack
|
|
|
95
73
|
def parse_nested_query(qs, separator = nil)
|
|
96
74
|
params = make_params
|
|
97
75
|
|
|
98
|
-
|
|
99
|
-
|
|
76
|
+
unless qs.nil? || qs.empty?
|
|
77
|
+
(qs || '').split(separator ? (COMMON_SEP[separator] || /[#{separator}] */n) : DEFAULT_SEP).each do |p|
|
|
78
|
+
k, v = p.split('=', 2).map! { |s| unescape(s) }
|
|
79
|
+
|
|
80
|
+
_normalize_params(params, k, v, 0)
|
|
81
|
+
end
|
|
100
82
|
end
|
|
101
83
|
|
|
102
|
-
params.to_h
|
|
84
|
+
return params.to_h
|
|
85
|
+
rescue ArgumentError => e
|
|
86
|
+
raise InvalidParameterError, e.message, e.backtrace
|
|
103
87
|
end
|
|
104
88
|
|
|
105
89
|
# normalize_params recursively expands parameters into structural types. If
|
|
@@ -145,8 +129,6 @@ module Rack
|
|
|
145
129
|
|
|
146
130
|
return if k.empty?
|
|
147
131
|
|
|
148
|
-
v ||= String.new
|
|
149
|
-
|
|
150
132
|
if after == ''
|
|
151
133
|
if k == '[]' && depth != 0
|
|
152
134
|
return [v]
|
|
@@ -207,63 +189,11 @@ module Rack
|
|
|
207
189
|
true
|
|
208
190
|
end
|
|
209
191
|
|
|
210
|
-
def unescape(
|
|
211
|
-
|
|
192
|
+
def unescape(string, encoding = Encoding::UTF_8)
|
|
193
|
+
URI.decode_www_form_component(string, encoding)
|
|
212
194
|
end
|
|
213
195
|
|
|
214
|
-
class Params
|
|
215
|
-
def initialize
|
|
216
|
-
@size = 0
|
|
217
|
-
@params = {}
|
|
218
|
-
end
|
|
219
|
-
|
|
220
|
-
def [](key)
|
|
221
|
-
@params[key]
|
|
222
|
-
end
|
|
223
|
-
|
|
224
|
-
def []=(key, value)
|
|
225
|
-
@params[key] = value
|
|
226
|
-
end
|
|
227
|
-
|
|
228
|
-
def key?(key)
|
|
229
|
-
@params.key?(key)
|
|
230
|
-
end
|
|
231
|
-
|
|
232
|
-
# Recursively unwraps nested `Params` objects and constructs an object
|
|
233
|
-
# of the same shape, but using the objects' internal representations
|
|
234
|
-
# (Ruby hashes) in place of the objects. The result is a hash consisting
|
|
235
|
-
# purely of Ruby primitives.
|
|
236
|
-
#
|
|
237
|
-
# Mutation warning!
|
|
238
|
-
#
|
|
239
|
-
# 1. This method mutates the internal representation of the `Params`
|
|
240
|
-
# objects in order to save object allocations.
|
|
241
|
-
#
|
|
242
|
-
# 2. The value you get back is a reference to the internal hash
|
|
243
|
-
# representation, not a copy.
|
|
244
|
-
#
|
|
245
|
-
# 3. Because the `Params` object's internal representation is mutable
|
|
246
|
-
# through the `#[]=` method, it is not thread safe. The result of
|
|
247
|
-
# getting the hash representation while another thread is adding a
|
|
248
|
-
# key to it is non-deterministic.
|
|
249
|
-
#
|
|
250
|
-
def to_h
|
|
251
|
-
@params.each do |key, value|
|
|
252
|
-
case value
|
|
253
|
-
when self
|
|
254
|
-
# Handle circular references gracefully.
|
|
255
|
-
@params[key] = @params
|
|
256
|
-
when Params
|
|
257
|
-
@params[key] = value.to_h
|
|
258
|
-
when Array
|
|
259
|
-
value.map! { |v| v.kind_of?(Params) ? v.to_h : v }
|
|
260
|
-
else
|
|
261
|
-
# Ignore anything that is not a `Params` object or
|
|
262
|
-
# a collection that can contain one.
|
|
263
|
-
end
|
|
264
|
-
end
|
|
265
|
-
@params
|
|
266
|
-
end
|
|
196
|
+
class Params < Hash
|
|
267
197
|
alias_method :to_params_hash, :to_h
|
|
268
198
|
end
|
|
269
199
|
end
|
data/lib/rack/request.rb
CHANGED
|
@@ -482,23 +482,17 @@ module Rack
|
|
|
482
482
|
|
|
483
483
|
# Returns the data received in the query string.
|
|
484
484
|
def GET
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
end
|
|
490
|
-
|
|
491
|
-
set_header(RACK_REQUEST_QUERY_HASH, expand_params(query_param_list))
|
|
492
|
-
end
|
|
493
|
-
|
|
494
|
-
def query_param_list
|
|
495
|
-
if get_header(RACK_REQUEST_QUERY_STRING) == query_string
|
|
496
|
-
get_header(RACK_REQUEST_QUERY_PAIRS)
|
|
485
|
+
rr_query_string = get_header(RACK_REQUEST_QUERY_STRING)
|
|
486
|
+
query_string = self.query_string
|
|
487
|
+
if rr_query_string == query_string
|
|
488
|
+
get_header(RACK_REQUEST_QUERY_HASH)
|
|
497
489
|
else
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
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
|
|
493
|
+
query_hash = parse_query(query_string, '&')
|
|
494
|
+
set_header(RACK_REQUEST_QUERY_STRING, query_string)
|
|
495
|
+
set_header(RACK_REQUEST_QUERY_HASH, query_hash)
|
|
502
496
|
end
|
|
503
497
|
end
|
|
504
498
|
|
|
@@ -507,16 +501,6 @@ module Rack
|
|
|
507
501
|
# This method support both application/x-www-form-urlencoded and
|
|
508
502
|
# multipart/form-data.
|
|
509
503
|
def POST
|
|
510
|
-
if get_header(RACK_REQUEST_FORM_INPUT).equal?(get_header(RACK_INPUT))
|
|
511
|
-
if form_hash = get_header(RACK_REQUEST_FORM_HASH)
|
|
512
|
-
return form_hash
|
|
513
|
-
end
|
|
514
|
-
end
|
|
515
|
-
|
|
516
|
-
set_header(RACK_REQUEST_FORM_HASH, expand_params(body_param_list))
|
|
517
|
-
end
|
|
518
|
-
|
|
519
|
-
def body_param_list
|
|
520
504
|
if error = get_header(RACK_REQUEST_FORM_ERROR)
|
|
521
505
|
raise error.class, error.message, cause: error.cause
|
|
522
506
|
end
|
|
@@ -524,36 +508,42 @@ module Rack
|
|
|
524
508
|
begin
|
|
525
509
|
rack_input = get_header(RACK_INPUT)
|
|
526
510
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
511
|
+
# If the form hash was already memoized:
|
|
512
|
+
if form_hash = get_header(RACK_REQUEST_FORM_HASH)
|
|
513
|
+
form_input = get_header(RACK_REQUEST_FORM_INPUT)
|
|
514
|
+
# And it was memoized from the same input:
|
|
515
|
+
if form_input.equal?(rack_input)
|
|
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
|
|
534
519
|
end
|
|
535
520
|
end
|
|
536
521
|
|
|
522
|
+
# Otherwise, figure out how to parse the input:
|
|
537
523
|
if rack_input.nil?
|
|
538
|
-
|
|
524
|
+
set_header RACK_REQUEST_FORM_INPUT, nil
|
|
525
|
+
set_header(RACK_REQUEST_FORM_HASH, {})
|
|
539
526
|
elsif form_data? || parseable_data?
|
|
540
|
-
|
|
541
|
-
|
|
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
|
|
531
|
+
form_vars = get_header(RACK_INPUT).read
|
|
542
532
|
|
|
543
533
|
# Fix for Safari Ajax postings that always append \0
|
|
544
534
|
# form_vars.sub!(/\0\z/, '') # performance replacement:
|
|
545
535
|
form_vars.slice!(-1) if form_vars.end_with?("\0")
|
|
546
536
|
|
|
547
537
|
set_header RACK_REQUEST_FORM_VARS, form_vars
|
|
548
|
-
|
|
538
|
+
set_header RACK_REQUEST_FORM_HASH, parse_query(form_vars, '&')
|
|
549
539
|
end
|
|
540
|
+
|
|
541
|
+
set_header RACK_REQUEST_FORM_INPUT, get_header(RACK_INPUT)
|
|
542
|
+
get_header RACK_REQUEST_FORM_HASH
|
|
550
543
|
else
|
|
551
|
-
|
|
544
|
+
set_header RACK_REQUEST_FORM_INPUT, get_header(RACK_INPUT)
|
|
545
|
+
set_header(RACK_REQUEST_FORM_HASH, {})
|
|
552
546
|
end
|
|
553
|
-
|
|
554
|
-
set_header RACK_REQUEST_FORM_INPUT, rack_input
|
|
555
|
-
set_header RACK_REQUEST_FORM_HASH, nil
|
|
556
|
-
set_header(RACK_REQUEST_FORM_PAIRS, form_pairs)
|
|
557
547
|
rescue => error
|
|
558
548
|
set_header(RACK_REQUEST_FORM_ERROR, error)
|
|
559
549
|
raise
|
|
@@ -626,24 +616,10 @@ module Rack
|
|
|
626
616
|
Rack::Request.ip_filter.call(ip)
|
|
627
617
|
end
|
|
628
618
|
|
|
629
|
-
# shortcut for <tt>request.params[key]</tt>
|
|
630
|
-
def [](key)
|
|
631
|
-
warn("Request#[] is deprecated and will be removed in a future version of Rack. Please use request.params[] instead", uplevel: 1)
|
|
632
|
-
|
|
633
|
-
params[key.to_s]
|
|
634
|
-
end
|
|
635
|
-
|
|
636
|
-
# shortcut for <tt>request.params[key] = value</tt>
|
|
637
|
-
#
|
|
638
|
-
# Note that modifications will not be persisted in the env. Use update_param or delete_param if you want to destructively modify params.
|
|
639
|
-
def []=(key, value)
|
|
640
|
-
warn("Request#[]= is deprecated and will be removed in a future version of Rack. Please use request.params[]= instead", uplevel: 1)
|
|
641
|
-
|
|
642
|
-
params[key.to_s] = value
|
|
643
|
-
end
|
|
644
|
-
|
|
645
619
|
# like Hash#values_at
|
|
646
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
|
+
|
|
647
623
|
keys.map { |key| params[key] }
|
|
648
624
|
end
|
|
649
625
|
|
|
@@ -693,23 +669,11 @@ module Rack
|
|
|
693
669
|
Rack::Multipart.extract_multipart(self, query_parser)
|
|
694
670
|
end
|
|
695
671
|
|
|
696
|
-
def
|
|
697
|
-
query_parser = query_parser()
|
|
698
|
-
unless query_parser.respond_to?(:split_query)
|
|
699
|
-
query_parser = Utils.default_query_parser
|
|
700
|
-
unless query_parser.respond_to?(:split_query)
|
|
701
|
-
query_parser = QueryParser.make_default(0)
|
|
702
|
-
end
|
|
703
|
-
end
|
|
704
|
-
|
|
705
|
-
query_parser.split_query(query, d)
|
|
706
|
-
end
|
|
707
|
-
|
|
708
|
-
def expand_params(pairs, query_parser = query_parser())
|
|
672
|
+
def expand_param_pairs(pairs, query_parser = query_parser())
|
|
709
673
|
params = query_parser.make_params
|
|
710
674
|
|
|
711
|
-
pairs.each do |
|
|
712
|
-
query_parser.normalize_params(params,
|
|
675
|
+
pairs.each do |k, v|
|
|
676
|
+
query_parser.normalize_params(params, k, v)
|
|
713
677
|
end
|
|
714
678
|
|
|
715
679
|
params.to_params_hash
|