rack 2.2.6.4 → 3.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of rack might be problematic. Click here for more details.

Files changed (87) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +205 -80
  3. data/CONTRIBUTING.md +53 -47
  4. data/MIT-LICENSE +1 -1
  5. data/README.md +309 -0
  6. data/SPEC.rdoc +183 -131
  7. data/lib/rack/auth/abstract/handler.rb +3 -1
  8. data/lib/rack/auth/abstract/request.rb +3 -1
  9. data/lib/rack/auth/basic.rb +0 -2
  10. data/lib/rack/auth/digest/md5.rb +1 -131
  11. data/lib/rack/auth/digest/nonce.rb +1 -54
  12. data/lib/rack/auth/digest/params.rb +1 -54
  13. data/lib/rack/auth/digest/request.rb +1 -43
  14. data/lib/rack/auth/digest.rb +256 -0
  15. data/lib/rack/body_proxy.rb +3 -1
  16. data/lib/rack/builder.rb +83 -63
  17. data/lib/rack/cascade.rb +2 -0
  18. data/lib/rack/chunked.rb +16 -13
  19. data/lib/rack/common_logger.rb +23 -18
  20. data/lib/rack/conditional_get.rb +18 -15
  21. data/lib/rack/constants.rb +64 -0
  22. data/lib/rack/content_length.rb +12 -16
  23. data/lib/rack/content_type.rb +8 -5
  24. data/lib/rack/deflater.rb +40 -26
  25. data/lib/rack/directory.rb +9 -3
  26. data/lib/rack/etag.rb +14 -23
  27. data/lib/rack/events.rb +4 -0
  28. data/lib/rack/file.rb +2 -0
  29. data/lib/rack/files.rb +15 -17
  30. data/lib/rack/head.rb +9 -8
  31. data/lib/rack/headers.rb +154 -0
  32. data/lib/rack/lint.rb +783 -682
  33. data/lib/rack/lock.rb +2 -5
  34. data/lib/rack/logger.rb +2 -0
  35. data/lib/rack/media_type.rb +1 -1
  36. data/lib/rack/method_override.rb +5 -1
  37. data/lib/rack/mime.rb +8 -0
  38. data/lib/rack/mock.rb +1 -271
  39. data/lib/rack/mock_request.rb +166 -0
  40. data/lib/rack/mock_response.rb +126 -0
  41. data/lib/rack/multipart/generator.rb +7 -5
  42. data/lib/rack/multipart/parser.rb +119 -61
  43. data/lib/rack/multipart/uploaded_file.rb +4 -0
  44. data/lib/rack/multipart.rb +20 -40
  45. data/lib/rack/null_logger.rb +9 -0
  46. data/lib/rack/query_parser.rb +78 -46
  47. data/lib/rack/recursive.rb +2 -0
  48. data/lib/rack/reloader.rb +0 -2
  49. data/lib/rack/request.rb +224 -106
  50. data/lib/rack/response.rb +136 -61
  51. data/lib/rack/rewindable_input.rb +24 -5
  52. data/lib/rack/runtime.rb +7 -6
  53. data/lib/rack/sendfile.rb +30 -25
  54. data/lib/rack/show_exceptions.rb +15 -2
  55. data/lib/rack/show_status.rb +17 -7
  56. data/lib/rack/static.rb +8 -8
  57. data/lib/rack/tempfile_reaper.rb +15 -4
  58. data/lib/rack/urlmap.rb +3 -1
  59. data/lib/rack/utils.rb +202 -176
  60. data/lib/rack/version.rb +9 -4
  61. data/lib/rack.rb +6 -76
  62. metadata +18 -38
  63. data/README.rdoc +0 -320
  64. data/Rakefile +0 -130
  65. data/bin/rackup +0 -5
  66. data/contrib/rack.png +0 -0
  67. data/contrib/rack.svg +0 -150
  68. data/contrib/rack_logo.svg +0 -164
  69. data/contrib/rdoc.css +0 -412
  70. data/example/lobster.ru +0 -6
  71. data/example/protectedlobster.rb +0 -16
  72. data/example/protectedlobster.ru +0 -10
  73. data/lib/rack/core_ext/regexp.rb +0 -14
  74. data/lib/rack/handler/cgi.rb +0 -59
  75. data/lib/rack/handler/fastcgi.rb +0 -100
  76. data/lib/rack/handler/lsws.rb +0 -61
  77. data/lib/rack/handler/scgi.rb +0 -71
  78. data/lib/rack/handler/thin.rb +0 -36
  79. data/lib/rack/handler/webrick.rb +0 -129
  80. data/lib/rack/handler.rb +0 -104
  81. data/lib/rack/lobster.rb +0 -70
  82. data/lib/rack/server.rb +0 -466
  83. data/lib/rack/session/abstract/id.rb +0 -523
  84. data/lib/rack/session/cookie.rb +0 -203
  85. data/lib/rack/session/memcache.rb +0 -10
  86. data/lib/rack/session/pool.rb +0 -85
  87. data/rack.gemspec +0 -46
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'uploaded_file'
4
+
3
5
  module Rack
