dnsruby 1.57.0 → 1.58.0

Sign up to get free protection for your applications and to get access to all the features.
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"}