dnsruby 1.59.0 → 1.59.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 30959b577be6d8f64a67832ec6418fd157ece057
4
- data.tar.gz: 94af23844c0a0f1d53b405b0720c9253364ad890
3
+ metadata.gz: 47fddae1c07436f1882cf04f2815f0f0f4e57991
4
+ data.tar.gz: 799ab6fe078f84d5ca9d651c4ca6701ed862a005
5
5
  SHA512:
6
- metadata.gz: 2237405083ea6ba078a24a172d681b8fde6f770d61fab1617cf4683f576ade793af97edadabe2e76fd649dc0d1eb868eb8bd931021b151ad64b8d0f3c99fc271
7
- data.tar.gz: b7f1a0e23ca45cbe15410bd604754c26688ca835edb39dcc340ac4aece8d76c3e67d6ed06a9c2c3488caf2d56c6a5d526fca80f1c6933b43c4e3f0f1ff7a6bce
6
+ metadata.gz: 7d83561ecdb469f15880d544c6d15b85f026700d24248a9e6c51804b85cc7a90da89be23a8d348b20dbe84519434e6f661b877881cac0bd533ea2fe9c8e7056c
7
+ data.tar.gz: 9969e39d04e70734c7ee077bfb137f7a36d1e2726a1ab0719cb31f322c34365a774efd7dd0edc2e11bd8418fa0d2a60250384e23bca35d0aca84a35c7011c467
@@ -1,5 +1,11 @@
1
1
  # Release Notes
2
2
 
3
+ ## v1.59.1
4
+
5
+ * Support for HMAC SHA512 TSIG keys
6
+ * Fix TCP pipelining tests
7
+ * IDN encoding error returned as Dnsruby::OtherResolvError
8
+
3
9
  ## v1.59.0
4
10
 
5
11
  * Add LICENSE file
data/Rakefile CHANGED
@@ -31,3 +31,4 @@ create_task(:test_online, 'test/ts_online.rb')
31
31
  create_task(:soak, 'test/tc_soak.rb')
32
32
  create_task(:message, 'test/tc_message.rb')
33
33
  create_task(:cache, 'test/tc_cache.rb')
34
+ create_task(:pipe, 'test/tc_tcp_pipelining.rb')
@@ -35,6 +35,7 @@ DNSSEC NSEC3 support.'
35
35
  s.add_development_dependency 'minitest', '~> 5.4'
36
36
  s.add_development_dependency 'rubydns', '~> 1.0'
37
37
  s.add_development_dependency 'nio4r', '~> 1.1'
38
+ s.add_development_dependency 'minitest-display', '>= 0.3.0'
38
39
 
39
40
  if RUBY_VERSION >= "1.9.3"
40
41
  s.add_development_dependency 'coveralls', '~> 0.7'
@@ -15,7 +15,11 @@ class MessageEncoder #:nodoc: all
15
15
  end
16
16
 
17
17
  def put_pack(template, *d)
18
- @data << d.pack(template)
18
+ begin
19
+ @data << d.pack(template)
20
+ rescue Encoding::CompatibilityError => e
21
+ raise Dnsruby::OtherResolvError.new("IDN support currently requires punycode string")
22
+ end
19
23
  end
20
24
 
21
25
  def put_length16
@@ -28,8 +32,12 @@ class MessageEncoder #:nodoc: all
28
32
  end
29
33
 
30
34
  def put_string(d)
31
- self.put_pack("C", d.length)
32
- @data << d
35
+ begin
36
+ self.put_pack("C", d.length)
37
+ @data << d
38
+ rescue Encoding::CompatibilityError => e
39
+ raise Dnsruby::OtherResolvError.new("IDN support currently requires punycode string")
40
+ end
33
41
  end
34
42
 
35
43
  def put_string_list(ds)
@@ -392,7 +392,7 @@ module Dnsruby
392
392
  end
393
393
  rescue Errno::EISCONN
394
394
  #already connected, do nothing and reuse!
395
- rescue IOError #close by remote host, reconnect
395
+ rescue IOError, Errno::ECONNRESET #close by remote host, reconnect
396
396
  @pipeline_socket = nil
397
397
  Dnsruby.log.debug("Connection closed - recreating socket")
398
398
  end
@@ -401,21 +401,27 @@ module Dnsruby
401
401
  create_pipeline_socket = -> do
402
402
  @tcp_pipeline_local_port = src_port
403
403
  src_address = @ipv6 ? @src_address6 : @src_address
404
-
405
- @pipeline_socket = Socket.new(AF_INET, SOCK_STREAM, 0)
406
- @pipeline_socket.bind(Addrinfo.tcp(src_address, src_port))
407
- @pipeline_socket.connect(sockaddr)
408
- @use_counts[@pipeline_socket] = 0
404
+ begin
405
+ @pipeline_socket = Socket.new(AF_INET, SOCK_STREAM, 0)
406
+ @pipeline_socket.bind(Addrinfo.tcp(src_address, src_port))
407
+ @pipeline_socket.connect(sockaddr)
408
+ Dnsruby.log.debug("Creating socket #{src_address}:#{src_port}")
409
+ @use_counts[@pipeline_socket] = 0
410
+ rescue Exception => e
411
+ @pipeline_socket = nil
412
+ raise e
413
+ end
409
414
  end
410
415
 
411
416
  # Don't combine the following 2 statements; the reuse lambda can set the
412
417
  # socket to nil and if so we'd want to call the create lambda to recreate it.
