dnsruby 1.57.0 → 1.58.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -2
  3. data/.travis.yml +1 -1
  4. data/README.md +2 -2
  5. data/RELEASE_NOTES.md +16 -0
  6. data/Rakefile +2 -0
  7. data/dnsruby.gemspec +2 -0
  8. data/lib/dnsruby.rb +5 -0
  9. data/lib/dnsruby/bit_mapping.rb +138 -0
  10. data/lib/dnsruby/bitmap.rb +108 -0
  11. data/lib/dnsruby/message/decoder.rb +90 -80
  12. data/lib/dnsruby/message/message.rb +16 -3
  13. data/lib/dnsruby/message/section.rb +3 -3
  14. data/lib/dnsruby/name.rb +2 -2
  15. data/lib/dnsruby/packet_sender.rb +73 -1
  16. data/lib/dnsruby/resolv.rb +56 -42
  17. data/lib/dnsruby/resolver.rb +95 -73
  18. data/lib/dnsruby/resource/GPOS.rb +9 -3
  19. data/lib/dnsruby/resource/HIP.rb +1 -1
  20. data/lib/dnsruby/resource/IN.rb +3 -1
  21. data/lib/dnsruby/resource/NAPTR.rb +2 -2
  22. data/lib/dnsruby/resource/NSEC3.rb +2 -2
  23. data/lib/dnsruby/resource/NXT.rb +302 -0
  24. data/lib/dnsruby/resource/OPT.rb +2 -2
  25. data/lib/dnsruby/resource/TXT.rb +2 -2
  26. data/lib/dnsruby/resource/generic.rb +1 -0
  27. data/lib/dnsruby/resource/type_bitmap.rb +0 -0
  28. data/lib/dnsruby/select_thread.rb +174 -83
  29. data/lib/dnsruby/single_resolver.rb +2 -2
  30. data/lib/dnsruby/version.rb +1 -1
  31. data/lib/dnsruby/zone_reader.rb +19 -9
  32. data/lib/dnsruby/zone_transfer.rb +1 -1
  33. data/test/spec_helper.rb +9 -1
  34. data/test/tc_axfr.rb +17 -4
  35. data/test/tc_gpos.rb +3 -3
  36. data/test/tc_message.rb +7 -1
  37. data/test/tc_nxt.rb +192 -0
  38. data/test/tc_recur.rb +2 -1
  39. data/test/tc_resolv.rb +73 -0
  40. data/test/tc_resolver.rb +22 -4
  41. data/test/tc_rr-opt.rb +9 -8
  42. data/test/tc_rr.rb +22 -0
  43. data/test/tc_single_resolver.rb +15 -15
  44. data/test/tc_soak.rb +154 -65
  45. data/test/tc_soak_base.rb +15 -15
  46. data/test/tc_tcp.rb +1 -1
  47. data/test/tc_tcp_pipelining.rb +203 -0
  48. data/test/tc_zone_reader.rb +73 -0
  49. data/test/test_dnsserver.rb +208 -0
  50. data/test/test_utils.rb +49 -0
  51. data/test/ts_offline.rb +59 -41
  52. data/test/ts_online.rb +92 -63
  53. metadata +40 -3
  54. data/test/tc_dnsruby.rb +0 -51
@@ -31,14 +31,20 @@ module Dnsruby
31
31
  # latitude: '20.0',
32
32
  # altitude: '30.0',
33
33
  # }
34
- def self.from_hash(gpos_params_hash)
34
+ #
35
+ # Since the type is assumed to be GPOS, it will be assigned
36
+ # automatially, and any other value will be overwritten.
37
+ # Therefore, having it present in the hash is not necessary.
38
+
39
+ def self.new_from_hash(gpos_params_hash)
40
+ gpos_params_hash[:type] = Types::GPOS
35
41
  RR.new_from_hash(gpos_params_hash)
36
42
  end
37
43
 
38
44
 
39
45
  # Create an instance from a string containing parameters, e.g.:
40
46
  # 'a.dnsruby.com. 10800 IN GPOS 10.0 20.0 30.0'
41
- def self.from_string(gpos_params_string)
47
+ def self.new_from_string(gpos_params_string)
42
48
  RR.new_from_string(gpos_params_string)
43
49
  end
44
50
 
@@ -49,7 +55,7 @@ module Dnsruby
49
55
  # [EXAMPLE_HOSTNAME, Types::GPOS, Classes::IN, EXAMPLE_TTL, rdata.length, rdata, 0]
50
56
  # end
51
57
  # self.from_data(*EXAMPLE_GPOS_DATA)
52
- def self.from_data(*gpos_params_data)
58
+ def self.new_from_data(*gpos_params_data)
53
59
  RR.new_from_data(*gpos_params_data)
54
60
  end
55
61
 
@@ -126,7 +126,7 @@ module Dnsruby
126
126
  public_key = msg.get_bytes(pk_length)
127
127
  rsvs = []