4
6
  module Multipart
5
7
  class Generator
@@ -74,12 +76,12 @@ module Rack
74
76
 
75
77
  def content_for_tempfile(io, file, name)
76
78
  length = ::File.stat(file.path).size if file.path
77
- filename = "; filename=\"#{Utils.escape(file.original_filename)}\"" if file.original_filename
79
+ filename = "; filename=\"#{Utils.escape_path(file.original_filename)}\""
78
80
  <<-EOF
79
81
  --#{MULTIPART_BOUNDARY}\r
80
- Content-Disposition: form-data; name="#{name}"#{filename}\r
81
- Content-Type: #{file.content_type}\r
82
- #{"Content-Length: #{length}\r\n" if length}\r
82
+ content-disposition: form-data; name="#{name}"#{filename}\r
83
+ content-type: #{file.content_type}\r
84
+ #{"content-length: #{length}\r\n" if length}\r
83
85
  #{io.read}\r
84
86
  EOF
85
87
  end
@@ -87,7 +89,7 @@ EOF
87
89
  def content_for_other(file, name)
88
90
  <<-EOF
89
91
  --#{MULTIPART_BOUNDARY}\r
90
- Content-Disposition: form-data; name="#{name}"\r
92
+ content-disposition: form-data; name="#{name}"\r
91
93
  \r
92
94
  #{file}\r
93
95
  EOF
@@ -2,22 +2,54 @@
2
2
 
3
3
  require 'strscan'
4
4
 
5
+ require_relative '../utils'
6
+
5
7
  module Rack
6
8
  module Multipart
7
9
  class MultipartPartLimitError < Errno::EMFILE; end
10
+
8
11
  class MultipartTotalPartLimitError < StandardError; end
9
12
 
10
- class Parser
11
- (require_relative '../core_ext/regexp'; using ::Rack::RegexpExtensions) if RUBY_VERSION < '2.4'
13
+ # Use specific error class when parsing multipart request
14
+ # that ends early.
15
+ class EmptyContentError < ::EOFError; end
16
+
17
+ # Base class for multipart exceptions that do not subclass from
18
+ # other exception classes for backwards compatibility.
19
+ class Error < StandardError; end
20
+
21
+ EOL = "\r\n"
22
+ 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
+ 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
12
45
 
46
+ class Parser
13
47
  BUFSIZE = 1_048_576
14
48
  TEXT_PLAIN = "text/plain"
15
49
  TEMPFILE_FACTORY = lambda { |filename, content_type|
16
50
  Tempfile.new(["RackMultipart", ::File.extname(filename.gsub("\0", '%00'))])
17
51
  }
18
52
 
19
- BOUNDARY_REGEX = /\A([^\n]*(?:\n|\Z))/
20
-
21
53
  class BoundedIO # :nodoc:
22
54
  def initialize(io, content_length)
23
55
  @io = io
@@ -39,16 +71,12 @@ module Rack
39
71
  if str
40
72
  @cursor += str.bytesize
41
73
  else
42
- # Raise an error for mismatching Content-Length and actual contents
74
+ # Raise an error for mismatching content-length and actual contents
43
75
  raise EOFError, "bad content body"
44
76
  end