413
418
  reuse_pipeline_socket.() if @pipeline_socket
419
+ new_socket = @pipeline_socket.nil?
414
420
  create_pipeline_socket.() unless @pipeline_socket
415
421
 
416
422
  @use_counts[@pipeline_socket] += 1
417
423
 
418
- @pipeline_socket
424
+ [@pipeline_socket, new_socket]
419
425
  end
420
426
 
421
427
  # This method sends the packet using the built-in pure Ruby event loop, with no dependencies.
@@ -437,10 +443,11 @@ module Dnsruby
437
443
  if (use_tcp)
438
444
  begin
439
445
  if (@tcp_pipelining)
440
- socket = tcp_pipeline_socket(src_port)
446
+ socket, new_socket = tcp_pipeline_socket(src_port)
441
447
  src_port = @tcp_pipeline_local_port
442
448
  else
443
449
  socket = TCPSocket.new(@server, @port, src_address, src_port)
450
+ new_socket = true
444
451
  end
445
452
  rescue Errno::EBADF, Errno::ENETUNREACH => e
446
453
  # Can't create a connection
@@ -458,6 +465,7 @@ module Dnsruby
458
465
  # ipv6 = @src_address =~ /:/
459
466
  socket = UDPSocket.new(@ipv6 ? Socket::AF_INET6 : Socket::AF_INET)
460
467
  end
468
+ new_socket = true
461
469
  socket.bind(src_address, src_port)
462
470
  socket.connect(@server, @port)
463
471
  end
@@ -465,7 +473,8 @@ module Dnsruby
465
473
  rescue Exception => e
466
474
  if (socket!=nil)
467
475
  begin
468
- socket.close
476
+ #let the select thread close the socket if tcp_pipeli
477
+ socket.close unless @tcp_pipelining && !new_socket
469
478
  rescue Exception
470
479
  end
471
480
  end
@@ -473,9 +482,15 @@ module Dnsruby
473
482
  # Maybe try a max number of times?
474
483
  if ((e.class != Errno::EADDRINUSE) || (numtries > 50) ||
475
484
  ((e.class == Errno::EADDRINUSE) && (src_port == @src_port[0])))
476
- err=IOError.new("dnsruby can't connect to #{@server}:#{@port} from #{src_address}:#{src_port}, use_tcp=#{use_tcp}, exception = #{e.class}, #{e}")
477
- Dnsruby.log.error { "#{err}" }
478
- st.push_exception_to_select(client_query_id, client_queue, err, nil)
485
+ err_msg = "dnsruby can't connect to #{@server}:#{@port} from #{src_address}:#{src_port}, use_tcp=#{use_tcp}, exception = #{e.class}, #{e} #{e.backtrace}"
486
+ err=IOError.new(err_msg)
487
+ Dnsruby.log.error( "#{err}")
488
+ Dnsruby.log.error(e.backtrace)
489
+ if @tcp_pipelining
490
+ st.push_exception_to_select(client_query_id, client_queue, SocketEofResolvError.new(err_msg), nil)
491
+ else
492
+ st.push_exception_to_select(client_query_id, client_queue, err, nil)
493
+ end
479
494
  return
480
495
  end
481
496
  end
@@ -498,15 +513,24 @@ module Dnsruby
498
513
  socket.send(lenmsg, 0)
499
514
  end
500
515
  socket.send(query_bytes, 0)
501
- # The select thread will now wait for the response and send that or a timeout
502
- # back to the client_queue.
516
+
517
+ # The select thread will now wait for the response and send that or a
518
+ # timeout back to the client_queue.
503
519
  st.add_to_select(query_settings)
504
520
  rescue Exception => e
505
- err=IOError.new("Send failed to #{@server}:#{@port} from #{src_address}:#{src_port}, use_tcp=#{use_tcp}, exception : #{e}")
521
+ err_msg = "Send failed to #{@server}:#{@port} from #{src_address}:#{src_port}, use_tcp=#{use_tcp}, exception : #{e}"
522
+ err=IOError.new(err_msg)
506
523
  Dnsruby.log.error { "#{err}" }
507
- st.push_exception_to_select(client_query_id, client_queue, err, nil)
524
+ Dnsruby.log.error(e.backtrace)
525
+ if @tcp_pipelining
526
+ st.push_exception_to_select(client_query_id, client_queue, SocketEofResolvError.new(err_msg), nil) if new_socket
527
+ else
528
+ st.push_exception_to_select(client_query_id, client_queue, err, nil)
529
+ end
508
530
  begin
509
- socket.close
531
+ #we let the select_thread close the socket when doing tcp
532
+ #pipelining
533
+ socket.close unless @tcp_pipelining && !new_socket
510
534
  rescue Exception
511
535
  end
512
536
  return
@@ -54,6 +54,7 @@ module Dnsruby
54
54
  HMAC_MD5 = Name.create("HMAC-MD5.SIG-ALG.REG.INT.")
55
55
  HMAC_SHA1 = Name.create("hmac-sha1.")
56
56
  HMAC_SHA256 = Name.create("hmac-sha256.")
57
+ HMAC_SHA512 = Name.create("hmac-sha512.")
57
58
 
58
59
  DEFAULT_FUDGE = 300
59
60
 
@@ -157,6 +158,8 @@ module Dnsruby
157
158
  mac = OpenSSL::HMAC.digest(OpenSSL::Digest::SHA1.new, key, data)
158
159
  elsif (algorithm == HMAC_SHA256)