128
128
  # Load in the RSV names, if there are any
129
- while (msg.has_remaining)
129
+ while (msg.has_remaining?)
130
130
  name = msg.get_name
131
131
  rsvs.push(name)
132
132
  end
@@ -52,7 +52,9 @@ module Dnsruby
52
52
  Types::SSHFP => SSHFP,
53
53
  Types::IPSECKEY => IPSECKEY,
54
54
  Types::HIP => HIP,
55
- Types::DHCID => DHCID
55
+ Types::DHCID => DHCID,
56
+ Types::GPOS => GPOS,
57
+ Types::NXT => NXT
56
58
  } #:nodoc: all
57
59
 
58
60
  # module IN contains ARPA Internet specific RRs
@@ -30,7 +30,7 @@ module Dnsruby
30
30
  # The NAPTR RR service field
31
31
  attr_accessor :service
32
32
  # The NAPTR RR regexp field
33
- attr_accessor :regexp
33
+ attr_reader :regexp
34
34
  # The NAPTR RR replacement field
35
35
  attr_accessor :replacement
36
36
 
@@ -95,4 +95,4 @@ module Dnsruby
95
95
  end
96
96
  end
97
97
  end
98
- end
98
+ end
@@ -242,7 +242,7 @@ module Dnsruby
242
242
 
243
243
  def decode_next_hashed(input)
244
244
  @next_hashed = NSEC3.decode_next_hashed(input)
245
- end
245
+ end
246
246
 
247
247
  def NSEC3.decode_next_hashed(input)
248
248
  return Base32.decode32hex(input)
@@ -329,4 +329,4 @@ module Dnsruby
329
329
  end
330
330
  end
331
331
  end
