rack 3.0.0 → 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 +125 -2
- data/CONTRIBUTING.md +11 -9
- data/README.md +55 -20
- data/SPEC.rdoc +38 -13
- data/lib/rack/auth/basic.rb +1 -4
- data/lib/rack/bad_request.rb +8 -0
- data/lib/rack/body_proxy.rb +18 -2
- data/lib/rack/builder.rb +55 -40
- data/lib/rack/cascade.rb +0 -3
- data/lib/rack/constants.rb +5 -2
- data/lib/rack/content_length.rb +0 -1
- data/lib/rack/headers.rb +98 -14
- data/lib/rack/lint.rb +126 -38
- data/lib/rack/logger.rb +2 -1
- data/lib/rack/media_type.rb +9 -4
- data/lib/rack/method_override.rb +2 -2
- 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 +147 -66
- data/lib/rack/multipart.rb +34 -1
- data/lib/rack/query_parser.rb +19 -72
- data/lib/rack/request.rb +65 -38
- data/lib/rack/response.rb +27 -22
- data/lib/rack/sendfile.rb +1 -1
- data/lib/rack/show_exceptions.rb +6 -2
- data/lib/rack/urlmap.rb +1 -1
- data/lib/rack/utils.rb +91 -114
- 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,49 +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
|
|
13
|
+
|
|
14
|
+
class MultipartTotalPartLimitError < StandardError
|
|
15
|
+
include BadRequest
|
|
16
|
+
end
|
|
10
17
|
|
|
11
18
|
# Use specific error class when parsing multipart request
|
|
12
19
|
# that ends early.
|
|
13
|
-
class EmptyContentError < ::EOFError
|
|
20
|
+
class EmptyContentError < ::EOFError
|
|
21
|
+
include BadRequest
|
|
22
|
+
end
|
|
14
23
|
|
|
15
24
|
# Base class for multipart exceptions that do not subclass from
|
|
16
25
|
# other exception classes for backwards compatibility.
|
|
17
|
-
class
|
|
26
|
+
class BoundaryTooLongError < StandardError
|
|
27
|
+
include BadRequest
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Prefer to use the BoundaryTooLongError class or Rack::BadRequest.
|
|
31
|
+
Error = BoundaryTooLongError
|
|
18
32
|
|
|
19
33
|
EOL = "\r\n"
|
|
20
34
|
MULTIPART = %r|\Amultipart/.*boundary=\"?([^\";,]+)\"?|ni
|
|
21
|
-
TOKEN = /[^\s()<>,;:\\"\/\[\]?=]+/
|
|
22
|
-
CONDISP = /Content-Disposition:\s*#{TOKEN}\s*/i
|
|
23
|
-
VALUE = /"(?:\\"|[^"])*"|#{TOKEN}/
|
|
24
|
-
BROKEN = /^#{CONDISP}.*;\s*filename=(#{VALUE})/i
|
|
25
35
|
MULTIPART_CONTENT_TYPE = /Content-Type: (.*)#{EOL}/ni
|
|
26
|
-
MULTIPART_CONTENT_DISPOSITION = /Content-Disposition
|
|
36
|
+
MULTIPART_CONTENT_DISPOSITION = /Content-Disposition:(.*)(?=#{EOL}(\S|\z))/ni
|
|
27
37
|
MULTIPART_CONTENT_ID = /Content-ID:\s*([^#{EOL}]*)/ni
|
|
28
|
-
# Updated definitions from RFC 2231
|
|
29
|
-
ATTRIBUTE_CHAR = %r{[^ \t\v\n\r)(><@,;:\\"/\[\]?='*%]}
|
|
30
|
-
ATTRIBUTE = /#{ATTRIBUTE_CHAR}+/
|
|
31
|
-
SECTION = /\*[0-9]+/
|
|
32
|
-
REGULAR_PARAMETER_NAME = /#{ATTRIBUTE}#{SECTION}?/
|
|
33
|
-
REGULAR_PARAMETER = /(#{REGULAR_PARAMETER_NAME})=(#{VALUE})/
|
|
34
|
-
EXTENDED_OTHER_NAME = /#{ATTRIBUTE}\*[1-9][0-9]*\*/
|
|
35
|
-
EXTENDED_OTHER_VALUE = /%[0-9a-fA-F]{2}|#{ATTRIBUTE_CHAR}/
|
|
36
|
-
EXTENDED_OTHER_PARAMETER = /(#{EXTENDED_OTHER_NAME})=(#{EXTENDED_OTHER_VALUE}*)/
|
|
37
|
-
EXTENDED_INITIAL_NAME = /#{ATTRIBUTE}(?:\*0)?\*/
|
|
38
|
-
EXTENDED_INITIAL_VALUE = /[a-zA-Z0-9\-]*'[a-zA-Z0-9\-]*'#{EXTENDED_OTHER_VALUE}*/
|
|
39
|
-
EXTENDED_INITIAL_PARAMETER = /(#{EXTENDED_INITIAL_NAME})=(#{EXTENDED_INITIAL_VALUE})/
|
|
40
|
-
EXTENDED_PARAMETER = /#{EXTENDED_INITIAL_PARAMETER}|#{EXTENDED_OTHER_PARAMETER}/
|
|
41
|
-
DISPPARM = /;\s*(?:#{REGULAR_PARAMETER}|#{EXTENDED_PARAMETER})\s*/
|
|
42
|
-
RFC2183 = /^#{CONDISP}(#{DISPPARM})+$/i
|
|
43
38
|
|
|
44
39
|
class Parser
|
|
45
40
|
BUFSIZE = 1_048_576
|
|
46
41
|
TEXT_PLAIN = "text/plain"
|
|
47
42
|
TEMPFILE_FACTORY = lambda { |filename, content_type|
|
|
48
|
-
|
|
43
|
+
extension = ::File.extname(filename.gsub("\0", '%00'))[0, 129]
|
|
44
|
+
|
|
45
|
+
Tempfile.new(["RackMultipart", extension])
|
|
49
46
|
}
|
|
50
47
|
|
|
51
48
|
class BoundedIO # :nodoc:
|
|
@@ -96,7 +93,7 @@ module Rack
|
|
|
96
93
|
if boundary.length > 70
|
|
97
94
|
# RFC 1521 Section 7.2.1 imposes a 70 character maximum for the boundary.
|
|
98
95
|
# Most clients use no more than 55 characters.
|
|
99
|
-
raise
|
|
96
|
+
raise BoundaryTooLongError, "multipart boundary size too large (#{boundary.length} characters)"
|
|
100
97
|
end
|
|
101
98
|
|
|
102
99
|
io = BoundedIO.new(io, content_length) if content_length
|
|
@@ -166,7 +163,7 @@ module Rack
|
|
|
166
163
|
|
|
167
164
|
@mime_parts[mime_index] = klass.new(body, head, filename, content_type, name)
|
|
168
165
|
|
|
169
|
-
|
|
166
|
+
check_part_limits
|
|
170
167
|
end
|
|
171
168
|
|
|
172
169
|
def on_mime_body(mime_index, content)
|
|
@@ -178,13 +175,23 @@ module Rack
|
|
|
178
175
|
|
|
179
176
|
private
|
|
180
177
|
|
|
181
|
-
def
|
|
182
|
-
|
|
183
|
-
|
|
178
|
+
def check_part_limits
|
|
179
|
+
file_limit = Utils.multipart_file_limit
|
|
180
|
+
part_limit = Utils.multipart_total_part_limit
|
|
181
|
+
|
|
182
|
+
if file_limit && file_limit > 0
|
|
183
|
+
if @open_files >= file_limit
|
|
184
184
|
@mime_parts.each(&:close)
|
|
185
185
|
raise MultipartPartLimitError, 'Maximum file multiparts in content reached'
|
|
186
186
|
end
|
|
187
187
|
end
|
|
188
|
+
|
|
189
|
+
if part_limit && part_limit > 0
|
|
190
|
+
if @mime_parts.size >= part_limit
|
|
191
|
+
@mime_parts.each(&:close)
|
|
192
|
+
raise MultipartTotalPartLimitError, 'Maximum total multiparts in content reached'
|
|
193
|
+
end
|
|
194
|
+
end
|
|
188
195
|
end
|
|
189
196
|
end
|
|
190
197
|
|
|
@@ -201,6 +208,8 @@ module Rack
|
|
|
201
208
|
|
|
202
209
|
@sbuf = StringScanner.new("".dup)
|
|
203
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)
|
|
204
213
|
@rx_max_size = boundary.bytesize + 6 # (\r\n-- at start, either \r\n or -- at finish)
|
|
205
214
|
@head_regex = /(.*?#{EOL})#{EOL}/m
|
|
206
215
|
end
|
|
@@ -267,7 +276,14 @@ module Rack
|
|
|
267
276
|
@state = :MIME_HEAD
|
|
268
277
|
return
|
|
269
278
|
when :END_BOUNDARY
|
|
270
|
-
# 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
|
|
271
287
|
else
|
|
272
288
|
# no boundary found, keep reading data
|
|
273
289
|
return :want_read
|
|
@@ -285,17 +301,102 @@ module Rack
|
|
|
285
301
|
end
|
|
286
302
|
end
|
|
287
303
|
|
|
304
|
+
CONTENT_DISPOSITION_MAX_PARAMS = 16
|
|
305
|
+
CONTENT_DISPOSITION_MAX_BYTES = 1536
|
|
288
306
|
def handle_mime_head
|
|
289
307
|
if @sbuf.scan_until(@head_regex)
|
|
290
308
|
head = @sbuf[1]
|
|
291
309
|
content_type = head[MULTIPART_CONTENT_TYPE, 1]
|
|
292
|
-
if
|
|
293
|
-
|
|
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
|
|
294
388
|
else
|
|
295
389
|
name = head[MULTIPART_CONTENT_ID, 1]
|
|
296
390
|
end
|
|
297
391
|
|
|
298
|
-
|
|
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
|
|
299
400
|
|
|
300
401
|
if name.nil? || name.empty?
|
|
301
402
|
name = filename || "#{content_type || TEXT_PLAIN}[]".dup
|
|
@@ -310,7 +411,7 @@ module Rack
|
|
|
310
411
|
|
|
311
412
|
def handle_mime_body
|
|
312
413
|
if (body_with_boundary = @sbuf.check_until(@body_regex)) # check but do not advance the pointer yet
|
|
313
|
-
body = body_with_boundary.sub(
|
|
414
|
+
body = body_with_boundary.sub(@body_regex_at_end, '') # remove the boundary from the string
|
|
314
415
|
@collector.on_mime_body @mime_index, body
|
|
315
416
|
@sbuf.pos += body.length + 2 # skip \r\n after the content
|
|
316
417
|
@state = :CONSUME_TOKEN
|
|
@@ -340,39 +441,14 @@ module Rack
|
|
|
340
441
|
end
|
|
341
442
|
end
|
|
342
443
|
|
|
343
|
-
def
|
|
344
|
-
filename = nil
|
|
345
|
-
case head
|
|
346
|
-
when RFC2183
|
|
347
|
-
params = Hash[*head.scan(DISPPARM).flat_map(&:compact)]
|
|
348
|
-
|
|
349
|
-
if filename = params['filename*']
|
|
350
|
-
encoding, _, filename = filename.split("'", 3)
|
|
351
|
-
elsif filename = params['filename']
|
|
352
|
-
filename = $1 if filename =~ /^"(.*)"$/
|
|
353
|
-
end
|
|
354
|
-
when BROKEN
|
|
355
|
-
filename = $1
|
|
356
|
-
filename = $1 if filename =~ /^"(.*)"$/
|
|
357
|
-
end
|
|
358
|
-
|
|
359
|
-
return unless filename
|
|
360
|
-
|
|
444
|
+
def normalize_filename(filename)
|
|
361
445
|
if filename.scan(/%.?.?/).all? { |s| /%[0-9a-fA-F]{2}/.match?(s) }
|
|
362
446
|
filename = Utils.unescape_path(filename)
|
|
363
447
|
end
|
|
364
448
|
|
|
365
449
|
filename.scrub!
|
|
366
450
|
|
|
367
|
-
|
|
368
|
-
filename = filename.gsub(/\\(.)/, '\1')
|
|
369
|
-
end
|
|
370
|
-
|
|
371
|
-
if encoding
|
|
372
|
-
filename.force_encoding ::Encoding.find(encoding)
|
|
373
|
-
end
|
|
374
|
-
|
|
375
|
-
filename
|
|
451
|
+
filename.split(/[\/\\]/).last || String.new
|
|
376
452
|
end
|
|
377
453
|
|
|
378
454
|
CHARSET = "charset"
|
|
@@ -398,11 +474,7 @@ module Rack
|
|
|
398
474
|
v.strip!
|
|
399
475
|
v = v[1..-2] if v.start_with?('"') && v.end_with?('"')
|
|
400
476
|
if k == "charset"
|
|
401
|
-
encoding =
|
|
402
|
-
Encoding.find v
|
|
403
|
-
rescue ArgumentError
|
|
404
|
-
Encoding::BINARY
|
|
405
|
-
end
|
|
477
|
+
encoding = find_encoding(v)
|
|
406
478
|
end
|
|
407
479
|
end
|
|
408
480
|
end
|
|
@@ -412,6 +484,15 @@ module Rack
|
|
|
412
484
|
body.force_encoding(encoding)
|
|
413
485
|
end
|
|
414
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
|
+
|
|
415
496
|
def handle_empty_content!(content)
|
|
416
497
|
if content.nil? || content.empty?
|
|
417
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,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
|
data/lib/rack/query_parser.rb
CHANGED
|
@@ -1,38 +1,39 @@
|
|
|
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
|
|
@@ -128,8 +129,6 @@ module Rack
|
|
|
128
129
|
|
|
129
130
|
return if k.empty?
|
|
130
131
|
|
|
131
|
-
v ||= String.new
|
|
132
|
-
|
|
133
132
|
if after == ''
|
|
134
133
|
if k == '[]' && depth != 0
|
|
135
134
|
return [v]
|
|
@@ -190,63 +189,11 @@ module Rack
|
|
|
190
189
|
true
|
|
191
190
|
end
|
|
192
191
|
|
|
193
|
-
def unescape(
|
|
194
|
-
|
|
192
|
+
def unescape(string, encoding = Encoding::UTF_8)
|
|
193
|
+
URI.decode_www_form_component(string, encoding)
|
|
195
194
|
end
|
|
196
195
|
|
|
197
|
-
class Params
|
|
198
|
-
def initialize
|
|
199
|
-
@size = 0
|
|
200
|
-
@params = {}
|
|
201
|
-
end
|
|
202
|
-
|
|
203
|
-
def [](key)
|
|
204
|
-
@params[key]
|
|
205
|
-
end
|
|
206
|
-
|
|
207
|
-
def []=(key, value)
|
|
208
|
-
@params[key] = value
|
|
209
|
-
end
|
|
210
|
-
|
|
211
|
-
def key?(key)
|
|
212
|
-
@params.key?(key)
|
|
213
|
-
end
|
|
214
|
-
|
|
215
|
-
# Recursively unwraps nested `Params` objects and constructs an object
|
|
216
|
-
# of the same shape, but using the objects' internal representations
|
|
217
|
-
# (Ruby hashes) in place of the objects. The result is a hash consisting
|
|
218
|
-
# purely of Ruby primitives.
|
|
219
|
-
#
|
|
220
|
-
# Mutation warning!
|
|
221
|
-
#
|
|
222
|
-
# 1. This method mutates the internal representation of the `Params`
|
|
223
|
-
# objects in order to save object allocations.
|
|
224
|
-
#
|
|
225
|
-
# 2. The value you get back is a reference to the internal hash
|
|
226
|
-
# representation, not a copy.
|
|
227
|
-
#
|
|
228
|
-
# 3. Because the `Params` object's internal representation is mutable
|
|
229
|
-
# through the `#[]=` method, it is not thread safe. The result of
|
|
230
|
-
# getting the hash representation while another thread is adding a
|
|
231
|
-
# key to it is non-deterministic.
|
|
232
|
-
#
|
|
233
|
-
def to_h
|
|
234
|
-
@params.each do |key, value|
|
|
235
|
-
case value
|
|
236
|
-
when self
|
|
237
|
-
# Handle circular references gracefully.
|
|
238
|
-
@params[key] = @params
|
|
239
|
-
when Params
|
|
240
|
-
@params[key] = value.to_h
|
|
241
|
-
when Array
|
|
242
|
-
value.map! { |v| v.kind_of?(Params) ? v.to_h : v }
|
|
243
|
-
else
|
|
244
|
-
# Ignore anything that is not a `Params` object or
|
|
245
|
-
# a collection that can contain one.
|
|
246
|
-
end
|
|
247
|
-
end
|
|
248
|
-
@params
|
|
249
|
-
end
|
|
196
|
+
class Params < Hash
|
|
250
197
|
alias_method :to_params_hash, :to_h
|
|
251
198
|
end
|
|
252
199
|
end
|
data/lib/rack/request.rb
CHANGED
|
@@ -44,7 +44,7 @@ module Rack
|
|
|
44
44
|
@x_forwarded_proto_priority = [:proto, :scheme]
|
|
45
45
|
|
|
46
46
|
valid_ipv4_octet = /\.(25[0-5]|2[0-4][0-9]|[01]?[0-9]?[0-9])/
|
|
47
|
-
|
|
47
|
+
|
|
48
48
|
trusted_proxies = Regexp.union(
|
|
49
49
|
/\A127#{valid_ipv4_octet}{3}\z/, # localhost IPv4 range 127.x.x.x, per RFC-3330
|
|
50
50
|
/\A::1\z/, # localhost IPv6 ::1
|
|
@@ -54,7 +54,7 @@ module Rack
|
|
|
54
54
|
/\A192\.168#{valid_ipv4_octet}{2}\z/, # private IPv4 range 192.168.x.x
|
|
55
55
|
/\Alocalhost\z|\Aunix(\z|:)/i, # localhost hostname, and unix domain sockets
|
|
56
56
|
)
|
|
57
|
-
|
|
57
|
+
|
|
58
58
|
self.ip_filter = lambda { |ip| trusted_proxies.match?(ip) }
|
|
59
59
|
|
|
60
60
|
ALLOWED_SCHEMES = %w(https http wss ws).freeze
|
|
@@ -482,9 +482,14 @@ module Rack
|
|
|
482
482
|
|
|
483
483
|
# Returns the data received in the query string.
|
|
484
484
|
def GET
|
|
485
|
-
|
|
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)
|
|
@@ -496,26 +501,52 @@ module Rack
|
|
|
496
501
|
# This method support both application/x-www-form-urlencoded and
|
|
497
502
|
# multipart/form-data.
|
|
498
503
|
def POST
|
|
499
|
-
if get_header(
|
|
500
|
-
raise
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
504
|
+
if error = get_header(RACK_REQUEST_FORM_ERROR)
|
|
505
|
+
raise error.class, error.message, cause: error.cause
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
begin
|
|
509
|
+
rack_input = get_header(RACK_INPUT)
|
|
510
|
+
|
|
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
|
|
519
|
+
end
|
|
513
520
|
end
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
521
|
+
|
|
522
|
+
# Otherwise, figure out how to parse the input:
|
|
523
|
+
if rack_input.nil?
|
|
524
|
+
set_header RACK_REQUEST_FORM_INPUT, nil
|
|
525
|
+
set_header(RACK_REQUEST_FORM_HASH, {})
|
|
526
|
+
elsif form_data? || parseable_data?
|
|
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
|
|
532
|
+
|
|
533
|
+
# Fix for Safari Ajax postings that always append \0
|
|
534
|
+
# form_vars.sub!(/\0\z/, '') # performance replacement:
|
|
535
|
+
form_vars.slice!(-1) if form_vars.end_with?("\0")
|
|
536
|
+
|
|
537
|
+
set_header RACK_REQUEST_FORM_VARS, form_vars
|
|
538
|
+
set_header RACK_REQUEST_FORM_HASH, parse_query(form_vars, '&')
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
set_header RACK_REQUEST_FORM_INPUT, get_header(RACK_INPUT)
|
|
542
|
+
get_header RACK_REQUEST_FORM_HASH
|
|
543
|
+
else
|
|
544
|
+
set_header RACK_REQUEST_FORM_INPUT, get_header(RACK_INPUT)
|
|
545
|
+
set_header(RACK_REQUEST_FORM_HASH, {})
|
|
546
|
+
end
|
|
547
|
+
rescue => error
|
|
548
|
+
set_header(RACK_REQUEST_FORM_ERROR, error)
|
|
549
|
+
raise
|
|
519
550
|
end
|
|
520
551
|
end
|
|
521
552
|
|
|
@@ -585,24 +616,10 @@ module Rack
|
|
|
585
616
|
Rack::Request.ip_filter.call(ip)
|
|
586
617
|
end
|
|
587
618
|
|
|
588
|
-
# shortcut for <tt>request.params[key]</tt>
|
|
589
|
-
def [](key)
|
|
590
|
-
warn("Request#[] is deprecated and will be removed in a future version of Rack. Please use request.params[] instead", uplevel: 1)
|
|
591
|
-
|
|
592
|
-
params[key.to_s]
|
|
593
|
-
end
|
|
594
|
-
|
|
595
|
-
# shortcut for <tt>request.params[key] = value</tt>
|
|
596
|
-
#
|
|
597
|
-
# Note that modifications will not be persisted in the env. Use update_param or delete_param if you want to destructively modify params.
|
|
598
|
-
def []=(key, value)
|
|
599
|
-
warn("Request#[]= is deprecated and will be removed in a future version of Rack. Please use request.params[]= instead", uplevel: 1)
|
|
600
|
-
|
|
601
|
-
params[key.to_s] = value
|
|
602
|
-
end
|
|
603
|
-
|
|
604
619
|
# like Hash#values_at
|
|
605
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
|
+
|
|
606
623
|
keys.map { |key| params[key] }
|
|
607
624
|
end
|
|
608
625
|
|
|
@@ -652,6 +669,16 @@ module Rack
|
|
|
652
669
|
Rack::Multipart.extract_multipart(self, query_parser)
|
|
653
670
|
end
|
|
654
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
|
+
|
|
655
682
|
def split_header(value)
|
|
656
683
|
value ? value.strip.split(/[,\s]+/) : []
|
|
657
684
|
end
|