159
160
  mac = OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new, key, data)
161
+ elsif (algorithm == HMAC_SHA512)
162
+ mac = OpenSSL::HMAC.digest(OpenSSL::Digest::SHA512.new, key, data)
160
163
  else
161
164
  # Should we allow client to pass in their own signing function?
162
165
  raise VerifyError.new("Algorithm #{algorithm} unsupported by TSIG")
@@ -515,6 +518,7 @@ module Dnsruby
515
518
  # * hmac-md5
516
519
  # * hmac-sha1
517
520
  # * hmac-sha256
521
+ # * hmac-sha512
518
522
  def algorithm=(alg)
519
523
  if (alg.class == String)
520
524
  if (alg.downcase=="hmac-md5")
@@ -523,11 +527,13 @@ module Dnsruby
523
527
  @algorithm = HMAC_SHA1;
524
528
  elsif (alg.downcase=="hmac-sha256")
525
529
  @algorithm = HMAC_SHA256;
530
+ elsif (alg.downcase=="hmac-sha512")
531
+ @algorithm = HMAC_SHA512;
526
532
  else
527
533
  raise ArgumentError.new("Invalid TSIG algorithm")
528
534
  end
529
535
  elsif (alg.class == Name)
530
- if (alg!=HMAC_MD5 && alg!=HMAC_SHA1 && alg!=HMAC_SHA256)
536
+ if (alg!=HMAC_MD5 && alg!=HMAC_SHA1 && alg!=HMAC_SHA256 && alg!=HMAC_SHA512)
531
537
  raise ArgumentException.new("Invalid TSIG algorithm")
532
538
  end
533
539
  @algorithm=alg
@@ -590,4 +596,4 @@ module Dnsruby
590
596
  end
591
597
  end
592
598
  end
593
- end
599
+ end
@@ -1,3 +1,3 @@
1
1
  module Dnsruby
2
- VERSION = '1.59.0'
2
+ VERSION = '1.59.1'
3
3
  end
@@ -14,7 +14,17 @@ end
14
14
 
15
15
  require 'minitest'
16
16
  require 'minitest/autorun'
17
+ require 'minitest/display'
17
18
 
19
+ MiniTest::Display.options = {
20
+ suite_names: true,
21
+ color: true,
22
+ print: {
23
+ success: ".",
24
+ failure: "F",
25
+ error: "R"
26
+ }
27
+ }
18
28
  # This is in a self invoking anonymous lambda so local variables do not
19
29
  # leak to the outer scope.
20
30
  -> do
@@ -67,27 +67,27 @@ class TestCache < Minitest::Test
67
67
  Dnsruby::Cache.max_size=1
68
68
  res = Resolver.new()
69
69
  Dnsruby::PacketSender.clear_caches()
70
- assert (Dnsruby::PacketSender.recursive_cache_length == 0)
70
+ assert(Dnsruby::PacketSender.recursive_cache_length == 0)
71
71
  msg = res.query("example.com")
72
- assert (!msg.cached)
73
- assert (Dnsruby::PacketSender.recursive_cache_length == 1)
72
+ assert(!msg.cached)
73
+ assert(Dnsruby::PacketSender.recursive_cache_length == 1)
74
74
  msg = res.query("example.com")
75
- assert (msg.cached)
76
- assert (Dnsruby::PacketSender.recursive_cache_length == 1)
75
+ assert(msg.cached)
76
+ assert(Dnsruby::PacketSender.recursive_cache_length == 1)
77
77
  msg = res.query("google.com")
78
- assert (!msg.cached)
79
- assert (Dnsruby::PacketSender.recursive_cache_length == 1)
78
+ assert(!msg.cached)
79
+ assert(Dnsruby::PacketSender.recursive_cache_length == 1)
80
80
  msg = res.query("example.com")
81
- assert (!msg.cached)
82
- assert (Dnsruby::PacketSender.recursive_cache_length == 1)
81
+ assert(!msg.cached)
82
+ assert(Dnsruby::PacketSender.recursive_cache_length == 1)
83
83
  Dnsruby::Cache.max_size=2
84
- assert (Dnsruby::PacketSender.recursive_cache_length == 1)
84
+ assert(Dnsruby::PacketSender.recursive_cache_length == 1)
85
85
  msg = res.query("example.com")
86
- assert (msg.cached)
87
- assert (Dnsruby::PacketSender.recursive_cache_length == 1)
86
+ assert(msg.cached)
87
+ assert(Dnsruby::PacketSender.recursive_cache_length == 1)
88
88
  msg = res.query("google.com")
89
- assert (!msg.cached)
90
- assert (Dnsruby::PacketSender.recursive_cache_length == 2)
89
+ assert(!msg.cached)
90
+ assert(Dnsruby::PacketSender.recursive_cache_length == 2)
91
91
  end
92
92
 
93
93
  def test_resolver_do_caching
@@ -98,7 +98,7 @@ class TestCache < Minitest::Test
98
98
  res.do_caching = false
99
99
  assert(!res.do_caching)
100
100
  res.udp_size = 4096
101
- ret = res.query("overflow.net-dns.org", Types.TXT)
101
+ ret = res.query("net-dns.org", Types.TXT)
102
102
  # ret = res.query("overflow.dnsruby.validation-test-servers.nominet.org.uk", Types.TXT)
103
103
  # print "#{ret}\n"
104
104
  assert(!ret.cached)
@@ -106,12 +106,12 @@ class TestCache < Minitest::Test
106
106
  assert(ret.header.aa)
