mock_dns_server 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +18 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE.txt +24 -0
  5. data/README.md +127 -0
  6. data/RELEASE_NOTES.md +3 -0
  7. data/Rakefile +19 -0
  8. data/bin/show_dig_request +41 -0
  9. data/lib/mock_dns_server.rb +12 -0
  10. data/lib/mock_dns_server/action_factory.rb +84 -0
  11. data/lib/mock_dns_server/conditional_action.rb +42 -0
  12. data/lib/mock_dns_server/conditional_action_factory.rb +53 -0
  13. data/lib/mock_dns_server/conditional_actions.rb +73 -0
  14. data/lib/mock_dns_server/dnsruby_monkey_patch.rb +19 -0
  15. data/lib/mock_dns_server/history.rb +84 -0
  16. data/lib/mock_dns_server/history_inspections.rb +58 -0
  17. data/lib/mock_dns_server/ip_address_dispenser.rb +34 -0
  18. data/lib/mock_dns_server/message_builder.rb +199 -0
  19. data/lib/mock_dns_server/message_helper.rb +86 -0
  20. data/lib/mock_dns_server/message_transformer.rb +74 -0
  21. data/lib/mock_dns_server/predicate_factory.rb +108 -0
  22. data/lib/mock_dns_server/serial_history.rb +385 -0
  23. data/lib/mock_dns_server/serial_number.rb +129 -0
  24. data/lib/mock_dns_server/serial_transaction.rb +46 -0
  25. data/lib/mock_dns_server/server.rb +422 -0
  26. data/lib/mock_dns_server/server_context.rb +57 -0
  27. data/lib/mock_dns_server/server_thread.rb +13 -0
  28. data/lib/mock_dns_server/version.rb +3 -0
  29. data/mock_dns_server.gemspec +32 -0
  30. data/spec/mock_dns_server/conditions_factory_spec.rb +58 -0
  31. data/spec/mock_dns_server/history_inspections_spec.rb +84 -0
  32. data/spec/mock_dns_server/history_spec.rb +65 -0
  33. data/spec/mock_dns_server/ip_address_dispenser_spec.rb +30 -0
  34. data/spec/mock_dns_server/message_builder_spec.rb +18 -0
  35. data/spec/mock_dns_server/predicate_factory_spec.rb +147 -0
  36. data/spec/mock_dns_server/serial_history_spec.rb +385 -0
  37. data/spec/mock_dns_server/serial_number_spec.rb +119 -0
  38. data/spec/mock_dns_server/serial_transaction_spec.rb +37 -0
  39. data/spec/mock_dns_server/server_context_spec.rb +20 -0
  40. data/spec/mock_dns_server/server_spec.rb +411 -0
  41. data/spec/mock_dns_server/socket_research_spec.rb +59 -0
  42. data/spec/spec_helper.rb +44 -0
  43. data/todo.txt +0 -0
  44. metadata +212 -0
