rack 3.1.8 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +319 -3
- data/README.md +76 -20
- data/SPEC.rdoc +199 -306
- data/lib/rack/auth/abstract/request.rb +2 -0
- data/lib/rack/builder.rb +6 -0
- data/lib/rack/common_logger.rb +3 -2
- data/lib/rack/conditional_get.rb +4 -3
- data/lib/rack/constants.rb +1 -0
- data/lib/rack/directory.rb +5 -2
- data/lib/rack/etag.rb +3 -0
- data/lib/rack/events.rb +21 -6
- data/lib/rack/head.rb +2 -3
- data/lib/rack/lint.rb +430 -457
- data/lib/rack/media_type.rb +14 -10
- data/lib/rack/mock_response.rb +37 -5
- data/lib/rack/multipart/parser.rb +91 -13
- data/lib/rack/multipart/uploaded_file.rb +42 -5
- data/lib/rack/query_parser.rb +81 -20
- data/lib/rack/request.rb +49 -55
- data/lib/rack/rewindable_input.rb +4 -1
- data/lib/rack/sendfile.rb +51 -21
- data/lib/rack/show_exceptions.rb +4 -2
- data/lib/rack/show_status.rb +0 -2
- data/lib/rack/static.rb +2 -1
- data/lib/rack/utils.rb +14 -23
- data/lib/rack/version.rb +4 -8
- data/lib/rack.rb +0 -1
- metadata +5 -8
- data/lib/rack/logger.rb +0 -23
data/lib/rack/media_type.rb
CHANGED
|
@@ -14,12 +14,11 @@ module Rack
|
|
|
14
14
|
# For more information on the use of media types in HTTP, see:
|
|
15
15
|
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7
|
|
16
16
|
def type(content_type)
|
|
17
|
-
return nil unless content_type
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
end
|
|
17
|
+
return nil unless content_type && !content_type.empty?
|
|
18
|
+
type = content_type.split(SPLIT_PATTERN, 2).first
|
|
19
|
+
type.rstrip!
|
|
20
|
+
type.downcase!
|
|
21
|
+
type
|
|
23
22
|
end
|
|
24
23
|
|
|
25
24
|
# The media type parameters provided in CONTENT_TYPE as a Hash, or
|
|
@@ -27,8 +26,13 @@ module Rack
|
|
|
27
26
|
# provided. e.g., when the CONTENT_TYPE is "text/plain;charset=utf-8",
|
|
28
27
|
# this method responds with the following Hash:
|
|
29
28
|
# { 'charset' => 'utf-8' }
|
|
29
|
+
#
|
|
30
|
+
# This will pass back parameters with empty strings in the hash if they
|
|
31
|
+
# lack a value (e.g., "text/plain;charset=" will return { 'charset' => '' },
|
|
32
|
+
# and "text/plain;charset" will return { 'charset' => '' }, similarly to
|
|
33
|
+
# the query params parser (barring the latter case, which returns nil instead)).
|
|
30
34
|
def params(content_type)
|
|
31
|
-
return {} if content_type.nil?
|
|
35
|
+
return {} if content_type.nil? || content_type.empty?
|
|
32
36
|
|
|
33
37
|
content_type.split(SPLIT_PATTERN)[1..-1].each_with_object({}) do |s, hsh|
|
|
34
38
|
s.strip!
|
|
@@ -40,9 +44,9 @@ module Rack
|
|
|
40
44
|
|
|
41
45
|
private
|
|
42
46
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
47
|
+
def strip_doublequotes(str)
|
|
48
|
+
(str && str.start_with?('"') && str.end_with?('"')) ? str[1..-2] : str || ''
|
|
49
|
+
end
|
|
46
50
|
end
|
|
47
51
|
end
|
|
48
52
|
end
|
data/lib/rack/mock_response.rb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require '
|
|
3
|
+
require 'stringio'
|
|
4
4
|
require 'time'
|
|
5
5
|
|
|
6
6
|
require_relative 'response'
|
|
@@ -11,6 +11,30 @@ module Rack
|
|
|
11
11
|
# MockRequest.
|
|
12
12
|
|
|
13
13
|
class MockResponse < Rack::Response
|
|
14
|
+
class Cookie
|
|
15
|
+
attr_reader :name, :value, :path, :domain, :expires, :secure
|
|
16
|
+
|
|
17
|
+
def initialize(args)
|
|
18
|
+
@name = args["name"]
|
|
19
|
+
@value = args["value"]
|
|
20
|
+
@path = args["path"]
|
|
21
|
+
@domain = args["domain"]
|
|
22
|
+
@expires = args["expires"]
|
|
23
|
+
@secure = args["secure"]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def method_missing(method_name, *args, &block)
|
|
27
|
+
@value.send(method_name, *args, &block)
|
|
28
|
+
end
|
|
29
|
+
# :nocov:
|
|
30
|
+
ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
|
|
31
|
+
# :nocov:
|
|
32
|
+
|
|
33
|
+
def respond_to_missing?(method_name, include_all = false)
|
|
34
|
+
@value.respond_to?(method_name, include_all) || super
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
14
38
|
class << self
|
|
15
39
|
alias [] new
|
|
16
40
|
end
|
|
@@ -59,8 +83,16 @@ module Rack
|
|
|
59
83
|
# end
|
|
60
84
|
buffer = @buffered_body = String.new
|
|
61
85
|
|
|
62
|
-
|
|
63
|
-
|
|
86
|
+
begin
|
|
87
|
+
if @body.respond_to?(:each)
|
|
88
|
+
@body.each do |chunk|
|
|
89
|
+
buffer << chunk
|
|
90
|
+
end
|
|
91
|
+
else
|
|
92
|
+
@body.call(StringIO.new(buffer))
|
|
93
|
+
end
|
|
94
|
+
ensure
|
|
95
|
+
@body.close if @body.respond_to?(:close)
|
|
64
96
|
end
|
|
65
97
|
|
|
66
98
|
return buffer
|
|
@@ -83,7 +115,7 @@ module Rack
|
|
|
83
115
|
Array(set_cookie_header).each do |cookie|
|
|
84
116
|
cookie_name, cookie_filling = cookie.split('=', 2)
|
|
85
117
|
cookie_attributes = identify_cookie_attributes cookie_filling
|
|
86
|
-
parsed_cookie =
|
|
118
|
+
parsed_cookie = Cookie.new(
|
|
87
119
|
'name' => cookie_name.strip,
|
|
88
120
|
'value' => cookie_attributes.fetch('value'),
|
|
89
121
|
'path' => cookie_attributes.fetch('path', nil),
|
|
@@ -100,7 +132,7 @@ module Rack
|
|
|
100
132
|
def identify_cookie_attributes(cookie_filling)
|
|
101
133
|
cookie_bits = cookie_filling.split(';')
|
|
102
134
|
cookie_attributes = Hash.new
|
|
103
|
-
cookie_attributes.store('value', cookie_bits[0].strip)
|
|
135
|
+
cookie_attributes.store('value', Array(cookie_bits[0].strip))
|
|
104
136
|
cookie_bits.drop(1).each do |bit|
|
|
105
137
|
if bit.include? '='
|
|
106
138
|
cookie_attribute, attribute_value = bit.split('=', 2)
|
|
@@ -31,11 +31,25 @@ module Rack
|
|
|
31
31
|
Error = BoundaryTooLongError
|
|
32
32
|
|
|
33
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
|
|
34
36
|
MULTIPART = %r|\Amultipart/.*boundary=\"?([^\";,]+)\"?|ni
|
|
35
|
-
MULTIPART_CONTENT_TYPE =
|
|
36
|
-
MULTIPART_CONTENT_DISPOSITION =
|
|
37
|
-
MULTIPART_CONTENT_ID =
|
|
38
|
-
|
|
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
|
|
39
53
|
class Parser
|
|
40
54
|
BUFSIZE = 1_048_576
|
|
41
55
|
TEXT_PLAIN = "text/plain"
|
|
@@ -45,6 +59,27 @@ module Rack
|
|
|
45
59
|
Tempfile.new(["RackMultipart", extension])
|
|
46
60
|
}
|
|
47
61
|
|
|
62
|
+
BOUNDARY_START_LIMIT = 16 * 1024
|
|
63
|
+
private_constant :BOUNDARY_START_LIMIT
|
|
64
|
+
|
|
65
|
+
MIME_HEADER_BYTESIZE_LIMIT = 64 * 1024
|
|
66
|
+
private_constant :MIME_HEADER_BYTESIZE_LIMIT
|
|
67
|
+
|
|
68
|
+
env_int = lambda do |key, val|
|
|
69
|
+
if str_val = ENV[key]
|
|
70
|
+
begin
|
|
71
|
+
val = Integer(str_val, 10)
|
|
72
|
+
rescue ArgumentError
|
|
73
|
+
raise ArgumentError, "non-integer value provided for environment variable #{key}"
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
val
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
BUFFERED_UPLOAD_BYTESIZE_LIMIT = env_int.call("RACK_MULTIPART_BUFFERED_UPLOAD_BYTESIZE_LIMIT", 16 * 1024 * 1024)
|
|
81
|
+
private_constant :BUFFERED_UPLOAD_BYTESIZE_LIMIT
|
|
82
|
+
|
|
48
83
|
class BoundedIO # :nodoc:
|
|
49
84
|
def initialize(io, content_length)
|
|
50
85
|
@io = io
|
|
@@ -204,6 +239,8 @@ module Rack
|
|
|
204
239
|
|
|
205
240
|
@state = :FAST_FORWARD
|
|
206
241
|
@mime_index = 0
|
|
242
|
+
@body_retained = nil
|
|
243
|
+
@retained_size = 0
|
|
207
244
|
@collector = Collector.new tempfile
|
|
208
245
|
|
|
209
246
|
@sbuf = StringScanner.new("".dup)
|
|
@@ -241,7 +278,8 @@ module Rack
|
|
|
241
278
|
@collector.each do |part|
|
|
242
279
|
part.get_data do |data|
|
|
243
280
|
tag_multipart_encoding(part.filename, part.content_type, part.name, data)
|
|
244
|
-
|
|
281
|
+
name, data = handle_dummy_encoding(part.name, data)
|
|
282
|
+
@query_parser.normalize_params(@params, name, data)
|
|
245
283
|
end
|
|
246
284
|
end
|
|
247
285
|
MultipartInfo.new @params.to_params_hash, @collector.find_all(&:file?).map(&:body)
|
|
@@ -249,12 +287,6 @@ module Rack
|
|
|
249
287
|
|
|
250
288
|
private
|
|
251
289
|
|
|
252
|
-
def dequote(str) # From WEBrick::HTTPUtils
|
|
253
|
-
ret = (/\A"(.*)"\Z/ =~ str) ? $1 : str.dup
|
|
254
|
-
ret.gsub!(/\\(.)/, "\\1")
|
|
255
|
-
ret
|
|
256
|
-
end
|
|
257
|
-
|
|
258
290
|
def read_data(io, outbuf)
|
|
259
291
|
content = io.read(@bufsize, outbuf)
|
|
260
292
|
handle_empty_content!(content)
|
|
@@ -285,6 +317,10 @@ module Rack
|
|
|
285
317
|
|
|
286
318
|
# retry for opening boundary
|
|
287
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
|
|
323
|
+
|
|
288
324
|
# no boundary found, keep reading data
|
|
289
325
|
return :want_read
|
|
290
326
|
end
|
|
@@ -401,16 +437,30 @@ module Rack
|
|
|
401
437
|
name = filename || "#{content_type || TEXT_PLAIN}[]".dup
|
|
402
438
|
end
|
|
403
439
|
|
|
440
|
+
# Mime part head data is retained for both TempfilePart and BufferPart
|
|
441
|
+
# for the entireity of the parse, even though it isn't used for BufferPart.
|
|
442
|
+
update_retained_size(head.bytesize)
|
|
443
|
+
|
|
444
|
+
# If a filename is given, a TempfilePart will be used, so the body will
|
|
445
|
+
# not be buffered in memory. However, if a filename is not given, a BufferPart
|
|
446
|
+
# will be used, and the body will be buffered in memory.
|
|
447
|
+
@body_retained = !filename
|
|
448
|
+
|
|
404
449
|
@collector.on_mime_head @mime_index, head, filename, content_type, name
|
|
405
450
|
@state = :MIME_BODY
|
|
406
451
|
else
|
|
407
|
-
|
|
452
|
+
# We raise if the mime part header is too large, to avoid unbounded memory
|
|
453
|
+
# buffering. Note that the actual limit is the higher of 64KB and the buffer size (1MB by default)
|
|
454
|
+
raise Error, "multipart mime part header too large" if @sbuf.rest.bytesize > MIME_HEADER_BYTESIZE_LIMIT
|
|
455
|
+
|
|
456
|
+
return :want_read
|
|
408
457
|
end
|
|
409
458
|
end
|
|
410
459
|
|
|
411
460
|
def handle_mime_body
|
|
412
461
|
if (body_with_boundary = @sbuf.check_until(@body_regex)) # check but do not advance the pointer yet
|
|
413
462
|
body = body_with_boundary.sub(@body_regex_at_end, '') # remove the boundary from the string
|
|
463
|
+
update_retained_size(body.bytesize) if @body_retained
|
|
414
464
|
@collector.on_mime_body @mime_index, body
|
|
415
465
|
@sbuf.pos += body.length + 2 # skip \r\n after the content
|
|
416
466
|
@state = :CONSUME_TOKEN
|
|
@@ -419,7 +469,9 @@ module Rack
|
|
|
419
469
|
# Save what we have so far
|
|
420
470
|
if @rx_max_size < @sbuf.rest_size
|
|
421
471
|
delta = @sbuf.rest_size - @rx_max_size
|
|
422
|
-
|
|
472
|
+
body = @sbuf.peek(delta)
|
|
473
|
+
update_retained_size(body.bytesize) if @body_retained
|
|
474
|
+
@collector.on_mime_body @mime_index, body
|
|
423
475
|
@sbuf.pos += delta
|
|
424
476
|
@sbuf.string = @sbuf.rest
|
|
425
477
|
end
|
|
@@ -427,6 +479,13 @@ module Rack
|
|
|
427
479
|
end
|
|
428
480
|
end
|
|
429
481
|
|
|
482
|
+
def update_retained_size(size)
|
|
483
|
+
@retained_size += size
|
|
484
|
+
if @retained_size > BUFFERED_UPLOAD_BYTESIZE_LIMIT
|
|
485
|
+
raise Error, "multipart data over retained size limit"
|
|
486
|
+
end
|
|
487
|
+
end
|
|
488
|
+
|
|
430
489
|
# Scan until the we find the start or end of the boundary.
|
|
431
490
|
# If we find it, return the appropriate symbol for the start or
|
|
432
491
|
# end of the boundary. If we don't find the start or end of the
|
|
@@ -492,6 +551,25 @@ module Rack
|
|
|
492
551
|
Encoding::BINARY
|
|
493
552
|
end
|
|
494
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
|
+
|
|
495
573
|
def handle_empty_content!(content)
|
|
496
574
|
if content.nil? || content.empty?
|
|
497
575
|
raise EmptyContentError
|
|
@@ -5,14 +5,47 @@ require 'fileutils'
|
|
|
5
5
|
|
|
6
6
|
module Rack
|
|
7
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.
|
|
8
19
|
class UploadedFile
|
|
9
|
-
|
|
10
|
-
#
|
|
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.
|
|
11
23
|
attr_reader :original_filename
|
|
12
24
|
|
|
13
|
-
# The content type of the
|
|
25
|
+
# The content type of the instance.
|
|
14
26
|
attr_accessor :content_type
|
|
15
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.
|
|
16
49
|
def initialize(filepath = nil, ct = "text/plain", bin = false,
|
|
17
50
|
path: filepath, content_type: ct, binary: bin, filename: nil, io: nil)
|
|
18
51
|
if io
|
|
@@ -28,15 +61,19 @@ module Rack
|
|
|
28
61
|
@content_type = content_type
|
|
29
62
|
end
|
|
30
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.
|
|
31
66
|
def path
|
|
32
67
|
@tempfile.path if @tempfile.respond_to?(:path)
|
|
33
68
|
end
|
|
34
69
|
alias_method :local_path, :path
|
|
35
70
|
|
|
36
|
-
|
|
37
|
-
|
|
71
|
+
# Return true if the tempfile responds to the method.
|
|
72
|
+
def respond_to_missing?(*args)
|
|
73
|
+
@tempfile.respond_to?(*args)
|
|
38
74
|
end
|
|
39
75
|
|
|
76
|
+
# Delegate method missing calls to the tempfile.
|
|
40
77
|
def method_missing(method_name, *args, &block) #:nodoc:
|
|
41
78
|
@tempfile.__send__(method_name, *args, &block)
|
|
42
79
|
end
|
data/lib/rack/query_parser.rb
CHANGED
|
@@ -21,21 +21,49 @@ module Rack
|
|
|
21
21
|
include BadRequest
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
class
|
|
24
|
+
# QueryLimitError is for errors raised when the query provided exceeds one
|
|
25
|
+
# of the query parser limits.
|
|
26
|
+
class QueryLimitError < RangeError
|
|
27
27
|
include BadRequest
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
# ParamsTooDeepError is the old name for the error that is raised when params
|
|
31
|
+
# are recursively nested over the specified limit. Make it the same as
|
|
32
|
+
# as QueryLimitError, so that code that rescues ParamsTooDeepError error
|
|
33
|
+
# to handle bad query strings also now handles other limits.
|
|
34
|
+
ParamsTooDeepError = QueryLimitError
|
|
35
|
+
|
|
36
|
+
def self.make_default(param_depth_limit, **options)
|
|
37
|
+
new(Params, param_depth_limit, **options)
|
|
32
38
|
end
|
|
33
39
|
|
|
34
40
|
attr_reader :param_depth_limit
|
|
35
41
|
|
|
36
|
-
|
|
42
|
+
env_int = lambda do |key, val|
|
|
43
|
+
if str_val = ENV[key]
|
|
44
|
+
begin
|
|
45
|
+
val = Integer(str_val, 10)
|
|
46
|
+
rescue ArgumentError
|
|
47
|
+
raise ArgumentError, "non-integer value provided for environment variable #{key}"
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
val
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
BYTESIZE_LIMIT = env_int.call("RACK_QUERY_PARSER_BYTESIZE_LIMIT", 4194304)
|
|
55
|
+
private_constant :BYTESIZE_LIMIT
|
|
56
|
+
|
|
57
|
+
PARAMS_LIMIT = env_int.call("RACK_QUERY_PARSER_PARAMS_LIMIT", 4096)
|
|
58
|
+
private_constant :PARAMS_LIMIT
|
|
59
|
+
|
|
60
|
+
attr_reader :bytesize_limit
|
|
61
|
+
|
|
62
|
+
def initialize(params_class, param_depth_limit, bytesize_limit: BYTESIZE_LIMIT, params_limit: PARAMS_LIMIT)
|
|
37
63
|
@params_class = params_class
|
|
38
64
|
@param_depth_limit = param_depth_limit
|
|
65
|
+
@bytesize_limit = bytesize_limit
|
|
66
|
+
@params_limit = params_limit
|
|
39
67
|
end
|
|
40
68
|
|
|
41
69
|
# Stolen from Mongrel, with some small modifications:
|
|
@@ -43,14 +71,9 @@ module Rack
|
|
|
43
71
|
# to parse cookies by changing the characters used in the second parameter
|
|
44
72
|
# (which defaults to '&').
|
|
45
73
|
def parse_query(qs, separator = nil, &unescaper)
|
|
46
|
-
unescaper ||= method(:unescape)
|
|
47
|
-
|
|
48
74
|
params = make_params
|
|
49
75
|
|
|
50
|
-
(qs
|
|
51
|
-
next if p.empty?
|
|
52
|
-
k, v = p.split('=', 2).map!(&unescaper)
|
|
53
|
-
|
|
76
|
+
each_query_pair(qs, separator, unescaper) do |k, v|
|
|
54
77
|
if cur = params[k]
|
|
55
78
|
if cur.class == Array
|
|
56
79
|
params[k] << v
|
|
@@ -65,6 +88,19 @@ module Rack
|
|
|
65
88
|
return params.to_h
|
|
66
89
|
end
|
|
67
90
|
|
|
91
|
+
# Parses a query string by breaking it up at the '&', returning all key-value
|
|
92
|
+
# pairs as an array of [key, value] arrays. Unlike parse_query, this preserves
|
|
93
|
+
# all duplicate keys rather than collapsing them.
|
|
94
|
+
def parse_query_pairs(qs, separator = nil)
|
|
95
|
+
pairs = []
|
|
96
|
+
|
|
97
|
+
each_query_pair(qs, separator) do |k, v|
|
|
98
|
+
pairs << [k, v]
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
pairs
|
|
102
|
+
end
|
|
103
|
+
|
|
68
104
|
# parse_nested_query expands a query string into structural types. Supported
|
|
69
105
|
# types are Arrays, Hashes and basic value types. It is possible to supply
|
|
70
106
|
# query strings with parameters of conflicting types, in this case a
|
|
@@ -73,17 +109,11 @@ module Rack
|
|
|
73
109
|
def parse_nested_query(qs, separator = nil)
|
|
74
110
|
params = make_params
|
|
75
111
|
|
|
76
|
-
|
|
77
|
-
(
|
|
78
|
-
k, v = p.split('=', 2).map! { |s| unescape(s) }
|
|
79
|
-
|
|
80
|
-
_normalize_params(params, k, v, 0)
|
|
81
|
-
end
|
|
112
|
+
each_query_pair(qs, separator) do |k, v|
|
|
113
|
+
_normalize_params(params, k, v, 0)
|
|
82
114
|
end
|
|
83
115
|
|
|
84
116
|
return params.to_h
|
|
85
|
-
rescue ArgumentError => e
|
|
86
|
-
raise InvalidParameterError, e.message, e.backtrace
|
|
87
117
|
end
|
|
88
118
|
|
|
89
119
|
# normalize_params recursively expands parameters into structural types. If
|
|
@@ -189,6 +219,37 @@ module Rack
|
|
|
189
219
|
true
|
|
190
220
|
end
|
|
191
221
|
|
|
222
|
+
def each_query_pair(qs, separator, unescaper = nil)
|
|
223
|
+
return if !qs || qs.empty?
|
|
224
|
+
|
|
225
|
+
if qs.bytesize > @bytesize_limit
|
|
226
|
+
raise QueryLimitError, "total query size exceeds limit (#{@bytesize_limit})"
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
pairs = qs.split(separator ? (COMMON_SEP[separator] || /[#{separator}] */n) : DEFAULT_SEP, @params_limit + 1)
|
|
230
|
+
|
|
231
|
+
if pairs.size > @params_limit
|
|
232
|
+
param_count = pairs.size + pairs.last.count(separator || "&")
|
|
233
|
+
raise QueryLimitError, "total number of query parameters (#{param_count}) exceeds limit (#{@params_limit})"
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
if unescaper
|
|
237
|
+
pairs.each do |p|
|
|
238
|
+
next if p.empty?
|
|
239
|
+
k, v = p.split('=', 2).map!(&unescaper)
|
|
240
|
+
yield k, v
|
|
241
|
+
end
|
|
242
|
+
else
|
|
243
|
+
pairs.each do |p|
|
|
244
|
+
next if p.empty?
|
|
245
|
+
k, v = p.split('=', 2).map! { |s| unescape(s) }
|
|
246
|
+
yield k, v
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
rescue ArgumentError => e
|
|
250
|
+
raise InvalidParameterError, e.message, e.backtrace
|
|
251
|
+
end
|
|
252
|
+
|
|
192
253
|
def unescape(string, encoding = Encoding::UTF_8)
|
|
193
254
|
URI.decode_www_form_component(string, encoding)
|
|
194
255
|
end
|