107
107
  # Store the ttls
108
108
  first_ttls = ret.answer.rrset(
109
- "overflow.net-dns.org", Types.TXT).ttl
109
+ "net-dns.org", Types.TXT).ttl
110
110
  # "overflow.dnsruby.validation-test-servers.nominet.org.uk", Types.TXT).ttl
111
111
  # Wait a while
112
112
  sleep(1)
113
113
  # Ask for the same records
114
- ret = res.query("overflow.net-dns.org", Types.TXT)
114
+ ret = res.query("net-dns.org", Types.TXT)
115
115
  # ret = res.query("overflow.dnsruby.validation-test-servers.nominet.org.uk", Types.TXT)
116
116
  # print "#{ret}\n"
117
117
  assert(ret.rcode == RCode.NoError)
@@ -125,7 +125,7 @@ class TestCache < Minitest::Test
125
125
  res = SingleResolver.new("ns.nlnetlabs.nl.")
126
126
  # res = SingleResolver.new("ns0.validation-test-servers.nominet.org.uk.")
127
127
  res.udp_size = 4096
128
- query = Message.new("overflow.net-dns.org", Types.TXT)
128
+ query = Message.new("net-dns.org", Types.TXT)
129
129
  # query = Message.new("overflow.dnsruby.validation-test-servers.nominet.org.uk", Types.TXT)
130
130
  ret = res.send_message(query)
131
131
  # print "#{ret}\n"
@@ -134,19 +134,19 @@ class TestCache < Minitest::Test
134
134
  assert(ret.header.aa)
135
135
  # Store the ttls
136
136
  first_ttls = ret.answer.rrset(
137
- "overflow.net-dns.org", Types.TXT).ttl
137
+ "net-dns.org", Types.TXT).ttl
138
138
  # "overflow.dnsruby.validation-test-servers.nominet.org.uk", Types.TXT).ttl
139
139
  # Wait a while
140
140
  sleep(1)
141
141
  # Ask for the same records
142
- query = Message.new("overflow.net-dns.org", Types.TXT)
142
+ query = Message.new("net-dns.org", Types.TXT)
143
143
  # query = Message.new("overflow.dnsruby.validation-test-servers.nominet.org.uk", Types.TXT)
144
144
  ret = res.send_message(query)
145
145
  # print "#{ret}\n"
146
146
  assert(ret.rcode == RCode.NoError)
147
147
  assert(ret.cached)
148
148
  second_ttls = ret.answer.rrset(
149
- "overflow.net-dns.org", Types.TXT).ttl
149
+ "net-dns.org", Types.TXT).ttl
150
150
  # "overflow.dnsruby.validation-test-servers.nominet.org.uk", Types.TXT).ttl
151
151
  # make sure the ttl is less the time we waited
152
152
  assert((second_ttls == first_ttls - 1) || (second_ttls == first_ttls - 2),
@@ -195,20 +195,20 @@ class TestSingleResolver < Minitest::Test
195
195
  assert_equal('10.0.1.128', ip.to_s, 'nameserver() looks up cname.')
196
196
  end
197
197
 
198
- def test_truncated_response
199
- res = SingleResolver.new
200
- # print "Dnssec = #{res.dnssec}\n"
201
- # res.server=('ns0.validation-test-servers.nominet.org.uk')
202
- res.server=('ns.nlnetlabs.nl')
203
- res.packet_timeout = 15
204
- begin
205
- m = res.query("overflow.net-dns.org", 'txt')
206
- assert(m.header.ancount == 62, "62 answer records expected, got #{m.header.ancount}")
207
- assert(!m.header.tc, "Message was truncated!")
208
- rescue ResolvTimeout => e
209
- rescue ServFail => e # not sure why, but we get this on Travis...
210
- end
211
- end
198
+ # def test_truncated_response
199
+ # res = SingleResolver.new
200
+ # # print "Dnssec = #{res.dnssec}\n"
201
+ # # res.server=('ns0.validation-test-servers.nominet.org.uk')
202
+ # res.server=('ns.nlnetlabs.nl')
203
+ # res.packet_timeout = 15
204
+ # begin
205
+ # m = res.query("overflow.net-dns.org", 'txt')
206
+ # assert(m.header.ancount == 62, "62 answer records expected, got #{m.header.ancount}")
207
+ # assert(!m.header.tc, "Message was truncated!")
208
+ # rescue ResolvTimeout => e
209
+ # rescue ServFail => e # not sure why, but we get this on Travis...
210
+ # end
211
+ # end
212
212
 
213
213
  def test_illegal_src_port
214
214
  # Try to set src_port to an illegal value - make sure error raised, and port OK
@@ -17,27 +17,30 @@
17
17
  require_relative 'spec_helper'
18
18
  require_relative 'test_dnsserver'
19
19
 
20
- # The TCPPipeliningServer links our TCPPipeliningHandler on
20
+ # The TCPPipeliningServer links our NioTcpPipeliningHandler on
21
21
  # the loopback interface.
22
- class TCPPipeliningServer < RubyDNS::RuleBasedServer
23
-
24
- PORT = 53937
22
+ class TCPPipeliningServer < RubyDNS::Server
23
+ PORT = 53937
25
24
  IP = '127.0.0.1'
26
25
 
26
+ DEFAULT_MAX_REQUESTS = 4
27
+ DEFAULT_TIMEOUT = 3
28
+
27
29
  @@stats = Stats.new
28
30
 
29
31
  def self.stats
30
32
  @@stats
31
33
  end
32
34
 
35
+ def process(name, resource_class, transaction)
36
+ @logger.debug "name: #{name}"
37
+ transaction.respond!("93.184.216.34", { resource_class: Resolv::DNS::Resource::IN::A })
38
+ end
39
+
33
40
  def run
34
41
  fire(:setup)
35
42
 
36
- link TCPPipeliningHandler.new(self,
37
- IP,
38
- PORT,
39
- TCPPipeliningHandler::DEFAULT_MAX_REQUESTS,
40
- TCPPipeliningHandler::DEFAULT_TIMEOUT)
43
+ link NioTcpPipeliningHandler.new(self, IP, PORT, DEFAULT_MAX_REQUESTS, DEFAULT_TIMEOUT) #4 max request
41
44
 
42
45
  fire(:start)
43
46
  end
@@ -45,8 +48,6 @@ end
45
48
 
46
49
  class TestTCPPipelining < Minitest::Test
47
50
 
48
- QUERIES = %w(psi.net passport.net verisigninc.com google.com yahoo.com apple.com)
49
-
50
51
  class << self
51
52
  attr_accessor :query_id
52
53
  end
@@ -56,6 +57,7 @@ class TestTCPPipelining < Minitest::Test
56
57
  Celluloid.boot
57
58
  # By default, Celluloid logs output to console. Use Dnsruby.log instead
58
59
  Celluloid.logger = Dnsruby.log
60
+ #Dnsruby.log.level = Logger::ERROR
59
61
  @initialized = true
60
62
  @query_id = 0
61
63
  end
@@ -63,9 +65,6 @@ class TestTCPPipelining < Minitest::Test
63
65
 
64
66
  def setup
65
67
  self.class.init
66
- @@upstream ||= RubyDNS::Resolver.new([
67
- [:udp, '193.0.14.129', 53],
68
- [:tcp, '193.0.14.129', 53]])
69
68
 
70
69
  # Instantiate a new server that uses our tcp pipelining handler
71
70
  # For each query the server sends the query upstream (193.0.14.129)
@@ -74,11 +73,7 @@ class TestTCPPipelining < Minitest::Test
74
73
  asynchronous: true
75
74
  }
