rack 3.0.18 → 3.1.18
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 +198 -8
- data/CONTRIBUTING.md +11 -9
- data/README.md +42 -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/builder.rb +23 -10
- data/lib/rack/cascade.rb +0 -3
- data/lib/rack/constants.rb +3 -0
- data/lib/rack/headers.rb +86 -2
- data/lib/rack/lint.rb +118 -34
- data/lib/rack/logger.rb +2 -1
- data/lib/rack/media_type.rb +8 -3
- data/lib/rack/mime.rb +6 -5
- data/lib/rack/mock_request.rb +10 -15
- data/lib/rack/mock_response.rb +14 -16
- data/lib/rack/multipart/parser.rb +178 -66
- data/lib/rack/multipart.rb +34 -1
- data/lib/rack/query_parser.rb +16 -68
- data/lib/rack/request.rb +44 -22
- data/lib/rack/response.rb +28 -20
- data/lib/rack/sendfile.rb +50 -20
- data/lib/rack/show_exceptions.rb +6 -2
- data/lib/rack/utils.rb +71 -98
- data/lib/rack/version.rb +1 -14
- data/lib/rack.rb +1 -3
- metadata +5 -11
- 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
data/lib/rack/mock_response.rb
CHANGED
|
@@ -107,22 +107,20 @@ module Rack
|
|
|
107
107
|
|
|
108
108
|
def parse_cookies_from_header
|
|
109
109
|
cookies = Hash.new
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
Array(set_cookie_header).each do |
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
cookies.store(cookie_name, parsed_cookie)
|
|
125
|
-
end
|
|
110
|
+
set_cookie_header = headers['set-cookie']
|
|
111
|
+
if set_cookie_header && !set_cookie_header.empty?
|
|
112
|
+
Array(set_cookie_header).each do |cookie|
|
|
113
|
+
cookie_name, cookie_filling = cookie.split('=', 2)
|
|
114
|
+
cookie_attributes = identify_cookie_attributes cookie_filling
|
|
115
|
+
parsed_cookie = Cookie.new(
|
|
116
|
+
'name' => cookie_name.strip,
|
|
117
|
+
'value' => cookie_attributes.fetch('value'),
|
|
118
|
+
'path' => cookie_attributes.fetch('path', nil),
|
|
119
|
+
'domain' => cookie_attributes.fetch('domain', nil),
|
|
120
|
+
'expires' => cookie_attributes.fetch('expires', nil),
|
|
121
|
+
'secure' => cookie_attributes.fetch('secure', false)
|
|
122
|
+
)
|
|
123
|
+
cookies.store(cookie_name, parsed_cookie)
|
|
126
124
|
end
|
|
127
125
|
end
|
|
128
126
|
cookies
|
|
@@ -3,53 +3,71 @@
|
|
|
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"
|
|
34
|
+
FWS = /[ \t]+(?:\r\n[ \t]+)?/ # whitespace with optional folding
|
|
35
|
+
HEADER_VALUE = "(?:[^\r\n]|\r\n[ \t])*" # anything but a non-folding CRLF
|
|
22
36
|
MULTIPART = %r|\Amultipart/.*boundary=\"?([^\";,]+)\"?|ni
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
BROKEN = /^#{CONDISP}.*;\s*filename=(#{VALUE})/i
|
|
27
|
-
MULTIPART_CONTENT_TYPE = /Content-Type: (.*)#{EOL}/ni
|
|
28
|
-
MULTIPART_CONTENT_DISPOSITION = /Content-Disposition:[^:]*;\s*name=(#{VALUE})/ni
|
|
29
|
-
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
|
|
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
|
|
45
40
|
|
|
46
41
|
class Parser
|
|
47
42
|
BUFSIZE = 1_048_576
|
|
48
43
|
TEXT_PLAIN = "text/plain"
|
|
49
44
|
TEMPFILE_FACTORY = lambda { |filename, content_type|
|
|
50
|
-
|
|
45
|
+
extension = ::File.extname(filename.gsub("\0", '%00'))[0, 129]
|
|
46
|
+
|
|
47
|
+
Tempfile.new(["RackMultipart", extension])
|
|
51
48
|
}
|
|
52
49
|
|
|
50
|
+
BOUNDARY_START_LIMIT = 16 * 1024
|
|
51
|
+
private_constant :BOUNDARY_START_LIMIT
|
|
52
|
+
|
|
53
|
+
MIME_HEADER_BYTESIZE_LIMIT = 64 * 1024
|
|
54
|
+
private_constant :MIME_HEADER_BYTESIZE_LIMIT
|
|
55
|
+
|
|
56
|
+
env_int = lambda do |key, val|
|
|
57
|
+
if str_val = ENV[key]
|
|
58
|
+
begin
|
|
59
|
+
val = Integer(str_val, 10)
|
|
60
|
+
rescue ArgumentError
|
|
61
|
+
raise ArgumentError, "non-integer value provided for environment variable #{key}"
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
val
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
BUFFERED_UPLOAD_BYTESIZE_LIMIT = env_int.call("RACK_MULTIPART_BUFFERED_UPLOAD_BYTESIZE_LIMIT", 16 * 1024 * 1024)
|
|
69
|
+
private_constant :BUFFERED_UPLOAD_BYTESIZE_LIMIT
|
|
70
|
+
|
|
53
71
|
class BoundedIO # :nodoc:
|
|
54
72
|
def initialize(io, content_length)
|
|
55
73
|
@io = io
|
|
@@ -98,7 +116,7 @@ module Rack
|
|
|
98
116
|
if boundary.length > 70
|
|
99
117
|
# RFC 1521 Section 7.2.1 imposes a 70 character maximum for the boundary.
|
|
100
118
|
# Most clients use no more than 55 characters.
|
|
101
|
-
raise
|
|
119
|
+
raise BoundaryTooLongError, "multipart boundary size too large (#{boundary.length} characters)"
|
|
102
120
|
end
|
|
103
121
|
|
|
104
122
|
io = BoundedIO.new(io, content_length) if content_length
|
|
@@ -209,10 +227,13 @@ module Rack
|
|
|
209
227
|
|
|
210
228
|
@state = :FAST_FORWARD
|
|
211
229
|
@mime_index = 0
|
|
230
|
+
@body_retained = nil
|
|
231
|
+
@retained_size = 0
|
|
212
232
|
@collector = Collector.new tempfile
|
|
213
233
|
|
|
214
234
|
@sbuf = StringScanner.new("".dup)
|
|
215
235
|
@body_regex = /(?:#{EOL}|\A)--#{Regexp.quote(boundary)}(?:#{EOL}|--)/m
|
|
236
|
+
@body_regex_at_end = /#{@body_regex}\z/m
|
|
216
237
|
@end_boundary_size = boundary.bytesize + 4 # (-- at start, -- at finish)
|
|
217
238
|
@rx_max_size = boundary.bytesize + 6 # (\r\n-- at start, either \r\n or -- at finish)
|
|
218
239
|
@head_regex = /(.*?#{EOL})#{EOL}/m
|
|
@@ -289,6 +310,10 @@ module Rack
|
|
|
289
310
|
|
|
290
311
|
# retry for opening boundary
|
|
291
312
|
else
|
|
313
|
+
# We raise if we don't find the multipart boundary, to avoid unbounded memory
|
|
314
|
+
# buffering. Note that the actual limit is the higher of 16KB and the buffer size (1MB by default)
|
|
315
|
+
raise Error, "multipart boundary not found within limit" if @sbuf.string.bytesize > BOUNDARY_START_LIMIT
|
|
316
|
+
|
|
292
317
|
# no boundary found, keep reading data
|
|
293
318
|
return :want_read
|
|
294
319
|
end
|
|
@@ -305,32 +330,130 @@ module Rack
|
|
|
305
330
|
end
|
|
306
331
|
end
|
|
307
332
|
|
|
333
|
+
CONTENT_DISPOSITION_MAX_PARAMS = 16
|
|
334
|
+
CONTENT_DISPOSITION_MAX_BYTES = 1536
|
|
308
335
|
def handle_mime_head
|
|
309
336
|
if @sbuf.scan_until(@head_regex)
|
|
310
337
|
head = @sbuf[1]
|
|
311
338
|
content_type = head[MULTIPART_CONTENT_TYPE, 1]
|
|
312
|
-
if
|
|
313
|
-
|
|
339
|
+
if (disposition = head[MULTIPART_CONTENT_DISPOSITION, 1]) &&
|
|
340
|
+
disposition.bytesize <= CONTENT_DISPOSITION_MAX_BYTES
|
|
341
|
+
|
|
342
|
+
# ignore actual content-disposition value (should always be form-data)
|
|
343
|
+
i = disposition.index(';')
|
|
344
|
+
disposition.slice!(0, i+1)
|
|
345
|
+
param = nil
|
|
346
|
+
num_params = 0
|
|
347
|
+
|
|
348
|
+
# Parse parameter list
|
|
349
|
+
while i = disposition.index('=')
|
|
350
|
+
# Only parse up to max parameters, to avoid potential denial of service
|
|
351
|
+
num_params += 1
|
|
352
|
+
break if num_params > CONTENT_DISPOSITION_MAX_PARAMS
|
|
353
|
+
|
|
354
|
+
# Found end of parameter name, ensure forward progress in loop
|
|
355
|
+
param = disposition.slice!(0, i+1)
|
|
356
|
+
|
|
357
|
+
# Remove ending equals and preceding whitespace from parameter name
|
|
358
|
+
param.chomp!('=')
|
|
359
|
+
param.lstrip!
|
|
360
|
+
|
|
361
|
+
if disposition[0] == '"'
|
|
362
|
+
# Parameter value is quoted, parse it, handling backslash escapes
|
|
363
|
+
disposition.slice!(0, 1)
|
|
364
|
+
value = String.new
|
|
365
|
+
|
|
366
|
+
while i = disposition.index(/(["\\])/)
|
|
367
|
+
c = $1
|
|
368
|
+
|
|
369
|
+
# Append all content until ending quote or escape
|
|
370
|
+
value << disposition.slice!(0, i)
|
|
371
|
+
|
|
372
|
+
# Remove either backslash or ending quote,
|
|
373
|
+
# ensures forward progress in loop
|
|
374
|
+
disposition.slice!(0, 1)
|
|
375
|
+
|
|
376
|
+
# stop parsing parameter value if found ending quote
|
|
377
|
+
break if c == '"'
|
|
378
|
+
|
|
379
|
+
escaped_char = disposition.slice!(0, 1)
|
|
380
|
+
if param == 'filename' && escaped_char != '"'
|
|
381
|
+
# Possible IE uploaded filename, append both escape backslash and value
|
|
382
|
+
value << c << escaped_char
|
|
383
|
+
else
|
|
384
|
+
# Other only append escaped value
|
|
385
|
+
value << escaped_char
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
else
|
|
389
|
+
if i = disposition.index(';')
|
|
390
|
+
# Parameter value unquoted (which may be invalid), value ends at semicolon
|
|
391
|
+
value = disposition.slice!(0, i)
|
|
392
|
+
else
|
|
393
|
+
# If no ending semicolon, assume remainder of line is value and stop
|
|
394
|
+
# parsing
|
|
395
|
+
disposition.strip!
|
|
396
|
+
value = disposition
|
|
397
|
+
disposition = ''
|
|
398
|
+
end
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
case param
|
|
402
|
+
when 'name'
|
|
403
|
+
name = value
|
|
404
|
+
when 'filename'
|
|
405
|
+
filename = value
|
|
406
|
+
when 'filename*'
|
|
407
|
+
filename_star = value
|
|
408
|
+
# else
|
|
409
|
+
# ignore other parameters
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
# skip trailing semicolon, to proceed to next parameter
|
|
413
|
+
if i = disposition.index(';')
|
|
414
|
+
disposition.slice!(0, i+1)
|
|
415
|
+
end
|
|
416
|
+
end
|
|
314
417
|
else
|
|
315
418
|
name = head[MULTIPART_CONTENT_ID, 1]
|
|
316
419
|
end
|
|
317
420
|
|
|
318
|
-
|
|
421
|
+
if filename_star
|
|
422
|
+
encoding, _, filename = filename_star.split("'", 3)
|
|
423
|
+
filename = normalize_filename(filename || '')
|
|
424
|
+
filename.force_encoding(find_encoding(encoding))
|
|
425
|
+
elsif filename
|
|
426
|
+
filename = normalize_filename(filename)
|
|
427
|
+
end
|
|
319
428
|
|
|
320
429
|
if name.nil? || name.empty?
|
|
321
430
|
name = filename || "#{content_type || TEXT_PLAIN}[]".dup
|
|
322
431
|
end
|
|
323
432
|
|
|
433
|
+
# Mime part head data is retained for both TempfilePart and BufferPart
|
|
434
|
+
# for the entireity of the parse, even though it isn't used for BufferPart.
|
|
435
|
+
update_retained_size(head.bytesize)
|
|
436
|
+
|
|
437
|
+
# If a filename is given, a TempfilePart will be used, so the body will
|
|
438
|
+
# not be buffered in memory. However, if a filename is not given, a BufferPart
|
|
439
|
+
# will be used, and the body will be buffered in memory.
|
|
440
|
+
@body_retained = !filename
|
|
441
|
+
|
|
324
442
|
@collector.on_mime_head @mime_index, head, filename, content_type, name
|
|
325
443
|
@state = :MIME_BODY
|
|
326
444
|
else
|
|
327
|
-
|
|
445
|
+
# We raise if the mime part header is too large, to avoid unbounded memory
|
|
446
|
+
# buffering. Note that the actual limit is the higher of 64KB and the buffer size (1MB by default)
|
|
447
|
+
raise Error, "multipart mime part header too large" if @sbuf.string.bytesize > MIME_HEADER_BYTESIZE_LIMIT
|
|
448
|
+
|
|
449
|
+
return :want_read
|
|
328
450
|
end
|
|
329
451
|
end
|
|
330
452
|
|
|
331
453
|
def handle_mime_body
|
|
332
454
|
if (body_with_boundary = @sbuf.check_until(@body_regex)) # check but do not advance the pointer yet
|
|
333
|
-
body = body_with_boundary.sub(
|
|
455
|
+
body = body_with_boundary.sub(@body_regex_at_end, '') # remove the boundary from the string
|
|
456
|
+
update_retained_size(body.bytesize) if @body_retained
|
|
334
457
|
@collector.on_mime_body @mime_index, body
|
|
335
458
|
@sbuf.pos += body.length + 2 # skip \r\n after the content
|
|
336
459
|
@state = :CONSUME_TOKEN
|
|
@@ -339,7 +462,9 @@ module Rack
|
|
|
339
462
|
# Save what we have so far
|
|
340
463
|
if @rx_max_size < @sbuf.rest_size
|
|
341
464
|
delta = @sbuf.rest_size - @rx_max_size
|
|
342
|
-
|
|
465
|
+
body = @sbuf.peek(delta)
|
|
466
|
+
update_retained_size(body.bytesize) if @body_retained
|
|
467
|
+
@collector.on_mime_body @mime_index, body
|
|
343
468
|
@sbuf.pos += delta
|
|
344
469
|
@sbuf.string = @sbuf.rest
|
|
345
470
|
end
|
|
@@ -347,6 +472,13 @@ module Rack
|
|
|
347
472
|
end
|
|
348
473
|
end
|
|
349
474
|
|
|
475
|
+
def update_retained_size(size)
|
|
476
|
+
@retained_size += size
|
|
477
|
+
if @retained_size > BUFFERED_UPLOAD_BYTESIZE_LIMIT
|
|
478
|
+
raise Error, "multipart data over retained size limit"
|
|
479
|
+
end
|
|
480
|
+
end
|
|
481
|
+
|
|
350
482
|
# Scan until the we find the start or end of the boundary.
|
|
351
483
|
# If we find it, return the appropriate symbol for the start or
|
|
352
484
|
# end of the boundary. If we don't find the start or end of the
|
|
@@ -360,39 +492,14 @@ module Rack
|
|
|
360
492
|
end
|
|
361
493
|
end
|
|
362
494
|
|
|
363
|
-
def
|
|
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
|
-
|
|
495
|
+
def normalize_filename(filename)
|
|
381
496
|
if filename.scan(/%.?.?/).all? { |s| /%[0-9a-fA-F]{2}/.match?(s) }
|
|
382
497
|
filename = Utils.unescape_path(filename)
|
|
383
498
|
end
|
|
384
499
|
|
|
385
500
|
filename.scrub!
|
|
386
501
|
|
|
387
|
-
|
|
388
|
-
filename = filename.gsub(/\\(.)/, '\1')
|
|
389
|
-
end
|
|
390
|
-
|
|
391
|
-
if encoding
|
|
392
|
-
filename.force_encoding ::Encoding.find(encoding)
|
|
393
|
-
end
|
|
394
|
-
|
|
395
|
-
filename
|
|
502
|
+
filename.split(/[\/\\]/).last || String.new
|
|
396
503
|
end
|
|
397
504
|
|
|
398
505
|
CHARSET = "charset"
|
|
@@ -418,11 +525,7 @@ module Rack
|
|
|
418
525
|
v.strip!
|
|
419
526
|
v = v[1..-2] if v.start_with?('"') && v.end_with?('"')
|
|
420
527
|
if k == "charset"
|
|
421
|
-
encoding =
|
|
422
|
-
Encoding.find v
|
|
423
|
-
rescue ArgumentError
|
|
424
|
-
Encoding::BINARY
|
|
425
|
-
end
|
|
528
|
+
encoding = find_encoding(v)
|
|
426
529
|
end
|
|
427
530
|
end
|
|
428
531
|
end
|
|
@@ -432,6 +535,15 @@ module Rack
|
|
|
432
535
|
body.force_encoding(encoding)
|
|
433
536
|
end
|
|
434
537
|
|
|
538
|
+
# Return the related Encoding object. However, because
|
|
539
|
+
# enc is submitted by the user, it may be invalid, so
|
|
540
|
+
# use a binary encoding in that case.
|
|
541
|
+
def find_encoding(enc)
|
|
542
|
+
Encoding.find enc
|
|
543
|
+
rescue ArgumentError
|
|
544
|
+
Encoding::BINARY
|
|
545
|
+
end
|
|
546
|
+
|
|
435
547
|
def handle_empty_content!(content)
|
|
436
548
|
if content.nil? || content.empty?
|
|
437
549
|
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,24 +1,30 @@
|
|
|
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 =
|
|
8
|
-
COMMON_SEP = { ";" =>
|
|
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
|
|
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
|
|
20
|
+
class InvalidParameterError < ArgumentError
|
|
21
|
+
include BadRequest
|
|
22
|
+
end
|
|
18
23
|
|
|
19
24
|
# QueryLimitError is for errors raised when the query provided exceeds one
|
|
20
25
|
# of the query parser limits.
|
|
21
26
|
class QueryLimitError < RangeError
|
|
27
|
+
include BadRequest
|
|
22
28
|
end
|
|
23
29
|
|
|
24
30
|
# ParamsTooDeepError is the old name for the error that is raised when params
|
|
@@ -27,12 +33,8 @@ module Rack
|
|
|
27
33
|
# to handle bad query strings also now handles other limits.
|
|
28
34
|
ParamsTooDeepError = QueryLimitError
|
|
29
35
|
|
|
30
|
-
def self.make_default(
|
|
31
|
-
|
|
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
|
|
36
|
+
def self.make_default(param_depth_limit, **options)
|
|
37
|
+
new(Params, param_depth_limit, **options)
|
|
36
38
|
end
|
|
37
39
|
|
|
38
40
|
attr_reader :param_depth_limit
|
|
@@ -55,11 +57,9 @@ module Rack
|
|
|
55
57
|
PARAMS_LIMIT = env_int.call("RACK_QUERY_PARSER_PARAMS_LIMIT", 4096)
|
|
56
58
|
private_constant :PARAMS_LIMIT
|
|
57
59
|
|
|
58
|
-
|
|
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
|
|
60
|
+
attr_reader :bytesize_limit
|
|
62
61
|
|
|
62
|
+
def initialize(params_class, param_depth_limit, bytesize_limit: BYTESIZE_LIMIT, params_limit: PARAMS_LIMIT)
|
|
63
63
|
@params_class = params_class
|
|
64
64
|
@param_depth_limit = param_depth_limit
|
|
65
65
|
@bytesize_limit = bytesize_limit
|
|
@@ -220,7 +220,7 @@ module Rack
|
|
|
220
220
|
def check_query_string(qs, sep)
|
|
221
221
|
if qs
|
|
222
222
|
if qs.bytesize > @bytesize_limit
|
|
223
|
-
raise QueryLimitError, "total query size
|
|
223
|
+
raise QueryLimitError, "total query size exceeds limit (#{@bytesize_limit})"
|
|
224
224
|
end
|
|
225
225
|
|
|
226
226
|
if (param_count = qs.count(sep.is_a?(String) ? sep : '&')) >= @params_limit
|
|
@@ -237,59 +237,7 @@ module Rack
|
|
|
237
237
|
URI.decode_www_form_component(string, encoding)
|
|
238
238
|
end
|
|
239
239
|
|
|
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
|
|
240
|
+
class Params < Hash
|
|
293
241
|
alias_method :to_params_hash, :to_h
|
|
294
242
|
end
|
|
295
243
|
end
|