rack 1.4.7 → 1.5.0.beta.1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


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

Files changed (60) hide show
  1. data/README.rdoc +2 -30
  2. data/Rakefile +1 -0
  3. data/SPEC +68 -4
  4. data/example/protectedlobster.rb +1 -1
  5. data/lib/rack.rb +2 -14
  6. data/lib/rack/auth/abstract/request.rb +1 -5
  7. data/lib/rack/builder.rb +8 -4
  8. data/lib/rack/cascade.rb +2 -2
  9. data/lib/rack/config.rb +5 -0
  10. data/lib/rack/deflater.rb +2 -1
  11. data/lib/rack/file.rb +25 -28
  12. data/lib/rack/handler.rb +18 -5
  13. data/lib/rack/handler/mongrel.rb +1 -1
  14. data/lib/rack/handler/scgi.rb +1 -1
  15. data/lib/rack/handler/thin.rb +6 -3
  16. data/lib/rack/handler/webrick.rb +1 -0
  17. data/lib/rack/head.rb +2 -0
  18. data/lib/rack/lint.rb +132 -7
  19. data/lib/rack/lobster.rb +3 -3
  20. data/lib/rack/lock.rb +2 -0
  21. data/lib/rack/methodoverride.rb +0 -2
  22. data/lib/rack/mime.rb +29 -0
  23. data/lib/rack/multipart/parser.rb +0 -9
  24. data/lib/rack/request.rb +66 -25
  25. data/lib/rack/response.rb +1 -2
  26. data/lib/rack/sendfile.rb +18 -4
  27. data/lib/rack/server.rb +20 -12
  28. data/lib/rack/session/abstract/id.rb +60 -59
  29. data/lib/rack/session/cookie.rb +11 -16
  30. data/lib/rack/utils.rb +97 -85
  31. data/rack.gemspec +1 -6
  32. data/test/spec_builder.rb +7 -0
  33. data/test/spec_cgi.rb +1 -1
  34. data/test/spec_chunked.rb +3 -5
  35. data/test/spec_content_length.rb +3 -6
  36. data/test/spec_deflater.rb +26 -9
  37. data/test/spec_fastcgi.rb +1 -1
  38. data/test/spec_file.rb +24 -11
  39. data/test/spec_head.rb +3 -8
  40. data/test/spec_lint.rb +6 -6
  41. data/test/spec_lock.rb +4 -7
  42. data/test/spec_methodoverride.rb +4 -1
  43. data/test/spec_mime.rb +51 -0
  44. data/test/spec_mongrel.rb +1 -1
  45. data/test/spec_multipart.rb +15 -49
  46. data/test/spec_nulllogger.rb +3 -6
  47. data/test/spec_request.rb +112 -18
  48. data/test/spec_response.rb +8 -8
  49. data/test/spec_sendfile.rb +52 -13
  50. data/test/spec_server.rb +6 -0
  51. data/test/spec_session_abstract_id.rb +11 -1
  52. data/test/spec_session_cookie.rb +140 -153
  53. data/test/spec_thin.rb +6 -1
  54. data/test/spec_utils.rb +23 -17
  55. data/test/spec_webrick.rb +1 -1
  56. metadata +37 -83
  57. checksums.yaml +0 -7
  58. data/test/cgi/lighttpd.errors +0 -1
  59. data/test/multipart/three_files_three_fields +0 -31
  60. data/test/spec_auth.rb +0 -57
@@ -6,9 +6,12 @@ module Rack
6
6
  module Handler
7
7
  class Thin
8
8
  def self.run(app, options={})
9
- server = ::Thin::Server.new(options[:Host] || '0.0.0.0',
10
- options[:Port] || 8080,
11
- app)
9
+ host = options.delete(:Host) || '0.0.0.0'
10
+ port = options.delete(:Port) || 8080
11
+ args = [host, port, app, options]
12
+ # Thin versions below 0.8.0 do not support additional options
13
+ args.pop if ::Thin::VERSION::MAJOR < 1 && ::Thin::VERSION::MINOR < 8
14
+ server = ::Thin::Server.new(*args)
12
15
  yield server if block_given?
13
16
  server.start
14
17
  end
@@ -7,6 +7,7 @@ module Rack
7
7
  class WEBrick < ::WEBrick::HTTPServlet::AbstractServlet