76
75
 
77
- @@supervisor ||= RubyDNS::run_server(options) do
78
- otherwise do |transaction|
79
- transaction.passthrough!(@@upstream)
80
- end
81
- end
76
+ @@supervisor ||= RubyDNS::run_server(options)
82
77
 
83
78
  # Instantiate our resolver. The resolver will use the same pipeline as much as possible.
84
79
  # If a timeout occurs or max_request_per_connection a new connection should be initiated
@@ -95,9 +90,11 @@ class TestTCPPipelining < Minitest::Test
95
90
 
96
91
  # Send x number of queries asynchronously to our resolver
97
92
  def send_async_messages(number_of_messages, queue, wait_seconds = 0)
98
- query_cycler = QUERIES.cycle
93
+ Celluloid.logger.debug "Sending #{number_of_messages} messages"
99
94
  number_of_messages.times do
100
- message = Dnsruby::Message.new(query_cycler.next)
95
+ name = "#{self.class.query_id}.com"
96
+ Celluloid.logger.debug "Sending #{name}"
97
+ message = Dnsruby::Message.new(name)
101
98
  # self.class.query_id identifies our query, must be different for each message
102
99
  @@resolver.send_async(message, queue, self.class.query_id)
103
100
  self.class.query_id += 1
@@ -117,9 +114,39 @@ class TestTCPPipelining < Minitest::Test
117
114
  end
118
115
  end
119
116
 
117
+ def accept_wait(accept_count, max)
118
+ i = 0
119
+ while TCPPipeliningServer.stats.accept_count < accept_count
120
+ sleep 0.5
121
+ i+=0.5
122
+ assert(i<max, "Max wait for accept reached #{TCPPipeliningServer.stats.accept_count} accepts < #{accept_count}")
123
+ end
124
+ end
125
+
126
+ def connection_wait(connection_count, max)
127
+ i = 0
128
+ while TCPPipeliningServer.stats.connections > connection_count
129
+ sleep 0.5
130
+ i+=0.5
131
+ assert(i<max, "Max wait for connection reached: #{TCPPipeliningServer.stats.connections} active connections > #{connection_count}")
132
+ end
133
+ end
134
+
135
+ def timeout_wait(timeout_count, max)
136
+ i = 0
137
+ while TCPPipeliningServer.stats.timeout_count < timeout_count
138
+ sleep 0.5
139
+ i+=0.5
140
+ assert(i<max, "Max wait for timeout reached #{TCPPipeliningServer.stats.timeout_count} timeounts < #{timeout_count}")
141
+ end
142
+ end
143
+
120
144
  # This test initiates multiple asynchronous requests and verifies they go on the same tcp
121
145
  # pipeline or a new one depending on timeouts
122
146
  def test_TCP_pipelining_timeout
147
+ Celluloid.logger.debug "test_TCP_pipelining_timeout"
148
+ connection_wait(0, TCPPipeliningServer::DEFAULT_TIMEOUT*5)
149
+
123
150
  accept_count = TCPPipeliningServer.stats.accept_count
124
151
  timeout_count = TCPPipeliningServer.stats.timeout_count
125
152
 
@@ -133,8 +160,8 @@ class TestTCPPipelining < Minitest::Test
133
160
 
134
161
  assert_equal(accept_count + 1, TCPPipeliningServer.stats.accept_count)