332
- end
332
+ end
@@ -0,0 +1,302 @@
1
+ require_relative = ->(*args) do
2
+ this_file_dir = File.expand_path(File.dirname(__FILE__))
3
+ args.each { |arg| require(File.join(this_file_dir, arg)) }
4
+ end
5
+
6
+ require_relative.('../bitmap', '../bit_mapping', 'RR')
7
+
8
+ module Dnsruby
9
+ class RR
10
+
11
+ # Class for NXT resource records.
12
+ #
13
+ # NXT-specific data types, present in RDATA, are:
14
+ # next_domain: the next domain name, as a Name instance
15
+ # types: array of record types as numbers
16
+ #
17
+ # RFC 2535 (https://www.ietf.org/rfc/rfc2535.txt)
18
+ #
19
+ # The RFC mentions that a low bit of zero in the type RDATA
20
+ # indicates that the highest type code does not exceed 127,
21
+ # and that a low bit of 1 indicates that some mechanism
22
+ # other than a bitmap is being used. This class does not
23
+ # support such non-bitmap mechanisms, and assumes there
24
+ # will always be a bitmap.
25
+ class NXT < RR
26
+
27
+ ClassHash[[TypeValue = Types::NXT, Classes::IN]] = self #:nodoc: all
28
+
29
+ attr_accessor :next_domain, :types
30
+
31
+ REQUIRED_KEYS = [:next_domain, :types]
32
+
33
+ def from_hash(params_hash)
34
+ unless REQUIRED_KEYS.all? { |key| params_hash[key] }
35
+ raise ArgumentError.new("NXT hash must contain all of: #{REQUIRED_KEYS.join(', ')}.")
36
+ end
37
+ @next_domain = Name.create(params_hash[:next_domain]) unless @next_domain.is_a?(Name)
38
+ @types = params_hash[:types]
39
+ end
40
+
41
+ def from_data(data)
42
+ next_domain, types = data
43
+ from_hash(next_domain: next_domain, types: types)
44
+ end
45
+
46
+ def from_string(string)
47
+ next_domain, *type_names = string.split # type names are all but first
48
+ types = NxtTypes::names_to_codes(type_names)
49
+ from_hash(next_domain: next_domain, types: types)
50
+ end
51
+
52
+ # As with all resource record subclasses of RR, this class cannot be
53
+ # directly instantiated, but instead must be instantiated via use of
54
+ # one of the RR class methods. These NXT class methods are wrappers
55
+ # around those RR methods, so that there is an interface on the NXT
56
+ # class for creating NXT instances.
57
+
58
+ # Create an instance from a hash of parameters, e.g.:
59
+ #
60
+ # rr = RR::NXT.new_from_hash(
61
+ # name: 'b.dnsruby.com.',
62
+ # ttl: 10800,
63
+ # klass: Classes::IN,
64
+ # next_domain: 'a.dnsruby.com.',
65
+ # types: [Types::SOA, Types::NXT])
66
+ #
67
+ # Since the type is assumed to be NXT, it will be assigned
68
+ # automatically, and any other value will be overwritten.
69
+ # Therefore, having it present in the hash is not necessary.
70
+ def self.new_from_hash(params_hash)
71
+ params_hash[:type] = Types::NXT
72
+ RR.new_from_hash(params_hash)
73
+ end
74
+
75
+ # Create an instance from a string containing parameters, e.g.:
76
+ # b.dnsruby.com. 10800 IN NXT A.dnsruby.com. SOA NXT
77
+ def self.new_from_string(params_string)
78
+ RR.new_from_string(params_string)
79
+ end
80
+
81
+ # Create an instance from an ordered parameter list, e.g.:
82
+ # rdata = RR::NXT.build_rdata('a.dnsruby.com.', [Types::SOA, Types::NXT])
83
+ #
84
+ # rr = RR::NXT.new_from_data('b.dnsruby.com.', Types::NXT,
85
+ # Classes::IN, 10800, rdata.size, rdata, 0)
86
+ def self.new_from_data(*params_data)
87
+ RR.new_from_data(*params_data)
88
+ end
89
+
90
+ # Builds rdata from the provided information.
91
+ # @param next_domain either a string or a Name
92
+ # @param types an array of types (where each type is the numeric type code)
93
+ # or a TypeBitmap
94
+ def self.build_rdata(next_domain, types)
95
+ next_domain = Name.create(next_domain) if next_domain.is_a?(String)
96
+ types = TypeBitmap.from_type_codes(types) if types.is_a?(Array)
97
+
98
+ binary_string = ''.force_encoding('ASCII-8BIT')
99
+ binary_string << next_domain.canonical
100
+ binary_string << BitMapping.reverse_binary_string_bits(types.to_binary_string)
101
+ binary_string
102
+ end
103
+
104
+ # From the RFC:
105
+ # NXT has the following format:
106
+ # foo.nil. NXT big.foo.nil NS KEY SOA NXT
107
+ # <owner> NXT <next_domain> <record types>
108
+ #
109
+ # We handle the rdata, the RR superclass does the rest.
110
+ def rdata_to_string
111
+ "#{next_domain} #{NxtTypes.codes_to_names(types).join(' ')}"
112
+ end
113
+
114
+ def encode_rdata(message_encoder, _canonical)
115
+ message_encoder.put_bytes(build_rdata)
116
+ end
117
+
118
+ def build_rdata
119
+ self.class.build_rdata(next_domain, types)
120
+ end
121
+
122
+ def self.decode_rdata(message_decoder)
123
+
124
+ start_index = message_decoder.index
125
+
126
+ rdata_len = -> do
127
+ rdata_length_str = message_decoder.data[start_index - 2, 2]
128
+ rdata_length_str.unpack('n').first
129
+ end
130
+
131
+ next_domain_and_bitmap = -> do
132
+ next_domain = message_decoder.get_name
133
+ bitmap_start_index = message_decoder.index
134
+
135
+ # If we're being called from new_from_data, the MessageDecoder
136
+ # contains only the rdata, not the entire message, and there will
137
+ # be no encoded length for us to read.
138
+ called_from_new_from_data = (start_index == 0)
139
+ bitmap_length = called_from_new_from_data \
140
+ ? message_decoder.data.size \
141
+ : rdata_len.() - (bitmap_start_index - start_index)
142
+
143
+ bitmap = message_decoder.get_bytes(bitmap_length)
144
+ bitmap = BitMapping.reverse_binary_string_bits(bitmap)
145
+ [next_domain, bitmap]
146
+ end
147
+
148
+ next_domain, type_bitmap = next_domain_and_bitmap.()
149
+ types = TypeBitmap.from_binary_string(type_bitmap).to_type_array
150
+ new(next_domain: next_domain, types: types)
151
+ end
152
+
153
+ # 'name' is used in the RR superclass, but 'owner' is the term referred to
154
+ # in the RFC, so we'll make owner an alias for name.
155
+ alias_method(:owner, :name)
156
+ alias_method(:owner=, :name=)
157
+
158
+
159
+ # Methods used to manipulate the storage and representation of
160
+ # record types as stored in NXT record bitmaps.
161
+ module NxtTypes
162
+
163
+ module_function
164
+
165
+ # Maximum bitmap size is 128 bytes; since it's zero offset
166
+ # values are 0..(2 ** 128 - 1). However, the least
167
+ # significant bit must not be set, so the maximum is 1 less than that.
168
+ MAX_BITMAP_NUMBER_VALUE = (2 ** 128) - 1 - 1
169
+
170
+ # Convert a numeric type code to its corresponding name (e.g. "A" => 1).
171
+ # Unknown types are named "TYPE#{number}".
172
+ def code_to_name(number)
173
+ Types.to_string(number) || "TYPE#{number}"
174
+ end
175
+
176
+ # Convert a type name to its corresponding numeric type code.
177
+ # Names matching /^TYPE(\d+)$/ are assumed to have a code
178
+ # corresponding to the numeric value of the substring following 'TYPE'.
179
+ def name_to_code(name)
180
+ code = Types.to_code(name)
181
+ if code.nil?
182
+ matches = /^TYPE(\d+)$/.match(name)
183
+ code = matches[1].to_i if matches
184
+ end
185
+ code
186
+ end
187
+
188
+ # For a given array of type names, return an array of codes.
189
+ def names_to_codes(names)
190
+ names.map { |s| name_to_code(s) }
191
+ end
192
+
193
+ # For the specified string containing names (e.g. 'A NS'),
194
+ # return an array containing the corresponding codes.
195
+ def names_string_to_codes(name_string)
196
+ names_to_codes(name_string.split(' '))
197
+ end
198
+
199
+ # For the given array of type codes, return an array of their
200
+ # corresponding names.
201
+ def codes_to_names(codes)
202
+ codes.map { |code| code_to_name(code) }
203
+ end
204
+
205
+ # Generate a string containing the names corresponding to the
206
+ # numeric type codes. Sort it by the numeric type code, ascending.
207
+ def codes_to_string(codes)
208
+ codes.sort.map { |code| code_to_name(code) }.join(' ')
209
+ end
210
+
211
+ # From a binary string of type code bits, return an array
212
+ # of type codes.
213
+ def binary_string_to_codes(binary_string)
214
+ bitmap_number = BitMapping.binary_string_to_number(binary_string)
215
+ assert_legal_bitmap_value(bitmap_number)
216
+ BitMapping.number_to_set_bit_positions_array(bitmap_number)
217
+ end
218
+
219
+ # From a binary string of type code bits, return an array
220
+ # of type names.
221
+ def binary_string_to_names(binary_string)
222
+ codes = binary_string_to_codes(binary_string)
223
+ codes_to_names(codes)
224
+ end
225
+
226
+ # From an array of type codes, return a binary string.
227
+ def codes_to_binary_string(codes)
228
+ codes = codes.sort
229
+ unless legal_code_value?(codes.first) && legal_code_value?(codes.last)
230
+ raise ArgumentError.new("All codes must be between 1 and 127: #{codes.inspect}.")
231
+ end
232
+ bitmap_number = BitMapping.set_bit_position_array_to_number(codes)
233
+ BitMapping.number_to_binary_string(bitmap_number)
234
+ end
235
+
236
+ # Assert that the specified number is a legal value with which to
237
+ # instantiate a NXT type bitmap. Raise on error, do nothing on success.
238
+ def assert_legal_bitmap_value(number)
239
+ max_value = NxtTypes::MAX_BITMAP_NUMBER_VALUE
240
+ if number > max_value
241
+ raise ArgumentError.new("Bitmap maximum value is #{max_value} (0x#{max_value.to_s(16)}).")
242
+ end
243
+ if number & 1 == 1
244
+ raise ArgumentError.new("Bitmap number must not have low bit set.")
245
+ end
246
+ end
247
+
248
+ def legal_code_value?(code)
249
+ (1..127).include?(code)
250
+ end
251
+ end
252
+
253
+
254
+ class TypeBitmap
255
+
256
+ attr_accessor :bitmap
257
+
258
+ # Create an instance from a string containing type names separated by spaces
259
+ # e.g. "A TXT NXT"
260
+ def self.from_names_string(names_string)
261
+ type_codes = BitMapping.names_string_to_codes(names_string)
262
+ from_type_codes(type_codes)
263
+ end
264
+
265
+ # Create an instance from type numeric codes (e.g. 30 for NXT).
266
+ def self.from_type_codes(type_codes)
267
+ new(BitMapping.set_bit_position_array_to_number(type_codes))
268
+ end
269
+
270
+ # Create an instance from a binary string, e.g. from a NXT record RDATA:
271
+ def self.from_binary_string(binary_string)
272
+ new(BitMapping.binary_string_to_number(binary_string))
273
+ end
274
+
275
+ # The constructor is made private so that the name of the method called
276
+ # to create the instance reveals to the reader the type of the initial data.
277
+ private_class_method :new
278
+ def initialize(bitmap_number)
279
+ NxtTypes.assert_legal_bitmap_value(bitmap_number)
280
+ @bitmap = Bitmap.from_number(bitmap_number)
281
+ end
282
+
283
+ # Returns a binary string representing this data, in as few bytes as possible
284
+ # (i.e. no leading zero bytes).
285
+ def to_binary_string
286
+ bitmap.to_binary_string
287
+ end
288
+
289
+ # Returns the instance's data as an array of type codes.
290
+ def to_type_array
291
+ bitmap.to_set_bit_position_array
292
+ end
293
+
294
+ # Output types in dig format, e.g. "A AAAA NXT"
295
+ def to_s
296
+ type_codes = bitmap.to_set_bit_position_array
297
+ NxtTypes.codes_to_string(type_codes)
298
+ end
299
+ end
300
+ end
301
+ end
302
+ end
@@ -248,9 +248,9 @@ module Dnsruby
248
248
  end