45
77
 
46
78
  str
47
79
  end
48
-
49
- def rewind
50
- @io.rewind
51
- end
52
80
  end
53
81
 
54
82
  MultipartInfo = Struct.new :params, :tmp_files
@@ -67,18 +95,17 @@ module Rack
67
95
  boundary = parse_boundary content_type
68
96
  return EMPTY unless boundary
69
97
 
98
+ if boundary.length > 70
99
+ # RFC 1521 Section 7.2.1 imposes a 70 character maximum for the boundary.
100
+ # Most clients use no more than 55 characters.
101
+ raise Error, "multipart boundary size too large (#{boundary.length} characters)"
102
+ end
103
+
70
104
  io = BoundedIO.new(io, content_length) if content_length
71
- outbuf = String.new
72
105
 
73
106
  parser = new(boundary, tmpfile, bufsize, qp)
74
- parser.on_read io.read(bufsize, outbuf)
107
+ parser.parse(io)
75
108
 
76
- loop do
77
- break if parser.state == :DONE
78
- parser.on_read io.read(bufsize, outbuf)
79
- end
80
-
81
- io.rewind
82
109
  parser.result
83
110
  end
84
111
 
@@ -178,32 +205,46 @@ module Rack
178
205
  def initialize(boundary, tempfile, bufsize, query_parser)
179
206
  @query_parser = query_parser
180
207
  @params = query_parser.make_params
181
- @boundary = "--#{boundary}"
182
208
  @bufsize = bufsize
183
209
 
184
- @full_boundary = @boundary
185
- @end_boundary = @boundary + '--'
186
210
  @state = :FAST_FORWARD
187
211
  @mime_index = 0
188
212
  @collector = Collector.new tempfile
189
213
 
190
214
  @sbuf = StringScanner.new("".dup)