135
162
 
136
- # Wait for the timeout to occur (5s) and check timeout_count
137
- sleep TCPPipeliningHandler::DEFAULT_TIMEOUT + 0.5
163
+ connection_wait(0, TCPPipeliningServer::DEFAULT_TIMEOUT*5)
164
+ timeout_wait(timeout_count + 1, TCPPipeliningServer::DEFAULT_TIMEOUT*5)
138
165
 
139
166
  assert_equal(timeout_count + 1, TCPPipeliningServer.stats.timeout_count)
140
167
 
@@ -142,39 +169,48 @@ class TestTCPPipelining < Minitest::Test
142
169
  send_async_messages(3, query_queue)
143
170
  verify_responses(3, query_queue)
144
171
 
145
- assert_equal(accept_count + 2, TCPPipeliningServer.stats.accept_count)
146
-
147
- # Wait for the timeout to occur and check timeout_count
148
- sleep TCPPipeliningHandler::DEFAULT_TIMEOUT + 0.5
172
+ connection_wait(0, TCPPipeliningServer::DEFAULT_TIMEOUT*5)
173
+ timeout_wait(timeout_count + 2, TCPPipeliningServer::DEFAULT_TIMEOUT*5)
149
174
 
175
+ assert_equal(accept_count + 2, TCPPipeliningServer.stats.accept_count)
150
176
  assert_equal(timeout_count + 2, TCPPipeliningServer.stats.timeout_count)
177
+
178
+ connection_wait(0, TCPPipeliningServer::DEFAULT_TIMEOUT*5)
151
179
  end
152
180
 
153
181
  # Test timeout occurs and new connection is initiated inbetween 2 sends
154
182
  def test_TCP_pipelining_timeout_in_send
183
+ Celluloid.logger.debug "test_TCP_pipelining_timeout_in_send"
184
+ connection_wait(0, TCPPipeliningServer::DEFAULT_TIMEOUT*5)
185
+
155
186
  accept_count = TCPPipeliningServer.stats.accept_count
156
187
  timeout_count = TCPPipeliningServer.stats.timeout_count
157
188
 
158
189
  query_queue = Queue.new
159
190
 
160
- # Initiate another 3 queries but wait 3s after each query.
191
+ # Initiate another 2 queries wait and then send a final query
161
192
  # Check accept_count. Wait for timeout and verify we got 2 additional timeouts.
162
- send_async_messages(3, query_queue, TCPPipeliningHandler::DEFAULT_TIMEOUT / 2.0 + 0.5)
163
- verify_responses(3, query_queue)
193
+ send_async_messages(2, query_queue)
194
+ verify_responses(2, query_queue)
195
+
196
+ accept_wait(accept_count+1, TCPPipeliningServer::DEFAULT_TIMEOUT*5)
197
+ connection_wait(0, TCPPipeliningServer::DEFAULT_TIMEOUT*5)
198
+
199
+ send_async_messages(1, query_queue)
200
+
201
+ verify_responses(1, query_queue)
164
202
 
165
203
  assert_equal(accept_count + 2, TCPPipeliningServer.stats.accept_count)
166
204
 
167
- sleep TCPPipeliningHandler::DEFAULT_TIMEOUT + 0.5
205
+ connection_wait(0, TCPPipeliningServer::DEFAULT_TIMEOUT*5)
168
206
 
169
- assert_equal(timeout_count + 2, TCPPipeliningServer.stats.timeout_count)
207
+ timeout_wait(timeout_count + 2, TCPPipeliningServer::DEFAULT_TIMEOUT*5)
170
208
  end
171
209
 
172
210
  # Test that we get a SocketEofResolvError if the servers closes the socket before
173
211
  # all queries are answered
174
212
  def test_TCP_pipelining_socket_eof
175
- accept_count = TCPPipeliningServer.stats.accept_count
176
- timeout_count = TCPPipeliningServer.stats.timeout_count
177
- max_count = TCPPipeliningServer.stats.max_count
213
+ connection_wait(0, TCPPipeliningServer::DEFAULT_TIMEOUT*5)
178
214
 
179
215
  query_queue = Queue.new
180
216
 
@@ -183,11 +219,21 @@ class TestTCPPipelining < Minitest::Test
183
219
  # Verify we got max_count was incremented
184
220
  send_async_messages(6, query_queue)
185
221
 
186
- step = 0
222
+ responses = []
223
+
187
224
  6.times do
188
- _response_id, response, exception = query_queue.pop
189
- if step < TCPPipeliningHandler::DEFAULT_MAX_REQUESTS
190
- assert_nil(exception)
225
+ response = query_queue.pop
226
+ responses << response
227
+ end
228
+
229
+ responses.sort_by { |response| response[0] }
230
+
231
+ step = 0
232
+
233
+ responses.each do | response |
234
+ _response_id, response, exception = response
235
+ if step < TCPPipeliningServer::DEFAULT_MAX_REQUESTS
236
+ assert_nil(exception, "Exception not nil for msg #{step} < #{TCPPipeliningServer::DEFAULT_MAX_REQUESTS} requests")
191
237
  assert(response.is_a?(Dnsruby::Message))
192
238
  else
193
239
  assert_equal(Dnsruby::SocketEofResolvError, exception.class)
@@ -196,8 +242,6 @@ class TestTCPPipelining < Minitest::Test
196
242
  step += 1
197
243
  end
198
244
 