249
249
 
250
250
  def self.decode_rdata(msg)#:nodoc: all
251
- if (msg.has_remaining)
251
+ if (msg.has_remaining?)
252
252
  options = []
253
- while (msg.has_remaining) do
253
+ while (msg.has_remaining?) do
254
254
  code = msg.get_unpack('n')[0]
255
255
  len = msg.get_unpack('n')[0]
256
256
  data = msg.get_bytes(len)
@@ -15,7 +15,7 @@
15
15
  # ++
16
16
  begin
17
17
  require 'jcode'
18
- rescue LoadError => e
18
+ rescue LoadError => _e
19
19
  end
20
20
  module Dnsruby
21
21
  class RR
@@ -189,4 +189,4 @@ module Dnsruby
189
189
  end
190
190
  end
191
191
  end
192
- end
192
+ end
@@ -164,3 +164,4 @@ require 'dnsruby/resource/HIP'
164
164
  require 'dnsruby/resource/KX'
165
165
  require 'dnsruby/resource/DHCID'
166
166
  require 'dnsruby/resource/GPOS'
167
+ require 'dnsruby/resource/NXT'
File without changes
@@ -20,6 +20,7 @@ begin
20
20
  rescue LoadError
21
21
  require 'thread'
22
22
  end
23
+ require 'set'
23
24
  require 'singleton'