191
- @body_regex = /(?:#{EOL})?#{Regexp.quote(@boundary)}(?:#{EOL}|--)/m
192
- @rx_max_size = EOL.size + @boundary.bytesize + [EOL.size, '--'.size].max
215
+ @body_regex = /(?:#{EOL}|\A)--#{Regexp.quote(boundary)}(?:#{EOL}|--)/m
216
+ @rx_max_size = boundary.bytesize + 6 # (\r\n-- at start, either \r\n or -- at finish)
193
217
  @head_regex = /(.*?#{EOL})#{EOL}/m
194
218
  end
195
219
 
196
- def on_read(content)
197
- handle_empty_content!(content)
198
- @sbuf.concat content
199
- run_parser
220
+ def parse(io)
221
+ outbuf = String.new
222
+ read_data(io, outbuf)
223
+
224
+ loop do
225
+ status =
226
+ case @state
227
+ when :FAST_FORWARD
228
+ handle_fast_forward
229
+ when :CONSUME_TOKEN
230
+ handle_consume_token
231
+ when :MIME_HEAD
232
+ handle_mime_head
233
+ when :MIME_BODY
234
+ handle_mime_body
235
+ else # when :DONE
236
+ return
237
+ end
238
+
239
+ read_data(io, outbuf) if status == :want_read
240
+ end
200
241
  end
201
242
 
202
243
  def result
203
244
  @collector.each do |part|
204
245
  part.get_data do |data|
205
246
  tag_multipart_encoding(part.filename, part.content_type, part.name, data)
206
- @query_parser.normalize_params(@params, part.name, data, @query_parser.param_depth_limit)
247
+ @query_parser.normalize_params(@params, part.name, data)
207
248
  end
208
249
  end
209
250
  MultipartInfo.new @params.to_params_hash, @collector.find_all(&:file?).map(&:body)
@@ -211,29 +252,38 @@ module Rack
211
252
 
212
253
  private
213
254
 
214
- def run_parser
215
- loop do
216
- case @state
217
- when :FAST_FORWARD
218
- break if handle_fast_forward == :want_read
219
- when :CONSUME_TOKEN
220
- break if handle_consume_token == :want_read
221
- when :MIME_HEAD
222
- break if handle_mime_head == :want_read
223
- when :MIME_BODY
224
- break if handle_mime_body == :want_read
225
- when :DONE
226
- break
227
- end
228
- end
255
+ def dequote(str) # From WEBrick::HTTPUtils
256
+ ret = (/\A"(.*)"\Z/ =~ str) ? $1 : str.dup
257
+ ret.gsub!(/\\(.)/, "\\1")
258
+ ret
229
259
  end
230
260
 
261
+ def read_data(io, outbuf)
262
+ content = io.read(@bufsize, outbuf)
263
+ handle_empty_content!(content)
264
+ @sbuf.concat(content)
265
+ end
266
+
267
+ # This handles the initial parser state. We read until we find the starting
268
+ # boundary, then we can transition to the next state. If we find the ending
269
+ # boundary, this is an invalid multipart upload, but keep scanning for opening
270
+ # boundary in that case. If no boundary found, we need to keep reading data
271
+ # and retry. It's highly unlikely the initial read will not consume the
272
+ # boundary. The client would have to deliberately craft a response
273
+ # with the opening boundary beyond the buffer size for that to happen.
231
274
  def handle_fast_forward
232
- if consume_boundary
233
- @state = :MIME_HEAD
234
- else
235
- raise EOFError, "bad content body" if @sbuf.rest_size >= @bufsize
236
- :want_read
275
+ while true
276
+ case consume_boundary
277
+ when :BOUNDARY
278
+ # found opening boundary, transition to next state
279
+ @state = :MIME_HEAD
280
+ return
281
+ when :END_BOUNDARY
282
+ # invalid multipart upload, but retry for opening boundary
283
+ else
284
+ # no boundary found, keep reading data
285
+ return :want_read
286
+ end
237
287
  end
238
288
  end
239
289
 
@@ -252,7 +302,7 @@ module Rack
252
302
  head = @sbuf[1]
253
303
  content_type = head[MULTIPART_CONTENT_TYPE, 1]
254
304
  if name = head[MULTIPART_CONTENT_DISPOSITION, 1]
255
- name = Rack::Auth::Digest::Params::dequote(name)
305
+ name = dequote(name)
256
306
  else
257
307
  name = head[MULTIPART_CONTENT_ID, 1]
258
308
  end
@@ -289,15 +339,16 @@ module Rack
289
339
  end
290
340
  end
291
341
 
292
- def full_boundary; @full_boundary; end
293
-
342
+ # Scan until the we find the start or end of the boundary.
343
+ # If we find it, return the appropriate symbol for the start or
344
+ # end of the boundary. If we don't find the start or end of the
345
+ # boundary, clear the buffer and return nil.
294
346
  def consume_boundary
295
- while read_buffer = @sbuf.scan_until(BOUNDARY_REGEX)
296
- case read_buffer.strip
297
- when full_boundary then return :BOUNDARY
298
- when @end_boundary then return :END_BOUNDARY
299
- end
300
- return if @sbuf.eos?
347
+ if read_buffer = @sbuf.scan_until(@body_regex)
348
+ read_buffer.end_with?(EOL) ? :BOUNDARY : :END_BOUNDARY
349
+ else
350
+ @sbuf.terminate
351
+ nil
301
352
  end
302
353
  end
303
354
 
@@ -307,10 +358,10 @@ module Rack
307
358
  when RFC2183
308
359
  params = Hash[*head.scan(DISPPARM).flat_map(&:compact)]
309
360
 
310
- if filename = params['filename']
311
- filename = $1 if filename =~ /^"(.*)"$/
312
- elsif filename = params['filename*']
361
+ if filename = params['filename*']
313
362
  encoding, _, filename = filename.split("'", 3)
363
+ elsif filename = params['filename']
364
+ filename = $1 if filename =~ /^"(.*)"$/
314
365
  end
315
366
  when BROKEN
316
367
  filename = $1
@@ -337,6 +388,7 @@ module Rack
337
388
  end
338
389
 
339
390
  CHARSET = "charset"
391
+ deprecate_constant :CHARSET
340
392
 
341
393
  def tag_multipart_encoding(filename, content_type, name, body)
342
394
  name = name.to_s
@@ -357,7 +409,13 @@ module Rack
357
409
  k.strip!
358
410
  v.strip!
359
411
  v = v[1..-2] if v.start_with?('"') && v.end_with?('"')
360
- encoding = Encoding.find v if k == CHARSET
412
+ if k == "charset"
413
+ encoding = begin
414
+ Encoding.find v
415
+ rescue ArgumentError
416
+ Encoding::BINARY
417
+ end
418
+ end
361
419
  end
362
420
  end
363
421
  end
@@ -368,7 +426,7 @@ module Rack
368
426
 
369
427
  def handle_empty_content!(content)
370
428
  if content.nil? || content.empty?
371
- raise EOFError
429
+ raise EmptyContentError
372
430
  end
373
431
  end
374
432
  end
@@ -1,8 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'tempfile'
4
+ require 'fileutils'
5
+
3
6
  module Rack
4
7
  module Multipart
5
8
  class UploadedFile
9
+
6
10
  # The filename, *not* including the path, of the "uploaded" file
7
11
  attr_reader :original_filename
8
12
 
@@ -1,64 +1,44 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'constants'
4
+ require_relative 'utils'
5
+
3
6
  require_relative 'multipart/parser'
7
+ require_relative 'multipart/generator'
4
8
 
5
9
  module Rack
6
10
  # A multipart form data parser, adapted from IOWA.
7
11
  #
8
12
  # Usually, Rack::Request#POST takes care of calling this.
9
13
  module Multipart
10
- autoload :UploadedFile, 'rack/multipart/uploaded_file'
11
- autoload :Generator, 'rack/multipart/generator'
12
-
13
- EOL = "\r\n"
14
14
  MULTIPART_BOUNDARY = "AaB03x"
15
- MULTIPART = %r|\Amultipart/.*boundary=\"?([^\";,]+)\"?|ni
16
- TOKEN = /[^\s()<>,;:\\"\/\[\]?=]+/
17
- CONDISP = /Content-Disposition:\s*#{TOKEN}\s*/i
18
- VALUE = /"(?:\\"|[^"])*"|#{TOKEN}/
19
- BROKEN = /^#{CONDISP}.*;\s*filename=(#{VALUE})/i
20
- MULTIPART_CONTENT_TYPE = /Content-Type: (.*)#{EOL}/ni
21
- MULTIPART_CONTENT_DISPOSITION = /Content-Disposition:[^:]*;\s*name=(#{VALUE})/ni
22
- MULTIPART_CONTENT_ID = /Content-ID:\s*([^#{EOL}]*)/ni
23
- # Updated definitions from RFC 2231
24
- ATTRIBUTE_CHAR = %r{[^ \x00-\x1f\x7f)(><@,;:\\"/\[\]?='*%]}
25
- ATTRIBUTE = /#{ATTRIBUTE_CHAR}+/
26
- SECTION = /\*[0-9]+/
27
- REGULAR_PARAMETER_NAME = /#{ATTRIBUTE}#{SECTION}?/
28
- REGULAR_PARAMETER = /(#{REGULAR_PARAMETER_NAME})=(#{VALUE})/
29
- EXTENDED_OTHER_NAME = /#{ATTRIBUTE}\*[1-9][0-9]*\*/
30
- EXTENDED_OTHER_VALUE = /%[0-9a-fA-F]{2}|#{ATTRIBUTE_CHAR}/
31
- EXTENDED_OTHER_PARAMETER = /(#{EXTENDED_OTHER_NAME})=(#{EXTENDED_OTHER_VALUE}*)/
32
- EXTENDED_INITIAL_NAME = /#{ATTRIBUTE}(?:\*0)?\*/
33
- EXTENDED_INITIAL_VALUE = /[a-zA-Z0-9\-]*'[a-zA-Z0-9\-]*'#{EXTENDED_OTHER_VALUE}*/
34
- EXTENDED_INITIAL_PARAMETER = /(#{EXTENDED_INITIAL_NAME})=(#{EXTENDED_INITIAL_VALUE})/
35
- EXTENDED_PARAMETER = /#{EXTENDED_INITIAL_PARAMETER}|#{EXTENDED_OTHER_PARAMETER}/
36
- DISPPARM = /;\s*(?:#{REGULAR_PARAMETER}|#{EXTENDED_PARAMETER})\s*/
37
- RFC2183 = /^#{CONDISP}(#{DISPPARM})+$/i
38
15
 
39
16
  class << self
40
17
  def parse_multipart(env, params = Rack::Utils.default_query_parser)
41
- extract_multipart Rack::Request.new(env), params
42
- end
18
+ io = env[RACK_INPUT]
19
+
20
+ if content_length = env['CONTENT_LENGTH']
21
+ content_length = content_length.to_i
22
+ end
43
23
 
44
- def extract_multipart(req, params = Rack::Utils.default_query_parser)
45
- io = req.get_header(RACK_INPUT)
46
- io.rewind
47
- content_length = req.content_length
48
- content_length = content_length.to_i if content_length
24
+ content_type = env['CONTENT_TYPE']
49
25
 
50
- tempfile = req.get_header(RACK_MULTIPART_TEMPFILE_FACTORY) || Parser::TEMPFILE_FACTORY
51
- bufsize = req.get_header(RACK_MULTIPART_BUFFER_SIZE) || Parser::BUFSIZE
26
+ tempfile = env[RACK_MULTIPART_TEMPFILE_FACTORY] || Parser::TEMPFILE_FACTORY
27
+ bufsize = env[RACK_MULTIPART_BUFFER_SIZE] || Parser::BUFSIZE
52
28
 
53
- info = Parser.parse io, content_length, req.get_header('CONTENT_TYPE'), tempfile, bufsize, params
54
- req.set_header(RACK_TEMPFILES, info.tmp_files)
55
- info.params
29
+ info = Parser.parse(io, content_length, content_type, tempfile, bufsize, params)
30
+ env[RACK_TEMPFILES] = info.tmp_files
31
+
32
+ return info.params
33
+ end
34
+
35
+ def extract_multipart(request, params = Rack::Utils.default_query_parser)
36
+ parse_multipart(request.env)
56
37
  end
57
38
 
58
39
  def build_multipart(params, first = true)
59
40
  Generator.new(params, first).dump
60
41
  end
61
42
  end
62
-
63
43
  end
64
44
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'constants'
4
+
3
5
  module Rack
4
6
  class NullLogger
5
7
  def initialize(app)
@@ -22,6 +24,11 @@ module Rack
22
24
  def warn? ; end
23
25
  def error? ; end
24
26
  def fatal? ; end
27
+ def debug! ; end
28
+ def error! ; end
29
+ def fatal! ; end
30
+ def info! ; end
31
+ def warn! ; end
25
32
  def level ; end
26
33
  def progname ; end
27
34
  def datetime_format ; end
@@ -34,6 +41,8 @@ module Rack
34
41
  def sev_threshold=(sev_threshold); end
35
42
  def close ; end
36
43
  def add(severity, message = nil, progname = nil, &block); end
44
+ def log(severity, message = nil, progname = nil, &block); end
37
45
  def <<(msg); end
46
+ def reopen(logdev = nil); end
38
47
  end
39
48
  end
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'uri'
4
+
3
5
  module Rack
4
6
  class QueryParser
5
- (require_relative 'core_ext/regexp'; using ::Rack::RegexpExtensions) if RUBY_VERSION < '2.4'
6
-
7
- DEFAULT_SEP = /[&;] */n
7
+ DEFAULT_SEP = /[&] */n
8
8
  COMMON_SEP = { ";" => /[;] */n, ";," => /[;,] */n, "&" => /[&] */n }
9
9
 
10
10
  # ParameterTypeError is the error that is raised when incoming structural
@@ -20,29 +20,35 @@ module Rack
20
20
  # nested over the specified limit.
21
21
  class ParamsTooDeepError < RangeError; end
22
22
 
23
- def self.make_default(key_space_limit, param_depth_limit)
24
- new Params, key_space_limit, param_depth_limit
23
+ def self.make_default(_key_space_limit=(not_deprecated = true; nil), param_depth_limit)
24
+ unless not_deprecated
25
+ 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)
26
+ end
27
+
28
+ new Params, param_depth_limit
25
29
  end
26
30
 
27
- attr_reader :key_space_limit, :param_depth_limit
31
+ attr_reader :param_depth_limit
32
+
33
+ def initialize(params_class, _key_space_limit=(not_deprecated = true; nil), param_depth_limit)
34
+ unless not_deprecated
35
+ 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)
36
+ end
28
37
 
29
- def initialize(params_class, key_space_limit, param_depth_limit)
30
38
  @params_class = params_class
31
- @key_space_limit = key_space_limit
32
39
  @param_depth_limit = param_depth_limit
33
40
  end
34
41
 
35
42
  # Stolen from Mongrel, with some small modifications:
36
- # Parses a query string by breaking it up at the '&'
37
- # and ';' characters. You can also use this to parse
38
- # cookies by changing the characters used in the second
39
- # parameter (which defaults to '&;').
40
- def parse_query(qs, d = nil, &unescaper)
43
+ # Parses a query string by breaking it up at the '&'. You can also use this
44
+ # to parse cookies by changing the characters used in the second parameter
45
+ # (which defaults to '&').
46
+ def parse_query(qs, separator = nil, &unescaper)
41
47
  unescaper ||= method(:unescape)
42
48
 
43
49
  params = make_params
44
50
 
45
- (qs || '').split(d ? (COMMON_SEP[d] || /[#{d}] */n) : DEFAULT_SEP).each do |p|
51
+ (qs || '').split(separator ? (COMMON_SEP[separator] || /[#{separator}] */n) : DEFAULT_SEP).each do |p|
46
52
  next if p.empty?
47
53
  k, v = p.split('=', 2).map!(&unescaper)
48
54
 
@@ -65,14 +71,14 @@ module Rack
65
71
  # query strings with parameters of conflicting types, in this case a
66
72
  # ParameterTypeError is raised. Users are encouraged to return a 400 in this
67
73
  # case.
68
- def parse_nested_query(qs, d = nil)
74
+ def parse_nested_query(qs, separator = nil)
69
75
  params = make_params
70
76
 
71
77
  unless qs.nil? || qs.empty?
72
- (qs || '').split(d ? (COMMON_SEP[d] || /[#{d}] */n) : DEFAULT_SEP).each do |p|
78
+ (qs || '').split(separator ? (COMMON_SEP[separator] || /[#{separator}] */n) : DEFAULT_SEP).each do |p|
73
79
  k, v = p.split('=', 2).map! { |s| unescape(s) }
74
80
 
75
- normalize_params(params, k, v, param_depth_limit)
81
+ _normalize_params(params, k, v, 0)
76
82
  end
77
83
  end
78
84
 
@@ -83,58 +89,87 @@ module Rack
83
89
 
84
90
  # normalize_params recursively expands parameters into structural types. If
85
91
  # the structural types represented by two different parameter names are in
86
- # conflict, a ParameterTypeError is raised.
87
- def normalize_params(params, name, v, depth)
88
- raise ParamsTooDeepError if depth <= 0
89
-
90
- name =~ %r(\A[\[\]]*([^\[\]]+)\]*)
91
- k = $1 || ''
92
- after = $' || ''
92
+ # conflict, a ParameterTypeError is raised. The depth argument is deprecated
93
+ # and should no longer be used, it is kept for backwards compatibility with
94
+ # earlier versions of rack.
95
+ def normalize_params(params, name, v, _depth=nil)
96
+ _normalize_params(params, name, v, 0)
97
+ end
93
98
 
94
- if k.empty?
95
- if !v.nil? && name == "[]"
96
- return Array(v)
99
+ private def _normalize_params(params, name, v, depth)
100
+ raise ParamsTooDeepError if depth >= param_depth_limit
101
+
102
+ if !name
103
+ # nil name, treat same as empty string (required by tests)
104
+ k = after = ''
105
+ elsif depth == 0
106
+ # Start of parsing, don't treat [] or [ at start of string specially
107
+ if start = name.index('[', 1)
108
+ # Start of parameter nesting, use part before brackets as key
109
+ k = name[0, start]
110
+ after = name[start, name.length]
97
111
  else
98
- return
112
+ # Plain parameter with no nesting
113
+ k = name
114
+ after = ''
99
115
  end
116
+ elsif name.start_with?('[]')
117
+ # Array nesting
118
+ k = '[]'
119
+ after = name[2, name.length]
120
+ elsif name.start_with?('[') && (start = name.index(']', 1))
121
+ # Hash nesting, use the part inside brackets as the key
122
+ k = name[1, start-1]
123
+ after = name[start+1, name.length]
124
+ else
125
+ # Probably malformed input, nested but not starting with [
126
+ # treat full name as key for backwards compatibility.
127
+ k = name
128
+ after = ''
100
129
  end
101
130
 
131
+ return if k.empty?
132
+
102
133
  if after == ''
103
- params[k] = v
134
+ if k == '[]' && depth != 0
135
+ return [v]
136
+ else
137
+ params[k] = v
138
+ end
104
139
  elsif after == "["
105
140
  params[name] = v
106
141
  elsif after == "[]"
107
142
  params[k] ||= []
108
143
  raise ParameterTypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)
109
144
  params[k] << v
110
- elsif after =~ %r(^\[\]\[([^\[\]]+)\]$) || after =~ %r(^\[\](.+)$)
111
- child_key = $1
145
+ elsif after.start_with?('[]')
146
+ # Recognize x[][y] (hash inside array) parameters
147
+ unless after[2] == '[' && after.end_with?(']') && (child_key = after[3, after.length-4]) && !child_key.empty? && !child_key.index('[') && !child_key.index(']')
148
+ # Handle other nested array parameters
149
+ child_key = after[2, after.length]
150
+ end
112
151
  params[k] ||= []
113
152
  raise ParameterTypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)
114
153
  if params_hash_type?(params[k].last) && !params_hash_has_key?(params[k].last, child_key)
115
- normalize_params(params[k].last, child_key, v, depth - 1)
154
+ _normalize_params(params[k].last, child_key, v, depth + 1)
116
155
  else
117
- params[k] << normalize_params(make_params, child_key, v, depth - 1)
156
+ params[k] << _normalize_params(make_params, child_key, v, depth + 1)
118
157
  end
119
158
  else
120
159
  params[k] ||= make_params
121
160
  raise ParameterTypeError, "expected Hash (got #{params[k].class.name}) for param `#{k}'" unless params_hash_type?(params[k])
122
- params[k] = normalize_params(params[k], after, v, depth - 1)
161
+ params[k] = _normalize_params(params[k], after, v, depth + 1)
123
162
  end
124
163
 
125
164
  params
126
165
  end
127
166
 
128
167
  def make_params
129
- @params_class.new @key_space_limit
130
- end
131
-
132
- def new_space_limit(key_space_limit)
133
- self.class.new @params_class, key_space_limit, param_depth_limit
168
+ @params_class.new
134
169
  end
135
170
 
136
171
  def new_depth_limit(param_depth_limit)
137
- self.class.new @params_class, key_space_limit, param_depth_limit
172
+ self.class.new @params_class, param_depth_limit
138
173
  end
139
174
 
140
175
  private
@@ -155,13 +190,12 @@ module Rack
155
190
  true
156
191
  end
157
192
 
158
- def unescape(s)
159
- Utils.unescape(s)
193
+ def unescape(string, encoding = Encoding::UTF_8)
194
+ URI.decode_www_form_component(string, encoding)
160
195
  end
161
196
 
162
197
  class Params
163
- def initialize(limit)
164
- @limit = limit
198
+ def initialize
165
199
  @size = 0
166
200
  @params = {}
167
201
  end
@@ -171,8 +205,6 @@ module Rack
171
205
  end
172
206
 
173
207
  def []=(key, value)
174
- @size += key.size if key && !@params.key?(key)
175
- raise ParamsTooDeepError, 'exceeded available parameter key space' if @size > @limit
176
208
  @params[key] = value
177
209
  end
178
210
 
@@ -2,6 +2,8 @@
2
2
 
3
3
  require 'uri'
4
4
 
5
+ require_relative 'constants'
6
+
5
7
  module Rack
6
8
  # Rack::ForwardRequest gets caught by Rack::Recursive and redirects
7
9
  # the current request to the app at +url+.