199
- assert_equal(accept_count + 1, TCPPipeliningServer.stats.accept_count)
200
- assert_equal(timeout_count, TCPPipeliningServer.stats.timeout_count)
201
- assert_equal(max_count + 1, TCPPipeliningServer.stats.max_count)
245
+ connection_wait(0, TCPPipeliningServer::DEFAULT_TIMEOUT*5)
202
246
  end
203
247
  end
@@ -17,22 +17,62 @@
17
17
  require 'rubydns'
18
18
  require 'nio'
19
19
  require 'socket'
20
+ require 'thread'
20
21
 
21
- # TCPPipeliningHandler accepts new tcp connection and reads data from the sockets until
22
+ class SimpleTimers
23
+ def initialize
24
+ @events = {}
25
+ end
26
+
27
+ def empty?
28
+ @events.empty?
29
+ end
30
+
31
+ def after(seconds, &block)
32
+ eventTime = Time.now + seconds
33
+ @events[eventTime] ||= []
34
+ @events[eventTime] << block
35
+ end
36
+
37
+ def fire
38
+ now = Time.now
39
+
40
+ events = @events.select { |key, value| key <= now }
41
+
42
+ (events || []).each do |key, blocks|
43
+ blocks.each do |block|
44
+ block.call
45
+ end
46
+ @events.delete(key)
47
+ end
48
+ end
49
+
50
+ def wait_interval
51
+ next_event = @events.keys.min
52
+ next_event.nil? ? nil : next_event - Time.now
53
+ end
54
+ end
55
+
56
+ # NioTcpPipeliningHandler accepts new tcp connection and reads data from the sockets until
22
57
  # either the client closes the connection, @max_requests_per_connection is reached
23
58
  # or @timeout is attained.
24
59
 
25
60
  class NioTcpPipeliningHandler < RubyDNS::GenericHandler
26
61
 
27
62
  DEFAULT_MAX_REQUESTS = 4
28
-
63
+ DEFAULT_TIMEOUT = 3
29
64
  # TODO Add timeout
30
- def initialize(server, host, port, max_requests = DEFAULT_MAX_REQUESTS)
65
+ def initialize(server, host, port, max_requests = DEFAULT_MAX_REQUESTS, timeout = DEFAULT_TIMEOUT)
31
66
  super(server)
32
67
  @max_requests_per_connection = max_requests
68
+ @timeout = timeout
33
69
  @socket = TCPServer.new(host, port)
34
70
  @count = {}
35
71
 
72
+ @server.class.stats.connections = @count.keys.count
73
+
74
+ @timers = SimpleTimers.new
75
+
36
76
  @selector = NIO::Selector.new
37
77
  monitor = @selector.register(@socket, :r)
38
78
  monitor.value = proc { accept }
@@ -62,18 +102,26 @@ class NioTcpPipeliningHandler < RubyDNS::GenericHandler
62
102
  _, _remote_port, remote_host = socket.peeraddr
63
103
  options = { peer: remote_host }
64
104
 
65
- input_data = RubyDNS::StreamTransport.read_chunk(socket)
66
- response = process_query(input_data, options)
67
- RubyDNS::StreamTransport.write_message(socket, response)
68
-
105
+ new_connection = @count[socket].nil?
69
106
  @count[socket] ||= 0
70
107
  @count[socket] += 1
108
+ @server.class.stats.connection_accept(new_connection, @count.keys.count)
109
+
110
+ #we read all data until timeout
111
+ input_data = RubyDNS::StreamTransport.read_chunk(socket)
71
112
 
113
+ if @count[socket] <= @max_requests_per_connection
114
+ response = process_query(input_data, options)
115
+ RubyDNS::StreamTransport.write_message(socket, response)
116
+ end
117
+
118
+ =begin
72
119
  if @count[socket] >= @max_requests_per_connection
73
120
  _, port, host = socket.peeraddr
74
121
  @logger.debug("*** max request for #{host}:#{port}")
75
122
  remove(socket)
76
123
  end
124
+ =end
77
125
  rescue EOFError
78
126
  _, port, host = socket.peeraddr
79
127
  @logger.debug("*** #{host}:#{port} disconnected")
@@ -81,118 +129,92 @@ class NioTcpPipeliningHandler < RubyDNS::GenericHandler
81
129
  remove(socket)
82
130
  end
83
131
 
84
- def remove(socket)
132
+ def remove(socket, update_connections=true)
85
133
  @logger.debug("Removing socket from selector")
86
134
  socket.close rescue nil
87
- @selector.deregister(socket)
88
- @count.delete(socket)
135
+ @selector.deregister(socket) rescue nil
136
+ socket_count = @count.delete(socket)
137
+ @server.class.stats.connections = @count.keys.count if update_connections
138
+ socket_count
89
139
  end
90
140
 
91
141
  def create_selector_thread
92
142
  Thread.new do
93
143
  loop do
94
- @selector.select { |monitor| monitor.value.call(monitor) }
95
- break if @selector.closed?
144
+ begin
145
+ @timers.fire
146
+ intervals = [@timers.wait_interval || 0.1, 0.1]
147
+
148
+ @selector.select(intervals.min > 0 ? intervals.min : 0.1) do
149
+ |monitor| monitor.value.call(monitor)
150
+ end
151
+
152
+ @logger.debug "Woke up"
153
+ break if @selector.closed?
154
+ rescue Exception => e
155
+ @logger.debug "Exception #{e}"
156
+ @logger.debug "Backtrace #{e.backtrace}"
157
+ end
96
158
  end
