dnsruby 1.57.0 → 1.58.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -2
- data/.travis.yml +1 -1
- data/README.md +2 -2
- data/RELEASE_NOTES.md +16 -0
- data/Rakefile +2 -0
- data/dnsruby.gemspec +2 -0
- data/lib/dnsruby.rb +5 -0
- data/lib/dnsruby/bit_mapping.rb +138 -0
- data/lib/dnsruby/bitmap.rb +108 -0
- data/lib/dnsruby/message/decoder.rb +90 -80
- data/lib/dnsruby/message/message.rb +16 -3
- data/lib/dnsruby/message/section.rb +3 -3
- data/lib/dnsruby/name.rb +2 -2
- data/lib/dnsruby/packet_sender.rb +73 -1
- data/lib/dnsruby/resolv.rb +56 -42
- data/lib/dnsruby/resolver.rb +95 -73
- data/lib/dnsruby/resource/GPOS.rb +9 -3
- data/lib/dnsruby/resource/HIP.rb +1 -1
- data/lib/dnsruby/resource/IN.rb +3 -1
- data/lib/dnsruby/resource/NAPTR.rb +2 -2
- data/lib/dnsruby/resource/NSEC3.rb +2 -2
- data/lib/dnsruby/resource/NXT.rb +302 -0
- data/lib/dnsruby/resource/OPT.rb +2 -2
- data/lib/dnsruby/resource/TXT.rb +2 -2
- data/lib/dnsruby/resource/generic.rb +1 -0
- data/lib/dnsruby/resource/type_bitmap.rb +0 -0
- data/lib/dnsruby/select_thread.rb +174 -83
- data/lib/dnsruby/single_resolver.rb +2 -2
- data/lib/dnsruby/version.rb +1 -1
- data/lib/dnsruby/zone_reader.rb +19 -9
- data/lib/dnsruby/zone_transfer.rb +1 -1
- data/test/spec_helper.rb +9 -1
- data/test/tc_axfr.rb +17 -4
- data/test/tc_gpos.rb +3 -3
- data/test/tc_message.rb +7 -1
- data/test/tc_nxt.rb +192 -0
- data/test/tc_recur.rb +2 -1
- data/test/tc_resolv.rb +73 -0
- data/test/tc_resolver.rb +22 -4
- data/test/tc_rr-opt.rb +9 -8
- data/test/tc_rr.rb +22 -0
- data/test/tc_single_resolver.rb +15 -15
- data/test/tc_soak.rb +154 -65
- data/test/tc_soak_base.rb +15 -15
- data/test/tc_tcp.rb +1 -1
- data/test/tc_tcp_pipelining.rb +203 -0
- data/test/tc_zone_reader.rb +73 -0
- data/test/test_dnsserver.rb +208 -0
- data/test/test_utils.rb +49 -0
- data/test/ts_offline.rb +59 -41
- data/test/ts_online.rb +92 -63
- metadata +40 -3
- 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
|
-
|
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.
|
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.
|
58
|
+
def self.new_from_data(*gpos_params_data)
|
53
59
|
RR.new_from_data(*gpos_params_data)
|
54
60
|
end
|
55
61
|
|
data/lib/dnsruby/resource/HIP.rb
CHANGED
data/lib/dnsruby/resource/IN.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
data/lib/dnsruby/resource/OPT.rb
CHANGED
@@ -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)
|
data/lib/dnsruby/resource/TXT.rb
CHANGED
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 =
|
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
|
-
|
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]
|
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
|
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
|
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
|
-
#
|
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
|
-
#
|
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
|
215
|
-
|
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.
|
231
|
-
|
232
|
-
|
233
|
-
|
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
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
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
|
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
|
-
|
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(
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
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
|
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
|
-
|
358
|
-
#
|
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
|
-
|
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
|
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"}
|