8
8
  def self.run(app, options={})
9
9
  options[:BindAddress] = options.delete(:Host) if options[:Host]
10
+ options[:Port] ||= 8080
10
11
  @server = ::WEBrick::HTTPServer.new(options)
11
12
  @server.mount "/", Rack::Handler::WEBrick, app
12
13
  yield @server if block_given?
@@ -1,6 +1,8 @@
1
1
  module Rack
2
2
 
3
3
  class Head
4
+ # Rack::Head returns an empty body for all HEAD requests. It leaves
5
+ # all other requests unchanged.
4
6
  def initialize(app)
5
7
  @app = app
6
8
  end
@@ -1,4 +1,5 @@
1
1
  require 'rack/utils'
2
+ require 'forwardable'
2
3
 
3
4
  module Rack
4
5
  # Rack::Lint validates your application and the requests and
@@ -50,6 +51,9 @@ module Rack
50
51
  check_status status
51
52
  ## the *headers*,
52
53
  check_headers headers
54
+
55
+ check_hijack_response headers, env
56
+
53
57
  ## and the *body*.
54
58
  check_content_type status, headers
55
59
  check_content_length status, headers
@@ -108,7 +112,8 @@ module Rack
108
112
  ## variables should correspond with
109
113
  ## the presence or absence of the
110
114
  ## appropriate HTTP header in the
111
- ## request.
115
+ ## request. See <a href="https://tools.ietf.org/html/rfc3875#section-4.1.18">
116
+ ## RFC3875 section 4.1.18</a> for specific behavior.
112
117
 
113
118
  ## In addition to this, the Rack environment must include these
114
119
  ## Rack-specific variables:
@@ -120,6 +125,9 @@ module Rack
120
125
  ## <tt>rack.multithread</tt>:: true if the application object may be simultaneously invoked by another thread in the same process, false otherwise.
121
126
  ## <tt>rack.multiprocess</tt>:: true if an equivalent application object may be simultaneously invoked by another process, false otherwise.
122
127
  ## <tt>rack.run_once</tt>:: true if the server expects (but does not guarantee!) that the application will only be invoked this one time during the life of its containing process. Normally, this will only be true for a server based on CGI (or something similar).
128
+ ## <tt>rack.hijack?</tt>:: present and true if the server supports connection hijacking. See below, hijacking.
129
+ ## <tt>rack.hijack</tt>:: an object responding to #call that must be called at least once before using rack.hijack_io. It is recommended #call return rack.hijack_io as well as setting it in env if necessary.
130
+ ## <tt>rack.hijack_io</tt>:: if rack.hijack? is true, and rack.hijack has received #call, this will contain an object resembling an IO. See hijacking.
123
131
  ##
124
132
 
125
133
  ## Additional environment specifications have approved to
@@ -226,6 +234,8 @@ module Rack
226
234
  check_input env["rack.input"]
227
235
  ## * There must be a valid error stream in <tt>rack.errors</tt>.
228
236
  check_error env["rack.errors"]
237
+ ## * There may be a valid hijack stream in <tt>rack.hijack_io</tt>
238
+ check_hijack env
229
239
 
230
240
  ## * The <tt>REQUEST_METHOD</tt> must be a valid token.