97
159
  end
98
160
  end
99
161
 
100
162
  def handle_connection(socket)
101
163
  @logger.debug "New connection"
102
- @server.class.stats.increment_connection
103
-
104
164
  @logger.debug "Add socket to @selector"
165
+
105
166
  monitor = @selector.register(socket, :r)
106
167
  monitor.value = proc { process_socket(socket) }
107
- end
108
- end
109
-
110
- class TCPPipeliningHandler < RubyDNS::GenericHandler
111
- DEFAULT_MAX_REQUESTS = 4
112
- DEFAULT_TIMEOUT = 3.0
113
-
114
- def initialize(server, host, port, max_requests = DEFAULT_MAX_REQUESTS, timeout = DEFAULT_TIMEOUT)
115
- super(server)
116
- @timeout = timeout
117
- @max_requests_per_connection = max_requests
118
- @socket = TCPServer.new(host, port)
119
168
 
120
- async.run
121
- end
122
-
123
- finalizer :finalize
124
-
125
- def finalize
126
- @socket.close if @socket
127
- end
128
-
129
- def run
130
- loop { async.handle_connection(@socket.accept) }
131
- end
132
-
133
-
134
- def handle_connection(socket)
135
- _, _remote_port, remote_host = socket.peeraddr
136
- options = { peer: remote_host }
137
-
138
- @logger.debug "New connection"
139
- @server.class.stats.increment_connection
140
-
141
- timeout = @timeout
142
- msg_count = 0
143
-
144
- loop do
145
- start_time = Time.now
146
- @logger.debug "Waiting for #{timeout} max"
147
- sockets = ::IO.select([socket], nil , nil, timeout)
148
- duration = Time.now - start_time
149
-
150
- @logger.debug "Slept for #{duration}"
151
-
152
- timeout -= duration
153
-
154
- if sockets
155
- input_data = RubyDNS::StreamTransport.read_chunk(socket)
156
- response = process_query(input_data, options)
157
- RubyDNS::StreamTransport.write_message(socket, response)
158
-
159
- msg_count += 1
160
- @logger.debug "Responded to message #{msg_count}"
161
- else
162
- @logger.debug "TCP session timeout!"
163
- @server.class.stats.increment_timeout
164
- break
169
+ @logger.debug "Add socket timer of #{@timeout}"
170
+ @timers.after(@timeout) do
171
+ @logger.debug "Timeout fired for socket #{socket}"
172
+ count = remove(socket, false)
173
+ unless count.nil?
174
+ @logger.debug "Timeout for socket #{socket}"
175
+ @logger.debug "Increasing timeout count"
176
+ @server.class.stats.connection_timeout(@count.keys.count)
165
177
  end
166
-
167
- if msg_count >= @max_requests_per_connection
168
- @logger.debug "Max number of requests attained (#{@max_requests_per_connection})"
169
- @server.class.stats.increment_max
170
- break
171
- end
172
-
173
178
  end
174
- rescue EOFError
175
- @logger.warn "TCP session ended (closed by client)"
176
- rescue DecodeError
177
- @logger.warn "Could not decode incoming TCP data!"
178
- ensure
179
- socket.close
180
179
  end
181
180
  end
182
181
 
183
182
  # Stats collects statistics from our tcp handler
184
183
  class Stats
185
184
  def initialize()
186
- @mutex = Mutex.new
187
- @accept_count = 0
185
+ @mutex = Mutex.new
186
+ @accept_count = 0
188
187
  @timeout_count = 0
189
- @max_count = 0
188
+ @max_count = 0
189
+ @connections = 0
190
190
  end
191
191
 
192
192
  def increment_max; @mutex.synchronize { @max_count += 1 } end
193
193
  def increment_timeout; @mutex.synchronize { @timeout_count += 1 } end
194
194
  def increment_connection; @mutex.synchronize { @accept_count += 1 } end
195
195
 
196
+ def connection_timeout(active_connections)
197
+ @mutex.synchronize do
198
+ @timeout_count += 1
199
+ @connections = active_connections
200
+ end
201
+ end
202
+
203
+ def connection_accept(new_connection, active_connections)
204
+ @mutex.synchronize {
205
+ @connections = active_connections
206
+ @accept_count += 1 if new_connection
207
+ }
208
+ end
209
+
210
+ def connections=(active_connections)
211
+ @mutex.synchronize { @connections = active_connections }
212
+ end
213
+
214
+ def connections
215
+ @mutex.synchronize { @connections }
216
+ end
217
+
196
218
  def accept_count
197
219
  @mutex.synchronize { @accept_count }
198
220
  end
@@ -204,5 +226,4 @@ class Stats
204
226
  def max_count
205
227
  @mutex.synchronize { @max_count }
206
228
  end
207
-
208
229
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dnsruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.59.0
4
+ version: 1.59.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alex Dalitz
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-10-21 00:00:00.000000000 Z
11
+ date: 2016-01-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: pry
@@ -100,6 +100,20 @@ dependencies:
100
100
  - - "~>"
101
101
  - !ruby/object:Gem::Version
102
102
  version: '1.1'
103
+ - !ruby/object:Gem::Dependency
104
+ name: minitest-display
105
+ requirement: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: 0.3.0
110
+ type: :development
111
+ prerelease: false
112
+ version_requirements: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: 0.3.0
103
117
  - !ruby/object:Gem::Dependency
104
118
  name: coveralls
105
119
  requirement: !ruby/object:Gem::Requirement