24
25
  require 'dnsruby/validator_thread.rb'
25
26
  module Dnsruby
@@ -46,13 +47,15 @@ module Dnsruby
46
47
  @@mutex.synchronize {
47
48
  @@in_select=false
48
49
  # @@notifier,@@notified=IO.pipe
49
- @@sockets = [] # @@notified]
50
+ @@sockets = Set.new
50
51
  @@timeouts = Hash.new
51
52
  # @@mutex.synchronize do
52
53
  @@query_hash = Hash.new
53
54
  @@socket_hash = Hash.new
55
+ @@socket_is_persistent = Hash.new
54
56
  @@observers = Hash.new
55
57
  @@tcp_buffers=Hash.new
58
+ @@socket_remaining_queries = Hash.new
56
59
  @@tick_observers = []
57
60
  @@queued_exceptions=[]
58
61
  @@queued_responses=[]
@@ -64,9 +67,8 @@ module Dnsruby
64
67
  BasicSocket.do_not_reverse_lookup = true
65
68
  # end
66
69
  # Now start the select thread
67
- @@select_thread = Thread.new {
68
- do_select
69
- }
70
+ @@select_thread = Thread.new { do_select }
71
+
70
72
  # # Start the validator thread
71
73
  # @@validator = ValidatorThread.instance
72
74
  }
@@ -93,7 +95,7 @@ module Dnsruby
93
95
  class QuerySettings
94
96
  attr_accessor :query_bytes, :query, :ignore_truncation, :client_queue,
95
97
  :client_query_id, :socket, :dest_server, :dest_port, :endtime, :udp_packet_size,
96
- :single_resolver
98
+ :single_resolver, :is_persistent_socket, :tcp_pipelining_max_queries
97
99
  # new(query_bytes, query, ignore_truncation, client_queue, client_query_id,
98
100
  # socket, dest_server, dest_port, endtime, , udp_packet_size, single_resolver)
99
101
  def initialize(*args)
@@ -108,9 +110,21 @@ module Dnsruby
108
110
  @endtime = args[8]
109
111
  @udp_packet_size = args[9]
110
112
  @single_resolver = args[10]
113
+ @is_persistent_socket = false
114
+ @tcp_pipelining_max_queries = nil
111
115
  end
112
116
  end
113
117
 
118
+ def tcp?(socket)
119
+ type = socket.getsockopt(Socket::SOL_SOCKET, Socket::SO_TYPE)
120
+ [Socket::SOCK_STREAM].pack("i") == type.data
121
+ end
122
+
123
+ def udp?(socket)
124
+ type = socket.getsockopt(Socket::SOL_SOCKET, Socket::SO_TYPE)
125
+ [Socket::SOCK_DGRAM].pack("i") == type.data
126
+ end
127
+
114
128
  def add_to_select(query_settings)
115
129
  # Add the query to sockets, and then wake the select thread up
116
130
  @@mutex.synchronize {
@@ -118,9 +132,12 @@ module Dnsruby
118
132
  # @TODO@ This assumes that all client_query_ids are unique!
119
133
  # Would be a good idea at least to check this...
120
134
  @@query_hash[query_settings.client_query_id]=query_settings
121
- @@socket_hash[query_settings.socket]=[query_settings.client_query_id] # @todo@ If we use persistent sockets then we need to update this array
135
+ @@socket_hash[query_settings.socket] ||= []
136
+ @@socket_hash[query_settings.socket] << query_settings.client_query_id
137
+ @@socket_remaining_queries[query_settings.socket] ||= query_settings.tcp_pipelining_max_queries if query_settings.tcp_pipelining_max_queries != :infinite
122
138
  @@timeouts[query_settings.client_query_id]=query_settings.endtime
123
- @@sockets.push(query_settings.socket)
139
+ @@sockets << query_settings.socket
140
+ @@socket_is_persistent[query_settings.socket] = query_settings.is_persistent_socket
124
141
  }
