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.
- 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"}
|