mock_dns_server 0.3.0

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