231
241
  assert("REQUEST_METHOD unknown: #{env["REQUEST_METHOD"]}") {
@@ -416,6 +426,121 @@ module Rack
416
426
  end
417
427
  end
418
428
 
429
+ class HijackWrapper
430
+ include Assertion
431
+ extend Forwardable
432
+
433
+ REQUIRED_METHODS = [
434
+ :read, :write, :read_nonblock, :write_nonblock, :flush, :close,
435
+ :close_read, :close_write, :closed?
436
+ ]
437
+
438
+ def_delegators :@io, *REQUIRED_METHODS
439
+
440
+ def initialize(io)
441
+ @io = io
442
+ REQUIRED_METHODS.each do |meth|
443
+ assert("rack.hijack_io must respond to #{meth}") { io.respond_to? meth }
444
+ end
445
+ end
446
+ end
447
+
448
+ ## === Hijacking
449
+ #
450
+ # AUTHORS: n.b. The trailing whitespace between paragraphs is important and
451
+ # should not be removed. The whitespace creates paragraphs in the RDoc
452
+ # output.
453
+ #
454
+ ## ==== Request (before status)
455
+ def check_hijack(env)
456
+ if env['rack.hijack?']
457
+ ## If rack.hijack? is true then rack.hijack must respond to #call.
458
+ original_hijack = env['rack.hijack']
459
+ assert("rack.hijack must respond to call") { original_hijack.respond_to?(:call) }
460
+ env['rack.hijack'] = proc do
461
+ ## rack.hijack must return the io that will also be assigned (or is
462
+ ## already present, in rack.hijack_io.
463
+ io = original_hijack.call
464
+ HijackWrapper.new(io)
465
+ ##
466
+ ## rack.hijack_io must respond to:
467
+ ## <tt>read, write, read_nonblock, write_nonblock, flush, close,
468
+ ## close_read, close_write, closed?</tt>
469
+ ##
470
+ ## The semantics of these IO methods must be a best effort match to
471
+ ## those of a normal ruby IO or Socket object, using standard
472
+ ## arguments and raising standard exceptions. Servers are encouraged
473
+ ## to simply pass on real IO objects, although it is recognized that
474
+ ## this approach is not directly compatible with SPDY and HTTP 2.0.
475
+ ##
476
+ ## IO provided in rack.hijack_io should preference the
477
+ ## IO::WaitReadable and IO::WaitWritable APIs wherever supported.
478
+ ##
479
+ ## There is a deliberate lack of full specification around
480
+ ## rack.hijack_io, as semantics will change from server to server.
481
+ ## Users are encouraged to utilize this API with a knowledge of their
482
+ ## server choice, and servers may extend the functionality of
483
+ ## hijack_io to provide additional features to users. The purpose of
484
+ ## rack.hijack is for Rack to "get out of the way", as such, Rack only
485
+ ## provides the minimum of specification and support.
486
+ env['rack.hijack_io'] = HijackWrapper.new(env['rack.hijack_io'])
487
+ io
488
+ end
489
+ else
490
+ ##
491
+ ## If rack.hijack? is false, then rack.hijack should not be set.
492
+ assert("rack.hijack? is false, but rack.hijack is present") { env['rack.hijack'].nil? }
493
+ ##
494
+ ## If rack.hijack? is false, then rack.hijack_io should not be set.
495
+ assert("rack.hijack? is false, but rack.hijack_io is present") { env['rack.hijack_io'].nil? }
496
+ end
497
+ end
498
+
499
+ ## ==== Response (after headers)
500
+ ## It is also possible to hijack a response after the status and headers
501
+ ## have been sent.
502
+ def check_hijack_response(headers, env)
503
+ ## In order to do this, an application may set the special header
504
+ ## <tt>rack.hijack</tt> to an object that responds to <tt>call</tt>
505
+ ## accepting an argument that conforms to the <tt>rack.hijack_io</tt>
506
+ ## protocol.
507
+ ##
508
+ ## After the headers have been sent, and this hijack callback has been
509
+ ## called, the application is now responsible for the remaining lifecycle
510
+ ## of the IO. The application is also responsible for maintaining HTTP
511
+ ## semantics. Of specific note, in almost all cases in the current SPEC,
512
+ ## applications will have wanted to specify the header Connection:close in
513
+ ## HTTP/1.1, and not Connection:keep-alive, as there is no protocol for
514
+ ## returning hijacked sockets to the web server. For that purpose, use the
515
+ ## body streaming API instead (progressively yielding strings via each).
516
+ ##
517
+ ## Servers must ignore the <tt>body</tt> part of the response tuple when
518
+ ## the <tt>rack.hijack</tt> response API is in use.
519
+
520
+ if env['rack.hijack?'] && headers['rack.hijack']
521
+ assert('rack.hijack header must respond to #call') {
522
+ headers['rack.hijack'].respond_to? :call
523
+ }
524
+ original_hijack = headers['rack.hijack']
525
+ headers['rack.hijack'] = proc do |io|
526
+ original_hijack.call HijackWrapper.new(io)
527
+ end
528
+ else
529
+ ##
530
+ ## The special response header <tt>rack.hijack</tt> must only be set
531
+ ## if the request env has <tt>rack.hijack?</tt> <tt>true</tt>.
532
+ assert('rack.hijack header must not be present if server does not support hijacking') {
533
+ headers['rack.hijack'].nil?
534
+ }
535
+ end
536
+ end
537
+ ## ==== Conventions
538
+ ## * Middleware should not use hijack unless it is handling the whole
539
+ ## response.
540
+ ## * Middleware may wrap the IO object for the response pattern.
541
+ ## * Middleware should not wrap the IO object for the request pattern. The
542
+ ## request pattern is intended to provide the hijacker with "raw tcp".
543
+
419
544
  ## == The Response
420
545
 
421
546
  ## === The Status
@@ -432,6 +557,10 @@ module Rack
432
557
  header.respond_to? :each
433
558
  }