125
142
  begin
126
143
  @@wakeup_sockets[0].send("wakeup!", 0)
@@ -158,14 +175,7 @@ module Dnsruby
158
175
  send_queued_responses
159
176
  send_queued_validation_responses
160
177
  timeout = tick_time = 0.1 # We provide a timer service to various Dnsruby classes
161
- sockets=[]
162
- timeouts=[]
163
- has_observer = false
164
- @@mutex.synchronize {
165
- sockets = @@sockets
166
- timeouts = @@timeouts.values
167
- has_observer = !@@observers.empty?
168
- }
178
+ sockets, timeouts, has_observer = @@mutex.synchronize { [@@sockets.to_a, @@timeouts.values, !@@observers.empty?] }
169
179
  if (timeouts.length > 0)
170
180
  timeouts.sort!
171
181
  timeout = timeouts[0] - Time.now
@@ -185,8 +195,11 @@ module Dnsruby
185
195
  rescue SelectWakeup
186
196
  # If SelectWakeup, then just restart this loop - the select call will be made with the new data
187
197
  next
188
- rescue IOError => e# Don't worry if the socket was closed already
198
+ rescue IOError => e
189
199
  # print "IO Error =: #{e}\n"
200
+ exceptions = clean_up_closed_sockets
201
+ exceptions.each { |exception| send_exception_to_client(*exception) }
202
+
190
203
  next
191
204
  end
192
205
  if ready && ready.include?(@@wakeup_sockets[1])
@@ -201,72 +214,117 @@ module Dnsruby
201
214
  end
202
215
  end
203
216
  if (ready == nil)
204
- # proces the timeouts
217
+ # process the timeouts
205
218
  process_timeouts
206
219
  unused_loop_count+=1
207
220
  else
208
221
  process_ready(ready)
209
222
  unused_loop_count=0
210
- # process_error(errors)
223
+ # process_error(errors)
211
224
  end
212
- @@mutex.synchronize{
225
+ @@mutex.synchronize do
213
226
  if (unused_loop_count > 10 && @@query_hash.empty? && @@observers.empty?)
214
- Dnsruby.log.debug{"Stopping select loop"}
215
- return
227
+ Dnsruby.log.debug("Try stop select loop")
228
+
229
+ non_persistent_sockets = @@sockets.select { |s| ! @@socket_is_persistent[s] }
230
+ non_persistent_sockets.each do |socket|
231
+ socket.close rescue nil
232
+ @@sockets.delete(socket)
233
+ end
234
+
235
+ Dnsruby.log.debug("Deleted #{non_persistent_sockets.size} non-persistent sockets," +
236
+ " #{@@sockets.count} persistent sockets remain.")
237
+ @@socket_hash.clear
238
+
239
+ if @@sockets.empty?
240
+ Dnsruby.log.debug("Stopping select loop")
241
+ return
242
+ end
216
243
  end
217
- }
244
+ end
218
245
  # }
219
246
  end
220
247
  end
221
248
 
249
+ # Removes closed sockets from @@sockets, and returns an array containing 1
250
+ # exception for each closed socket contained in @@socket_hash.
251
+ def clean_up_closed_sockets
252
+ exceptions = @@mutex.synchronize do
253
+ closed_sockets_in_hash = @@sockets.select(&:closed?).select { |s| @@socket_hash[s] }
254
+ @@sockets.delete_if { | socket | socket.closed? }
255
+ closed_sockets_in_hash.each_with_object([]) do |socket, exceptions|
256
+ @@socket_hash[socket].each do | client_id |
257
+ exceptions << [SocketEofResolvError.new("TCP socket closed before all answers received"), socket, client_id]
258
+ end
259
+ end
260
+ end
261
+ end
262
+
222
263
  def process_error(errors)
223
264
  Dnsruby.log.debug{"Error! #{errors.inspect}"}
224
265
  # @todo@ Process errors [can we do this in single socket environment?]
225
266
  end
226
267
 
268
+ def get_active_ids(queries, id)
269
+ queries.keys.select { |client_query_id| client_query_id[1].header.id == id }
270
+ end
271
+
227
272
  # @@query_hash[query_settings.client_query_id]=query_settings
228
- # @@socket_hash[query_settings.socket]=[query_settings.client_query_id] # @todo@ If we use persistent sockets then we need to update this array
229
273
  def process_ready(ready)
230
- ready.each do |socket|
231
- query_settings = nil
232
- @@mutex.synchronize{
233
- # Can do this if we have a query per socket, but not otherwise...
234
- c_q_id = @@socket_hash[socket][0] # @todo@ If we use persistent sockets then this won't work
235
- query_settings = @@query_hash[c_q_id]
236
- }
274
+ persistent_sockets, nonpersistent_sockets = @@mutex.synchronize { ready.partition { |socket| persistent?(socket) } }
275
+
276
+ nonpersistent_sockets.each do |socket|
277
+ query_settings = @@mutex.synchronize { @@query_hash[@@socket_hash[socket][0]] }
237
278
  next if !query_settings