@@ -0,0 +1,86 @@
1
+
2
+ module MockDnsServer
3
+
4
+ module MessageHelper
5
+
6
+ MESSAGE_LENGTH_PACK_UNPACK_FORMAT = 'n'
7
+
8
+ # If the string can convert to a Dnsruby::Message without throwing an exception,
9
+ # return the Dnsruby::Message instance; else, return the original string.
10
+ def self.to_dns_message(object)
11
+ case object
12
+ when String
13
+ begin
14
+ Dnsruby::Message.decode(object)
15
+ rescue
16
+ object
17
+ end
18
+ when Dnsruby::Message
19
+ object
20
+ end
21
+ end
22
+
23
+
24
+ def self.convertible_to_dnsruby_message?(object)
25
+ to_dns_message(object).is_a?(Dnsruby::Message)
26
+ end
27
+
28
+
29
+
30
+ # Builds a string for a TCP client to send to a DNS server
31
+ #
32
+ # @param message, either a DNS message or a string
33
+ # @return if message is a Dnsruby::Message, returns the wire_data prepended with the 2-byte size field
34
+ # else returns the message unchanged
35
+ def self.tcp_message_package_for_write(message)
36
+ message = message.encode if message.is_a?(Dnsruby::Message)
37
+ size_field = [message.size].pack(MESSAGE_LENGTH_PACK_UNPACK_FORMAT)
38
+ size_field + message
39
+ end
40
+
41
+
42
+ def self.udp_message_package_for_write(object)
43
+ case object
44
+ when Dnsruby::Message
45
+ object.encode
46
+ when String
47
+ object
48
+ end
49
+ end
50
+
51
+
52
+ # Reads a message from a TCP connection. First gets the 2 byte length, then reads the payload.
53
+ # Attempts to convert the payload into a Dnsruby::Message.
54
+ def self.read_tcp_message(socket)
55
+
56
+ message_len_str = socket.read(2)
57
+ raise "Unable to read from socket; read returned nil" if message_len_str.nil?
58
+ message_len = message_len_str.unpack(MESSAGE_LENGTH_PACK_UNPACK_FORMAT).first
59
+
60
+ bytes_not_yet_read = message_len
61
+ message_wire_data = ''
62
+
63
+ while bytes_not_yet_read > 0
64
+ str = socket.read(bytes_not_yet_read)
65
+ bytes_not_yet_read -= str.size
66
+ message_wire_data << str
67
+ end
68
+
69
+ message = MessageHelper.to_dns_message(message_wire_data)
70
+ message
71
+ end
72
+
73
+
74
+ # Sends a UDP message and returns the response, using a temporary socket.
75
+ def self.send_udp_and_get_response(message, host, port)
76
+ socket = UDPSocket.new
77
+ message = message.encode if message.is_a?(Dnsruby::Message)
78
+ socket.send(message, 0, host, port)
79
+ _, _, _ = IO.select([socket], nil, nil)
80
+ response_data, _ = socket.recvfrom(10_000)
81
+ response = to_dns_message(response_data)
82
+ socket.close
83
+ response
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,74 @@
1
+ module MockDnsServer
2
+
3
+ # Lambdas that transform a message into something else, usually a message component such as domain or qtype.
4
+ class MessageTransformer
5
+
6
+ attr_reader :message
7
+
8
+ # Initialize the transformer with a message.
9
+ # @param dns_message can be either a Dnsruby::Message instance or binary wire data
10
+ def initialize(dns_message)
11
+ self.message = dns_message
12
+ end
13
+
14
+
15
+ def message=(dns_message)
16
+ @message = dns_message.is_a?(String) ? Dnsruby::Message.decode(dns_message) : dns_message
17
+ end
18
+
19
+
20
+ # A SOA record is usually in the answer section, but in the case of IXFR requests
21
+ # it will be in the authority section.
22
+ #
23
+ # @location defaults to :answer, can override w/:authority
24
+ def serial(location = :answer)
25
+ return nil if message.nil?
26
+
27
+ target_section = message.send(location == :answer ? :answer : :authority)
28
+ return nil if target_section.nil?
29
+
30
+ soa_answer = target_section.detect { |record| record.is_a?(Dnsruby::RR::IN::SOA) }
31
+ soa_answer ? soa_answer.serial : nil
32
+ end
33
+
34
+
35
+ # @return the message's qtype as a String
36
+ def qtype
37
+ dnsruby_type_instance = question_attr(:qtype)
38
+ Dnsruby::Types.to_string(dnsruby_type_instance)
39
+ end
40
+
41
+
42
+ # @return the message's qname as a String
43
+ def qname
44
+ question_attr(:qname)
45
+ end
46
+
47
+
48
+ # @return the message's qclass as a String
49
+ def qclass
50
+ question_attr(:qclass)
51
+ end
52
+
53
+
54
+ def question_attr(symbol)
55
+ question = first_question
56
+ question ? question.send(symbol).to_s : nil
57
+ end
58
+
59
+
60
+ def first_question
61
+ has_question = message &&
62
+ message.question &&
63
+ message.question.first &&
64
+ message.question.first.is_a?(Dnsruby::Question)
65
+
66
+ has_question ? message.question.first : nil
67
+ end
68
+
69
+
70
+ def answer_count(answer_type)
71
+ message.answer.select { |a| a.rr_type.to_s == answer_type}.count
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,108 @@
1
+ require 'mock_dns_server/message_transformer'
2
+
3
+ module MockDnsServer
4
+
5
+ # Each method returns a predicate in the form of a lambda or proc that takes an
6
+ # incoming object (usually a Dnsruby::Message) object as a parameter and returns
7
+ # (as predicates do) true or false.
8
+ class PredicateFactory
9
+
10
+ # shorthand for the MessageTransformers instance
11
+ def mt(message)
12
+ MessageTransformer.new(message)
13
+ end
14
+
15
+ # TODO: Case insensitive?
16
+ # TODO: Add sender to signature for tcp/udp, etc.?
17
+
18
+ def all(*predicates)
19
+ ->(message, protocol = nil) do
20
+ predicates.all? { |p| p.call(message, protocol) }
21
+ end
22
+ end
23
+
24
+ def any(*predicates)
25
+ ->(message, protocol = nil) do
26
+ predicates.any? { |p| p.call(message, protocol) }
27
+ end
28
+ end
29
+
30
+ def none(*predicates)
31
+ ->(message, protocol = nil) do
32
+ predicates.none? { |p| p.call(message, protocol) }
33
+ end
34
+ end
35
+
36
+ def dns
37
+ ->(message, _ = nil) { message.is_a?(Dnsruby::Message) }
38
+ end
39
+
40
+ def soa
41
+ qtype('SOA')
42
+ end
43
+
44
+ def ixfr
45
+ qtype('IXFR')
46
+ end
47
+
48
+ def axfr
49
+ qtype('AXFR')
50
+ end
51
+
52
+ def xfr
53
+ any(axfr, ixfr)
54
+ end
55
+
56
+ # Returns true for messages relating to data from the zone load.
57
+ def zone_load
58
+ any(xfr, soa)
59
+ end
60
+
61
+ # Convenience method for testing for a specific qtype and qname.
62
+ def qtype_and_qname(qtype, qname)
63
+ all(qtype(qtype), qname(qname))
64
+ end
65
+
66
+ # Convenience method for testing a request of qtype 'A' with the given qname.
67
+ def a_request(qname)
68
+ qtype_and_qname('A', qname)
69
+ end
70
+
71
+ def qtype(qtype)
72
+ ->(message, _ = nil) do
73
+ dns.(message) && eq_case_insensitive(mt(message).qtype, qtype)
74
+ end
75
+ end
76
+
77
+ def qclass(qclass)
78
+ ->(message, _ = nil) { dns.(message) && eq_case_insensitive(mt(message).qclass, qclass) }
79
+ end
80
+
81
+ def qname(qname)
82
+ ->(message, _ = nil) { dns.(message) && eq_case_insensitive(mt(message).qname, qname) }
83
+ end
84
+
85
+ def from_tcp
86
+ ->(_, protocol) { protocol == :tcp }
87
+ end
88
+
89
+ def from_udp
90
+ ->(_, protocol) { protocol == :udp }
91
+ end
92
+
93
+ def always_true
94
+ ->(_, _) { true }
95
+ end
96
+
97
+ def always_false
98
+ ->(_, _) { false }
99
+ end
100
+
101
+ private
102
+
103
+ def eq_case_insensitive(s1, s2)
104
+ s1.downcase == s2.downcase
105
+ end
106
+
107
+ end
108
+ end
@@ -0,0 +1,385 @@
1
+ require 'thread_safe'
2
+ require 'mock_dns_server/serial_transaction'
3
+
4
+ module MockDnsServer
5
+
6
+ # Manages RR additions and deletions for multiple serials,
7
+ # and builds responses to AXFR and IXFR requests.
8
+ class SerialHistory
9
+
10
+ attr_accessor :zone, :low_serial, :ixfr_response_uses_axfr_style
11
+
12
+ # Creates the instance.
13
+ # @param zone
14
+ # @param start_serial the serial of the data set provided in the initial_records
15
+ # @param initial_records the starting data
16
+ # @param ixfr_response_uses_axfr_style when to respond to an IXFR request with an AXFR-style IXFR,
17
+ # rather than an IXFR list of changes. Regardless of this option,
18
+ # if the requested serial >= the last known serial of this history,
19
+ # a response with a single SOA record containing the highest known serial will be sent.
20
+ # The following options apply to any other case, and are:
21
+ #
22
+ # :never (default) - always return IXFR-style, but
23
+ # if the requested serial is not known by the server
24
+ # (i.e. if it is *not* the serial of one of the transactions in the history),
25
+ # then return 'transfer failed' rcode
26
+ # :always - always return AXFR-style
27
+ # :auto - if the requested serial is known by the server (i.e. if it is
28
+ # the serial of one of the transactions in the history,
29
+ # or is the initial serial of the history), then return an IXFR list;
30
+ # otherwise return an AXFR list.
31
+ # Note that even when an AXFR-style list is returned, it is still an IXFR
32
+ # response -- that is, the IXFR question from the query is copied into the response.
33
+ def initialize(zone, start_serial, initial_records = [], ixfr_response_uses_axfr_style = :never)
34
+ @zone = zone
35
+ @low_serial = SerialNumber.object(start_serial)
36
+ @initial_records = initial_records
37
+ self.ixfr_response_uses_axfr_style = ixfr_response_uses_axfr_style
38
+ @txns = ThreadSafe::Hash.new # txns is an abbreviation of transactions
39
+ end
40
+
41
+ def ixfr_response_uses_axfr_style=(mode)
42
+
43
+ validate_input = ->() do
44
+ valid_modes = [:never, :always, :auto]
45
+ unless valid_modes.include?(mode)
46
+ valid_modes_as_string = valid_modes.map(&:inspect).join(', ')
47
+ raise "ixfr_response_uses_axfr_style mode must be one of the following: #{valid_modes_as_string}"
48
+ end
49
+ end
50
+
51
+ validate_input.()
52
+ @ixfr_response_uses_axfr_style = mode
53
+ end
54
+
55
+ def set_serial_additions(serial, additions)
56
+ serial = SerialNumber.object(serial)
57
+ additions = Array(additions)
58
+ serial_transaction(serial).additions = additions
59
+ self
60
+ end
61
+
62
+ def serial_additions(serial)
63
+ serial = SerialNumber.object(serial)
64
+ @txns[serial] ? @txns[serial].additions : nil
65
+ end
66
+
67
+ def set_serial_deletions(serial, deletions)
68
+ serial = SerialNumber.object(serial)
69
+ deletions = Array(deletions)
70
+ serial_transaction(serial).deletions = deletions
71
+ self
72
+ end
73
+
74
+ def serial_deletions(serial)
75
+ serial = SerialNumber.object(serial)
76
+ @txns[serial] ? @txns[serial].deletions : nil
77
+ end
78
+
79
+ def txn_serials
80
+ @txns.keys
81
+ end
82
+
83
+ def serials
84
+ [low_serial] + txn_serials
85
+ end
86
+
87
+ def high_serial
88
+ txn_serials.empty? ? low_serial : txn_serials.last
89
+ end
90
+
91
+ def to_s
92
+ "#{self.class.name}: zone: #{zone}, initial serial: #{low_serial}, high_serial: #{high_serial}, records:\n#{ixfr_records}\n"
93
+ end
94
+
95
+ # Although Dnsruby has a <=> operator on RR's, we need a comparison that looks only
96
+ # at the type, name, and rdata (and not the TTL, for example), for purposes of
97
+ # detecting records that need be deleted.
98
+ def rr_compare(rr1, rr2)
99
+
100
+ rrs = [rr1, rr2]
101
+
102
+ name1, name2 = rrs.map { |rr| rr.name.to_s.downcase }
103
+ if name1 != name2
104
+ return name1 > name2 ? 1 : -1
105
+ end
106
+
107
+ type1, type2 = rrs.map { |rr| rr.type.to_s.downcase }
108
+ if type1 != type2
109
+ return type1 > type2 ? 1 : -1
110
+ end
111
+
112
+ rdata1, rdata2 = rrs.map(&:rdata)
113
+ if rdata1 != rdata2
114
+ rdata1 > rdata2 ? 1 : -1
115
+ else
116
+ 0
117
+ end
118
+ end
119
+
120
+
121
+ def rr_equivalent(rr1, rr2)
122
+ rr_compare(rr1, rr2) == 0
123
+ end
124
+
125
+
126
+ # @return a snapshot array of the data as of a given serial number
127
+ # @serial if a number, must be in the range of known serials
128
+ # if :current, the highest known serial will be used
129
+ def data_at_serial(serial)
130
+
131
+ serial = high_serial if serial == :current
132
+ serial = SerialNumber.object(serial)
133
+
134
+ if serial.nil? || serial > high_serial || serial < low_serial
135
+ raise "Serial must be in range #{low_serial} to #{high_serial} inclusive."
136
+ end
137
+ data = @initial_records.clone
138
+
139
+ txn_serials.each do |key|
140
+ txn = @txns[key]
141
+ break if txn.serial > serial
142
+ txn.deletions.each do |d|
143
+ data.reject! { |rr| rr_equivalent(rr, d) }
144
+ end
145
+ txn.additions.each do |a|
146
+ data.reject! { |rr| rr_equivalent(rr, a) }
147
+ data << a
148
+ end
149
+ end
150
+
151
+ data
152
+ end
153
+
154
+ def current_data
155
+ data_at_serial(:current)
156
+ end
157
+
158
+ def high_serial_soa_rr
159
+ MessageBuilder.soa_answer(name: zone, serial: high_serial)
160
+ end
161
+
162
+ def axfr_records
163
+ [high_serial_soa_rr, current_data, high_serial_soa_rr].flatten
164
+ end
165
+
166
+
167
+ # Finds the serial previous to that of this transaction.
168
+ # @return If txn is the first txn, returns start_serial of the history
169
+ # else the serial of the previous transaction
170
+ def previous_serial(serial)
171
+ serial = SerialNumber.object(serial)
172
+ return nil if serial <= low_serial || serial > high_serial
173
+
174
+ txn_index = txn_serials.find_index(serial)
175
+ txn_index > 0 ? txn_serials[txn_index - 1] : @low_serial
176
+ end
177
+
178
+
179
+ # @return an array of RR's that can be used to populate an IXFR response.
180
+ # @base_serial the serial from which to start when building the list of changes
181
+ def ixfr_records(base_serial = nil)
182
+ base_serial = SerialNumber.object(base_serial)
183
+
184
+ records = []
185
+ records << high_serial_soa_rr
186
+
187
+ serials = @txns.keys
188
+
189
+ # Note that the serials in the data structure are the 'to' serials,
190
+ # whereas the serial of this request will be the 'from' serial.
191
+ # To compensate for this, we take the first serial *after* the
192
+ # occurrence of base_serial in the array of serials, thus the +1 below.
193
+ index_minus_one = serials.find_index(base_serial)
194
+ index_is_index_other_than_last_index = index_minus_one && index_minus_one < serials.size - 1
195
+
196
+ base_serial_index = index_is_index_other_than_last_index ? index_minus_one + 1 : 0
197
+
198
+ serials_to_process = serials[base_serial_index..-1]
199
+ serials_to_process.each do |serial|
200
+ txn = @txns[serial]
201
+ txn_records = txn.ixfr_records(previous_serial(serial))
202
+ txn_records.each { |rec| records << rec }
203
+ end
204
+
205
+ records << high_serial_soa_rr
206
+ records
207
+ end
208
+
209
+
210
+ # Determines whether a given record array is AXFR- or IXFR-style.
211
+ # @param records array of IXFR or AXFR records
212
+ # @return :ixfr, :axfr, :error
213
+ def xfr_array_type(records)
214
+ begin
215
+ for num_consecutive_soas in (0..records.size)
216
+ break unless records[num_consecutive_soas].is_a?(Dnsruby::RR::SOA)
217
+ end
218
+ case num_consecutive_soas
219
+ when nil; :error
220
+ when 0; :error
221
+ when 1; :axfr
222
+ else; :ixfr
223
+ end
224
+ rescue => e
225
+ :error
226
+ end
227
+ end
228
+
229
+
230
+ def is_tracked_serial(serial)
231
+ serial = SerialNumber.object(serial)
232
+ serials.include?(serial)
233
+ end
234
+
235
+
236
+ # Returns the next serial value that could be added to the history,
237
+ # i.e. the successor to the highest serial we now have.
238
+ def next_serial_value
239
+ SerialNumber.next_serial_value(high_serial.to_i)
240
+ end
241
+
242
+
243
+ # When handling an IXFR request, use the following logic:
244
+ #
245
+ # if the serial number requested >= the current serial number (highest_serial),
246
+ # return a single SOA record (at the current serial number).
247
+ #
248
+ # Otherwise, given the current value of ixfr_response_uses_axfr_style:
249
+ #
250
+ # :always - always return an AXFR-style IXFR response
251
+ #
252
+ # :never (default) - if we have that serial in our history, return an IXFR response,
253
+ # else return a Transfer Failed error message
254
+ #
255
+ # :auto - if we have that serial in our history, return an IXFR response,
256
+ # else return an AXFR style response.
257
+ #
258
+ # @return the type of response appropriate to this serial and request
259
+ def ixfr_response_style(serial)
260
+ serial = SerialNumber.object(serial)
261
+
262
+ if serial >= high_serial
263
+ :single_soa
264
+ else
265
+ case ixfr_response_uses_axfr_style
266
+ when :never
267
+ is_tracked_serial(serial) ? :ixfr : :xfer_failed
268
+ when :auto
269
+ is_tracked_serial(serial) ? :ixfr : :axfr_style_ixfr
270
+ when :always
271
+ :axfr_style_ixfr
272
+ end
273
+ end
274
+ end
275
+
276
+
277
+ # Creates a response message based on the type and serial of the incoming message.
278
+ # @param incoming_message an AXFR or IXFR request
279
+ # @return a Dnsruby message containing the response, either or AXFR or IXFR
280
+ def xfr_response(incoming_message)
281
+
282
+ mt = MessageTransformer.new(incoming_message)
283
+ query_zone = mt.qname
284
+ query_type = mt.qtype.downcase.to_sym # :axfr or :ixfr
285
+ query_serial = mt.serial(:authority) # ixfr requests only, else will be nil
286
+
287
+ validate_inputs = ->() {
288
+ if query_zone.downcase != zone.downcase
289
+ raise "Query zone (#{query_zone}) differs from history zone (#{zone})."
290
+ end
291
+
292
+ unless [:axfr, :ixfr].include?(query_type)
293
+ raise "Invalid qtype (#{query_type}), must be AXFR or IXFR."
294
+ end
295
+
296
+ if query_type == :ixfr && query_serial.nil?
297
+ raise 'IXFR request did not specify serial in authority section.'
298
+ end
299
+ }
300
+
301
+ build_standard_response = ->(rrs = nil) do
302
+ response = Dnsruby::Message.new
303
+ response.header.qr = true
304
+ response.header.aa = true
305
+ rrs.each { |record| response.add_answer!(record) } if rrs
306
+ incoming_message.question.each { |q| response.add_question(q) }
307
+ response
308
+ end
309
+
310
+ build_error_response = ->() {
311
+ response = build_standard_response.()
312
+ response.header.rcode = Dnsruby::RCode::REFUSED
313
+ response
314
+ }
315
+
316
+ build_single_soa_response = ->() {
317
+ build_standard_response.([high_serial_soa_rr])
318
+ }
319
+
320
+ validate_inputs.()
321
+ xfr_response = nil
322
+
323
+ case query_type
324
+
325
+ when :axfr
326
+ xfr_response = build_standard_response.(axfr_records)
327
+ when :ixfr
328
+ response_style = ixfr_response_style(query_serial)
329
+
330
+ case response_style
331
+ when :axfr_style_ixfr
332
+ xfr_response = build_standard_response.(axfr_records)
333
+ when :ixfr
334
+ xfr_response = build_standard_response.(ixfr_records(query_serial))
335
+ when :single_soa
336
+ xfr_response = build_single_soa_response.()
337
+ when :error
338
+ xfr_response = build_error_response.()
339
+ end
340
+ end
341
+
342
+ xfr_response
343
+ end
344
+
345
+ private
346
+
347
+ # Checks to see that a new serial whose transactions will be added to the history
348
+ # has a valid serial value in the context of the data already there.
349
+ # Raises an error if the serial is bad, else does nothing.
350
+ def check_new_serial(new_serial)
351
+ if new_serial < low_serial
352
+ raise "New serial of #{new_serial} must not be lower than initial serial of #{low_serial}."
353
+ elsif new_serial < high_serial
354
+ raise "New serial of #{new_serial} must not be lower than highest preexisting serial of #{high_serial}."
355
+ end
356
+ end
357
+
358
+
359
+ # Returns the SerialTransaction instance associated with this serial value,
360
+ # creating it if it does not already exist.
361
+ def serial_transaction(serial)
362
+ unless @txns[serial]
363
+ check_new_serial(serial)
364
+ @txns[serial] ||= SerialTransaction.new(zone, serial)
365
+
366
+ # As long as we prohibit adding serials out of order, there is no need for this:
367
+ # recreate_hash
368
+ end
369
+
370
+ @txns[serial]
371
+ end
372
+
373
+
374
+ # Recreates the hash so that its keys are in ascending order.
375
+ # Currently (12/18/2013) this is redundant, since serials must be added
376
+ # in ascending order.
377
+ #def recreate_hash
378
+ # keys = @txns.keys
379
+ # new_hash = ThreadSafe::Hash.new
380
+ # keys.sort.each { |key| new_hash[key] = @txns[key] }
381
+ # @txns = new_hash
382
+ #end
383
+
384
+ end
385
+ end