434
559
  header.each { |key, value|
560
+ ## Special headers starting "rack." are for communicating with the
561
+ ## server, and must not be sent back to the client.
562
+ next if key =~ /^rack\..+$/
563
+
435
564
  ## The header keys must be Strings.
436
565
  assert("header key must be a string, was #{key.class}") {
437
566
  key.kind_of? String
@@ -463,9 +592,8 @@ module Rack
463
592
  ## === The Content-Type
464
593
  def check_content_type(status, headers)
465
594
  headers.each { |key, value|
466
- ## There must be a <tt>Content-Type</tt>, except when the
467
- ## +Status+ is 1xx, 204, 205 or 304, in which case there must be none
468
- ## given.
595
+ ## There must not be a <tt>Content-Type</tt>, when the +Status+ is 1xx,
596
+ ## 204, 205 or 304.
469
597
  if key.downcase == "content-type"
470
598
  assert("Content-Type header found in #{status} response, not allowed") {
471
599
  not Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include? status.to_i
@@ -473,9 +601,6 @@ module Rack
473
601
  return
474
602
  end
475
603
  }
476
- assert("No Content-Type header found") {
477
- Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include? status.to_i
478
- }
479
604
  end
480
605
 
481
606
  ## === The Content-Length
@@ -59,7 +59,7 @@ end
59
59
  if $0 == __FILE__
60
60
  require 'rack'
61
61
  require 'rack/showexceptions'
62
- Rack::Handler::WEBrick.run \
63
- Rack::ShowExceptions.new(Rack::Lint.new(Rack::Lobster.new)),
64
- :Port => 9292
62
+ Rack::Server.start(
63
+ :app => Rack::ShowExceptions.new(Rack::Lint.new(Rack::Lobster.new)), :Port => 9292
64
+ )
65
65
  end
@@ -2,6 +2,8 @@ require 'thread'
2
2
  require 'rack/body_proxy'
3
3
 
4
4
  module Rack
5
+ # Rack::Lock locks every request inside a mutex, so that every request
6
+ # will effectively be executed synchronously.
5
7
  class Lock
6
8
  FLAG = 'rack.multithread'.freeze
7
9
 
@@ -26,8 +26,6 @@ module Rack
26
26
  method = req.POST[METHOD_OVERRIDE_PARAM_KEY] ||
27
27
  env[HTTP_METHOD_OVERRIDE_HEADER]
28
28
  method.to_s.upcase
29
- rescue EOFError
30
- ""
31
29
  end
32
30
  end
33
31
  end
@@ -18,6 +18,35 @@ module Rack
18
18
  end
19
19
  module_function :mime_type
20
20
 
21
+ # Returns true if the given value is a mime match for the given mime match
22
+ # specification, false otherwise.
23
+ #
24
+ # Rack::Mime.match?('text/html', 'text/*') => true
25
+ # Rack::Mime.match?('text/plain', '*') => true
26
+ # Rack::Mime.match?('text/html', 'application/json') => false
27
+
28
+ def match?(value, matcher)
29
+ v1, v2 = value.split('/', 2)
30
+ m1, m2 = matcher.split('/', 2)
31
+
32
+ if m1 == '*'
33
+ if m2.nil? || m2 == '*'
34
+ return true
35
+ elsif m2 == v2
36
+ return true
37
+ else
38
+ return false
39
+ end
40
+ end
41
+
42
+ return false if v1 != m1
43
+
44
+ return true if m2.nil? || m2 == '*'
45
+
46
+ m2 == v2
47
+ end
48
+ module_function :match?
49
+
21
50
  # List of most common mime-types, selected various sources
22
51
  # according to their usefulness in a webserving scope for Ruby
23
52
  # users.
@@ -2,8 +2,6 @@ require 'rack/utils'
2
2
 
3
3
  module Rack
4
4
  module Multipart
5
- class MultipartLimitError < Errno::EMFILE; end
6
-
7
5
  class Parser
8
6
  BUFSIZE = 16384
9
7
 
@@ -16,17 +14,10 @@ module Rack
16
14
 
17
15
  fast_forward_to_first_boundary
18
16
 
19
- opened_files = 0
20
17
  loop do
21
-
22
18
  head, filename, content_type, name, body =
23
19
  get_current_head_and_filename_and_content_type_and_name_and_body
24
20
 
25
- if Utils.multipart_part_limit > 0
26
- opened_files += 1 if filename
27
- raise MultipartLimitError, 'Maximum file multiparts in content reached' if opened_files >= Utils.multipart_part_limit
28
- end
29
-
30
21
  # Save the rest.
31
22
  if i = @buf.index(rx)
32
23
  body << @buf.slice!(0, i)
@@ -98,10 +98,8 @@ module Rack
98
98
  port.to_i
99
99
  elsif port = @env['HTTP_X_FORWARDED_PORT']
100
100
  port.to_i
101
- elsif ssl?
102
- 443
103
101
  elsif @env.has_key?("HTTP_X_FORWARDED_HOST")
104
- 80
102
+ DEFAULT_PORTS[scheme]
105
103
  else
106
104
  @env["SERVER_PORT"].to_i
107
105
  end
@@ -118,25 +116,25 @@ module Rack
118
116
 
119
117
  # Checks the HTTP request method (or verb) to see if it was of type DELETE
120
118
  def delete?; request_method == "DELETE" end
121
-
119
+
122
120
  # Checks the HTTP request method (or verb) to see if it was of type GET
123
121
  def get?; request_method == "GET" end
124
-
122
+
125
123
  # Checks the HTTP request method (or verb) to see if it was of type HEAD
126
124
  def head?; request_method == "HEAD" end
127
-
125
+
128
126
  # Checks the HTTP request method (or verb) to see if it was of type OPTIONS
129
127
  def options?; request_method == "OPTIONS" end
130
-
128
+
131
129
  # Checks the HTTP request method (or verb) to see if it was of type PATCH
132
130
  def patch?; request_method == "PATCH" end
133
-
131
+
134
132
  # Checks the HTTP request method (or verb) to see if it was of type POST
135
133
  def post?; request_method == "POST" end
136
-
134
+
137
135
  # Checks the HTTP request method (or verb) to see if it was of type PUT
138
136
  def put?; request_method == "PUT" end
139
-
137
+
140
138
  # Checks the HTTP request method (or verb) to see if it was of type TRACE
141
139
  def trace?; request_method == "TRACE" end
142
140
 
@@ -157,6 +155,10 @@ module Rack
157
155
  'multipart/mixed'
158
156
  ]
159
157
 
158
+ # Default ports depending on scheme. Used to decide whether or not
159
+ # to include the port in a generated URI.
160
+ DEFAULT_PORTS = { 'http' => 80, 'https' => 443, 'coffee' => 80 }
161
+
160
162
  # Determine whether the request body contains form-data by checking
161
163
  # the request Content-Type for one of the media-types:
162
164
  # "application/x-www-form-urlencoded" or "multipart/form-data". The
@@ -217,10 +219,45 @@ module Rack
217
219
  end
218
220
 
219
221
  # The union of GET and POST data.
222
+ #
223
+ # Note that modifications will not be persisted in the env. Use update_param or delete_param if you want to destructively modify params.
220
224
  def params
221
225
  @params ||= self.GET.merge(self.POST)
222
226
  rescue EOFError
223
- self.GET
227
+ self.GET.dup
228
+ end
229
+
230
+ # Destructively update a parameter, whether it's in GET and/or POST. Returns nil.
231
+ #
232
+ # The parameter is updated wherever it was previous defined, so GET, POST, or both. If it wasn't previously defined, it's inserted into GET.
233
+ #
234
+ # env['rack.input'] is not touched.
235
+ def update_param(k, v)
236
+ found = false
237
+ if self.GET.has_key?(k)
238
+ found = true
239
+ self.GET[k] = v
240
+ end
241
+ if self.POST.has_key?(k)
242
+ found = true
243
+ self.POST[k] = v
244
+ end
245
+ unless found
246
+ self.GET[k] = v
247
+ end
248
+ @params = nil
249
+ nil
250
+ end
251
+
252
+ # Destructively delete a parameter, whether it's in GET or POST. Returns the value of the deleted parameter.
253
+ #
254
+ # If the parameter is in both GET and POST, the POST value takes precedence since that's how #params works.
255
+ #
256
+ # env['rack.input'] is not touched.
257
+ def delete_param(k)
258
+ v = [ self.POST.delete(k), self.GET.delete(k) ].compact.first
259
+ @params = nil
260
+ v
224
261
  end
225
262
 
226
263
  # shortcut for request.params[key]
@@ -229,6 +266,8 @@ module Rack
229
266
  end
230
267
 
231
268
  # shortcut for request.params[key] = value
269
+ #
270
+ # Note that modifications will not be persisted in the env. Use update_param or delete_param if you want to destructively modify params.
232
271
  def []=(key, value)
233
272
  params[key.to_s] = value
234
273
  end
@@ -271,14 +310,8 @@ module Rack
271
310
  end
272
311
 
273
312
  def base_url
274
- url = scheme + "://"
275
- url << host
276
-
277
- if scheme == "https" && port != 443 ||
278
- scheme == "http" && port != 80
279
- url << ":#{port}"
280
- end
281
-
313
+ url = "#{scheme}://#{host}"
314
+ url << ":#{port}" if port != DEFAULT_PORTS[scheme]
282
315
  url
283
316
  end
284
317
 
@@ -307,16 +340,16 @@ module Rack
307
340
  end
308
341
 
309
342
  def trusted_proxy?(ip)
310
- ip =~ /^127\.0\.0\.1$|^(10|172\.(1[6-9]|2[0-9]|30|31)|192\.168)\.|^::1$|^fd[0-9a-f]{2}:.+|^localhost$/i
343
+ ip =~ /^127\.0\.0\.1$|^(10|172\.(1[6-9]|2[0-9]|30|31)|192\.168)\.|^::1$|^fd[0-9a-f]{2}:.+|^localhost$|^unix$|^unix:/i
311
344
  end
312
345
 
313
346
  def ip
314
- remote_addrs = @env['REMOTE_ADDR'] ? @env['REMOTE_ADDR'].split(/[,\s]+/) : []
315
- remote_addrs.reject! { |addr| trusted_proxy?(addr) }
316
-
347
+ remote_addrs = split_ip_addresses(@env['REMOTE_ADDR'])
348
+ remote_addrs = reject_trusted_ip_addresses(remote_addrs)
349
+
317
350
  return remote_addrs.first if remote_addrs.any?
318
351
 
319
- forwarded_ips = @env['HTTP_X_FORWARDED_FOR'] ? @env['HTTP_X_FORWARDED_FOR'].strip.split(/[,\s]+/) : []
352
+ forwarded_ips = split_ip_addresses(@env['HTTP_X_FORWARDED_FOR'])
320
353
 
321
354
  if client_ip = @env['HTTP_CLIENT_IP']
322
355
  # If forwarded_ips doesn't include the client_ip, it might be an
@@ -324,10 +357,18 @@ module Rack
324
357
  return client_ip if forwarded_ips.include?(client_ip)
325
358
  end
326
359
 
327
- return forwarded_ips.reject { |ip| trusted_proxy?(ip) }.last || @env["REMOTE_ADDR"]
360
+ return reject_trusted_ip_addresses(forwarded_ips).last || @env["REMOTE_ADDR"]
328
361
  end
329
362
 
330
363
  protected
364
+ def split_ip_addresses(ip_addresses)
365
+ ip_addresses ? ip_addresses.strip.split(/[,\s]+/) : []
366
+ end
367
+
368
+ def reject_trusted_ip_addresses(ip_addresses)
369
+ ip_addresses.reject { |ip| trusted_proxy?(ip) }
370
+ end
371
+
331
372
  def parse_query(qs)
332
373
  Utils.parse_nested_query(qs)
333
374
  end