279
+
238
280
  udp_packet_size = query_settings.udp_packet_size
239
281
  msg, bytes = get_incoming_data(socket, udp_packet_size)
240
- if (msg!=nil)
241
- # Check that the IP we received from was the IP we sent to!
242
- answerip = msg.answerip.downcase
243
- answerfrom = msg.answerfrom.downcase
244
- dest_server = query_settings.dest_server
245
- answeripaddr = IPAddr.new(answerip)
246
- dest_server = IPAddr.new("0.0.0.0")
247
- begin
248
- destserveripaddr = IPAddr.new(dest_server)
249
- rescue ArgumentError
250
- # Host name not IP address
251
- end
252
- if (dest_server && (dest_server != '0.0.0.0') &&
253
- (answeripaddr != destserveripaddr) &&
254
- (answerfrom != dest_server))
255
- Dnsruby.log.warn("Unsolicited response received from #{answerip} instead of #{query_settings.dest_server}")
256
- else
257
- send_response_to_client(msg, bytes, socket)
258
- end
259
- end
282
+
283
+ process_message(msg, bytes, socket) if msg
284
+
285
+ ready.delete(socket)
286
+ end
287
+
288
+ persistent_sockets.each do |socket|
289
+ msg, bytes = get_incoming_data(socket, 0)
290
+ process_message(msg, bytes, socket) if msg
260
291
  ready.delete(socket)
261
292
  end
262
293
  end
263
294
 
295
+ def process_message(msg, bytes, socket)
296
+ @@mutex.synchronize do
297
+ ids = get_active_ids(@@query_hash, msg.header.id)
298
+ return if ids.empty? # should be only one
299
+ query_settings = @@query_hash[ids[0]].clone
300
+ end
301
+
302
+ answerip = msg.answerip.downcase
303
+ answerfrom = msg.answerfrom.downcase
304
+ answeripaddr = IPAddr.new(answerip)
305
+ dest_server = IPAddr.new("0.0.0.0")
306
+
307
+ begin
308
+ destserveripaddr = IPAddr.new(dest_server)
309
+ rescue ArgumentError
310
+ # Host name not IP address
311
+ end
312
+
313
+ if (dest_server && (dest_server != '0.0.0.0') &&
314
+ (answeripaddr != destserveripaddr) &&
315
+ (answerfrom != dest_server))
316
+ Dnsruby.log.warn("Unsolicited response received from #{answerip} instead of #{query_settings.dest_server}")
317
+ else
318
+ send_response_to_client(msg, bytes, socket)
319
+ end
320
+ end
321
+
264
322
  def send_response_to_client(msg, bytes, socket)
265
323
  # Figure out which client_ids we were expecting on this socket, then see if any header ids match up
266
324
  # @TODO@ Can get rid of this, as we only have one query per socket.
267
325
  client_ids=[]
268
326
  @@mutex.synchronize{
269
- client_ids = @@socket_hash[socket]
327
+ client_ids = @@socket_hash[socket].clone
270
328
  }
271
329
  # get the queries associated with them
272
330
  client_ids.each do |id|
@@ -284,7 +342,7 @@ module Dnsruby
284
342
  res = @@query_hash[id].single_resolver
285
343
  query = @@query_hash[id].query
286
344
  }
287
- tcp = (socket.class == TCPSocket)
345
+ tcp = tcp?(socket)
288
346
  # At this point, we should check if the response is OK
289
347
  if (ret = res.check_response(msg, bytes, query, client_queue, id, tcp))
290
348
  remove_id(id)
@@ -307,30 +365,48 @@ module Dnsruby
307
365
  print("Stray packet - " + msg.question()[0].qname.to_s + " from " + msg.answerip.to_s + ", #{client_ids.length} client_ids\n")
308
366
  end
309
367
 
368
+ def persistent?(socket)
369
+ @@socket_is_persistent[socket]
370
+ end
371
+
310
372
  def remove_id(id)
311
- socket=nil
312
- @@mutex.synchronize{
373
+
374
+ @@mutex.synchronize do
313
375
  socket = @@query_hash[id].socket
314
376
  @@timeouts.delete(id)
315
377
  @@query_hash.delete(id)
316
- @@socket_hash.delete(socket)
317
- @@sockets.delete(socket) # @TODO@ Not if persistent!
318
- }
319
- Dnsruby.log.debug{"Closing socket #{socket}"}
320
- begin
321
- socket.close # @TODO@ Not if persistent!
322
- rescue IOError # Don't worry if the socket was closed already
378
+ @@socket_hash[socket].delete(id)
379
+
380
+ decrement_remaining_queries(socket) if persistent?(socket)
381
+
382
+ if !persistent?(socket) || max_attained?(socket)
383
+ @@sockets.delete(socket)
384
+ @@socket_hash.delete(socket)
385
+ Dnsruby.log.debug("Closing socket #{socket}")
386
+ socket.close rescue nil
387
+ end
323
388
  end
324
389
  end
325
390
 
391
+ def decrement_remaining_queries(socket)
392
+ if @@socket_remaining_queries[socket]
393
+ @@socket_remaining_queries[socket] -= 1
394
+ end
395
+ end
396
+
397
+ def max_attained?(socket)
398
+ remaining = @@socket_remaining_queries[socket]
399
+ attained = persistent?(socket) && remaining && remaining <= 0
400
+ Dnsruby.log.debug("Max queries per conn attained") if attained
401
+ attained
402
+ end
403
+
326
404
  def process_timeouts
405
+ # NOTE: It's @@timeouts we need to protect; after the clone we're ok
406
+ timeouts = @@mutex.synchronize { @@timeouts.clone }
327
407
  time_now = Time.now
328
- timeouts={}
329
- @@mutex.synchronize {
330
- timeouts = @@timeouts
331
- }
332
408
  timeouts.each do |client_id, timeout|
333
- if (timeout < time_now)
409
+ if timeout < time_now
334
410
  send_exception_to_client(ResolvTimeout.new("Query timed out"), nil, client_id)
335
411
  end
336
412
  end
@@ -354,8 +430,19 @@ module Dnsruby
354
430
  begin
355
431
  input, = socket.recv_nonblock(expected_length-buf.length)
356
432
  if (input=="")
357
- TheLog.info("Bad response from server - no bytes read - ignoring")
358
- # @TODO@ Should we do anything about this?
433
+ Dnsruby.log.debug("EOF from server - no bytes read - closing socket")
434
+ socket.close #EOF closed by server, if we were interrupted we need to resend
435
+
436
+ exceptions = @@mutex.synchronize do
437
+ @@sockets.delete(socket) #remove ourselves from select, app will have to retry
438
+ #maybe fire an event
439
+ @@socket_hash[socket].map do | client_id |
440
+ [SocketEofResolvError.new("TCP socket closed before all answers received"), socket, client_id]
441
+ end
442
+ end
443
+
444
+ exceptions.each { |exception| send_exception_to_client(*exception) }
445
+
359
446
  return false
360
447
  end
361
448
  buf += input
@@ -391,21 +478,10 @@ module Dnsruby
391
478
  def get_incoming_data(socket, packet_size)
392
479
  answerfrom,answerip,answerport,answersize=nil
393
480
  ans,buf = nil
394
- begin
395
- if (socket.class == TCPSocket)
396
- # @todo@ Ruby Bug #9061 stops this working right
397
- # We'd like to do a socket.recvfrom, but that raises an Exception
398
- # on Windows for TCPSocket for Ruby 1.8.5 (and 1.8.6).
399
- # So, we need to do something different for TCP than UDP. *sigh*
400
- # @TODO@ This workaround will only work if there is exactly one socket per query
401
- # - *not* ideal TCP use!
402
- @@mutex.synchronize{
403
- client_id = @@socket_hash[socket][0]
404
- answerfrom = @@query_hash[client_id].dest_server
405
- answerip = answerfrom
406
- answerport = @@query_hash[client_id].dest_port
407
- }
481
+ is_tcp = tcp?(socket)
408
482
 
483
+ begin
484
+ if is_tcp
409
485
  # Call TCP read here - that will take care of reading the 2 byte length,
410
486
  # and then the full packet - without blocking select.
411
487
  buf = tcp_read(socket)
@@ -438,8 +514,23 @@ module Dnsruby
438
514
 
439
515
  begin
440
516
  ans = Message.decode(buf)
517
+
518
+ if is_tcp
519
+ @@mutex.synchronize do
520
+ ids = get_active_ids(@@query_hash, ans.header.id)
521
+ if ids.empty?
522
+ Dnsruby.log.error("Decode error from #{answerip} but can't determine packet id")
523
+ #todo add error event? The problem is we don't have a valid id so we don't
524
+ #know which client queue to send the exception to
525
+ end
526
+ answerfrom = @@query_hash[ids[0]].dest_server
527
+ answerip = answerfrom
528
+ answerport = @@query_hash[ids[0]].dest_port
529
+ end
530
+ end
531
+
441
532
  rescue Exception => e
442
- Dnsruby.log.error{"Decode error! #{e.class}, #{e}\nfor msg (length=#{buf.length}) : #{buf}"}
533
+ Dnsruby.log.error("Decode error! #{e.class}, #{e}\nfor msg (length=#{buf.length}) : #{buf}")
443
534
  client_id=get_client_id_from_answerfrom(socket, answerip, answerport)
444
535
  if (client_id == nil)
445
536
  Dnsruby.log.error{"Decode error from #{answerip} but can't determine packet id"}