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.
- checksums.yaml +7 -0
- data/.gitignore +18 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +24 -0
- data/README.md +127 -0
- data/RELEASE_NOTES.md +3 -0
- data/Rakefile +19 -0
- data/bin/show_dig_request +41 -0
- data/lib/mock_dns_server.rb +12 -0
- data/lib/mock_dns_server/action_factory.rb +84 -0
- data/lib/mock_dns_server/conditional_action.rb +42 -0
- data/lib/mock_dns_server/conditional_action_factory.rb +53 -0
- data/lib/mock_dns_server/conditional_actions.rb +73 -0
- data/lib/mock_dns_server/dnsruby_monkey_patch.rb +19 -0
- data/lib/mock_dns_server/history.rb +84 -0
- data/lib/mock_dns_server/history_inspections.rb +58 -0
- data/lib/mock_dns_server/ip_address_dispenser.rb +34 -0
- data/lib/mock_dns_server/message_builder.rb +199 -0
- data/lib/mock_dns_server/message_helper.rb +86 -0
- data/lib/mock_dns_server/message_transformer.rb +74 -0
- data/lib/mock_dns_server/predicate_factory.rb +108 -0
- data/lib/mock_dns_server/serial_history.rb +385 -0
- data/lib/mock_dns_server/serial_number.rb +129 -0
- data/lib/mock_dns_server/serial_transaction.rb +46 -0
- data/lib/mock_dns_server/server.rb +422 -0
- data/lib/mock_dns_server/server_context.rb +57 -0
- data/lib/mock_dns_server/server_thread.rb +13 -0
- data/lib/mock_dns_server/version.rb +3 -0
- data/mock_dns_server.gemspec +32 -0
- data/spec/mock_dns_server/conditions_factory_spec.rb +58 -0
- data/spec/mock_dns_server/history_inspections_spec.rb +84 -0
- data/spec/mock_dns_server/history_spec.rb +65 -0
- data/spec/mock_dns_server/ip_address_dispenser_spec.rb +30 -0
- data/spec/mock_dns_server/message_builder_spec.rb +18 -0
- data/spec/mock_dns_server/predicate_factory_spec.rb +147 -0
- data/spec/mock_dns_server/serial_history_spec.rb +385 -0
- data/spec/mock_dns_server/serial_number_spec.rb +119 -0
- data/spec/mock_dns_server/serial_transaction_spec.rb +37 -0
- data/spec/mock_dns_server/server_context_spec.rb +20 -0
- data/spec/mock_dns_server/server_spec.rb +411 -0
- data/spec/mock_dns_server/socket_research_spec.rb +59 -0
- data/spec/spec_helper.rb +44 -0
- data/todo.txt +0 -0
- metadata +212 -0
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
require 'thread_safe'
|
3
|
+
|
4
|
+
module MockDnsServer
|
5
|
+
|
6
|
+
class ConditionalActions
|
7
|
+
|
8
|
+
attr_reader :context
|
9
|
+
|
10
|
+
extend Forwardable
|
11
|
+
|
12
|
+
def_delegators :@context, :port, :history, :verbose
|
13
|
+
|
14
|
+
def initialize(context)
|
15
|
+
@context = context
|
16
|
+
@records = ThreadSafe::Array.new
|
17
|
+
end
|
18
|
+
|
19
|
+
|
20
|
+
def find_conditional_action(request, protocol)
|
21
|
+
@records.detect { |cond_action| cond_action.condition.call(request, protocol) }
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
def respond_to(request, sender, protocol)
|
26
|
+
conditional_action = find_conditional_action(request, protocol)
|
27
|
+
|
28
|
+
if conditional_action
|
29
|
+
puts 'Found action' if verbose
|
30
|
+
history.add_incoming(request, sender, protocol, conditional_action.description)
|
31
|
+
conditional_action.run(request, sender, context, protocol)
|
32
|
+
puts 'Completed action' if verbose
|
33
|
+
conditional_action.increment_use_count
|
34
|
+
handle_use_count(conditional_action)
|
35
|
+
else
|
36
|
+
puts 'Action not found' if verbose
|
37
|
+
history.add_action_not_found(request)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
def handle_use_count(conditional_action)
|
43
|
+
max_uses = conditional_action.max_uses
|
44
|
+
we_care = max_uses && max_uses > 0
|
45
|
+
if we_care && conditional_action.use_count >= max_uses
|
46
|
+
history.add_conditional_action_removal(conditional_action.description)
|
47
|
+
@records.delete(conditional_action)
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
def add(conditional_action)
|
53
|
+
# Place new record at beginning of array, so that the most recently
|
54
|
+
# added records are found first.
|
55
|
+
@records.unshift(conditional_action)
|
56
|
+
end
|
57
|
+
|
58
|
+
|
59
|
+
def remove(conditional_action)
|
60
|
+
@records.delete(conditional_action)
|
61
|
+
end
|
62
|
+
|
63
|
+
|
64
|
+
def size
|
65
|
+
@records.size
|
66
|
+
end
|
67
|
+
|
68
|
+
|
69
|
+
def empty?
|
70
|
+
size == 0
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'dnsruby'
|
2
|
+
|
3
|
+
# When adding an RR to a Dnsruby::Message, add_answer checks to see if it already occurs,
|
4
|
+
# and, if so, does not add it again. We need to disable this behavior so that we can
|
5
|
+
# add a SOA record twice for an AXFR response. So we implement add_answer!,
|
6
|
+
# similar to add_answer except that it does not do the inclusion check.
|
7
|
+
|
8
|
+
module Dnsruby
|
9
|
+
class Message
|
10
|
+
|
11
|
+
def add_answer!(rr) #:nodoc: all
|
12
|
+
#if (!@answer.include?rr)
|
13
|
+
@answer << rr
|
14
|
+
update_counts
|
15
|
+
#end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
@@ -0,0 +1,84 @@
|
|
1
|
+
module MockDnsServer
|
2
|
+
|
3
|
+
# Handles the history of events for this server.
|
4
|
+
class History
|
5
|
+
|
6
|
+
def initialize(context)
|
7
|
+
@context = context
|
8
|
+
@records = ThreadSafe::Array.new
|
9
|
+
end
|
10
|
+
|
11
|
+
|
12
|
+
def size
|
13
|
+
@records.size
|
14
|
+
end
|
15
|
+
|
16
|
+
|
17
|
+
def add(entry_hash)
|
18
|
+
entry_hash[:time] ||= Time.now
|
19
|
+
@records << entry_hash
|
20
|
+
entry_hash
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
def add_incoming(message, sender, protocol, description = nil)
|
25
|
+
add( {
|
26
|
+
type: :incoming,
|
27
|
+
message: message,
|
28
|
+
sender: sender,
|
29
|
+
protocol: protocol,
|
30
|
+
description: description
|
31
|
+
})
|
32
|
+
end
|
33
|
+
|
34
|
+
|
35
|
+
def add_action_not_found(message)
|
36
|
+
add( {
|
37
|
+
type: :action_not_found,
|
38
|
+
message: message
|
39
|
+
})
|
40
|
+
end
|
41
|
+
|
42
|
+
|
43
|
+
def add_conditional_action_removal(conditional_action)
|
44
|
+
add( {
|
45
|
+
type: :conditional_action_removal,
|
46
|
+
conditional_action: conditional_action
|
47
|
+
})
|
48
|
+
end
|
49
|
+
|
50
|
+
|
51
|
+
def add_notify_response(response, zts_host, zts_port, protocol)
|
52
|
+
add( {
|
53
|
+
type: :notify_response,
|
54
|
+
message: response,
|
55
|
+
host: zts_host,
|
56
|
+
port: zts_port,
|
57
|
+
protocol: protocol,
|
58
|
+
description: "notify response from #{zts_host}:#{zts_port}"
|
59
|
+
})
|
60
|
+
end
|
61
|
+
|
62
|
+
|
63
|
+
def occurred?(inspection)
|
64
|
+
HistoryInspections.new.apply(@records, inspection).size > 0
|
65
|
+
end
|
66
|
+
|
67
|
+
|
68
|
+
# @return a clone of the array
|
69
|
+
def to_a
|
70
|
+
@records.clone
|
71
|
+
end
|
72
|
+
|
73
|
+
|
74
|
+
def copy
|
75
|
+
@records.map { |record| record.clone }
|
76
|
+
end
|
77
|
+
|
78
|
+
|
79
|
+
def to_s
|
80
|
+
"#{super}: #{@records}"
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'mock_dns_server/message_transformer'
|
2
|
+
|
3
|
+
module MockDnsServer
|
4
|
+
|
5
|
+
class HistoryInspections
|
6
|
+
|
7
|
+
MT = MessageTransformer
|
8
|
+
|
9
|
+
def type(type)
|
10
|
+
->(record) { record[:type] == type }
|
11
|
+
end
|
12
|
+
|
13
|
+
def qtype(qtype)
|
14
|
+
->(record) do
|
15
|
+
qtype_in_message = MT.new(record[:message]).qtype.to_s
|
16
|
+
qtype_in_message == qtype
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def qname(qname)
|
21
|
+
->(record) do
|
22
|
+
qname_in_message = MT.new(record[:message]).qname.to_s
|
23
|
+
qname_in_message == qname
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def soa
|
28
|
+
qtype('SOA')
|
29
|
+
end
|
30
|
+
|
31
|
+
def protocol(protocol)
|
32
|
+
->(record) { record[:protocol] == protocol }
|
33
|
+
end
|
34
|
+
|
35
|
+
def all(*inspections)
|
36
|
+
->(record) do
|
37
|
+
inspections.all? { |inspection| inspection.(record) }
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def any(*inspections)
|
42
|
+
->(record) do
|
43
|
+
inspections.any? { |inspection| inspection.(record) }
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def none(*inspections)
|
48
|
+
->(record) do
|
49
|
+
inspections.none? { |inspection| inspection.(record) }
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def apply(records, inspection)
|
54
|
+
records.select { |record| inspection.(record) }
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'ipaddr'
|
2
|
+
require 'dnsruby'
|
3
|
+
|
4
|
+
module MockDnsServer
|
5
|
+
|
6
|
+
# Wraps Ruby's IPAddr class and yields successive IP addresses.
|
7
|
+
# Instantiate it with a starting address (IPV4 or IPV6), and when
|
8
|
+
# you call .next, it will provide addresses that increment by 1
|
9
|
+
# or optional step parameter, starting with your initial address.
|
10
|
+
#
|
11
|
+
# We'll probably want to instruct it to be more intelligent, e.g. skip .0 and .255.
|
12
|
+
class IpAddressDispenser
|
13
|
+
|
14
|
+
# @param initial_address first address dispensed, and basis for subsequent 'next' calls
|
15
|
+
def initialize(initial_address = '192.168.1.1')
|
16
|
+
@initial_address = initial_address
|
17
|
+
end
|
18
|
+
|
19
|
+
|
20
|
+
# @param step number of addresses to step in each next call, defaults to 1
|
21
|
+
def next(step = 1)
|
22
|
+
step.times do
|
23
|
+
if @address.nil?
|
24
|
+
@address = IPAddr.new(@initial_address)
|
25
|
+
elsif @address.to_s == '255.255.255.255'
|
26
|
+
@address = IPAddr.new('1.1.1.1')
|
27
|
+
else
|
28
|
+
@address = @address.succ
|
29
|
+
end
|
30
|
+
end
|
31
|
+
@address.to_s
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,199 @@
|
|
1
|
+
require 'ipaddr'
|
2
|
+
|
3
|
+
require 'dnsruby'
|
4
|
+
require 'mock_dns_server/dnsruby_monkey_patch'
|
5
|
+
require 'mock_dns_server/ip_address_dispenser'
|
6
|
+
require 'mock_dns_server/serial_number'
|
7
|
+
|
8
|
+
module MockDnsServer
|
9
|
+
|
10
|
+
module MessageBuilder
|
11
|
+
|
12
|
+
module_function
|
13
|
+
|
14
|
+
#def soa_response(zone, serial, expire = nil, refresh = nil)
|
15
|
+
#
|
16
|
+
# options = {
|
17
|
+
# type: 'SOA', klass: 'IN', name: zone, ttl: 3600, serial: serial
|
18
|
+
# }
|
19
|
+
# options[:refresh] = refresh if refresh
|
20
|
+
# options[:expire] = expire if expire
|
21
|
+
#
|
22
|
+
#
|
23
|
+
# response = Dnsruby::Message.new
|
24
|
+
#
|
25
|
+
# answer = Dnsruby::RR.new_from_hash(options)
|
26
|
+
# response.add_answer(answer)
|
27
|
+
#
|
28
|
+
# response
|
29
|
+
#
|
30
|
+
# Additional options:
|
31
|
+
# @mname = Name.create(hash[:mname])
|
32
|
+
# @rname = Name.create(hash[:rname])
|
33
|
+
# @serial = hash[:serial].to_i
|
34
|
+
# @refresh = hash[:refresh].to_i
|
35
|
+
# @retry = hash[:retry].to_i
|
36
|
+
# @expire = hash[:expire].to_i
|
37
|
+
# @minimum = hash[:minimum].to_i
|
38
|
+
#end
|
39
|
+
|
40
|
+
|
41
|
+
# Builds a response to an 'A' request from the encoded string passed.
|
42
|
+
# @param answer_string string in any format supported by Dnsruby::RR.create
|
43
|
+
# (see resource.rb, e.g. https://github.com/vertis/dnsruby/blob/master/lib/Dnsruby/resource/resource.rb,
|
44
|
+
# line 643 ff at the time of this writing):
|
45
|
+
#
|
46
|
+
# a = Dnsruby::RR.create("foo.example.com. 86400 A 10.1.2.3")
|
47
|
+
def specified_a_response(answer_string)
|
48
|
+
message = Dnsruby::Message.new
|
49
|
+
message.header.qr = true
|
50
|
+
answer = Dnsruby::RR.create(answer_string)
|
51
|
+
message.add_answer(answer)
|
52
|
+
message
|
53
|
+
end
|
54
|
+
|
55
|
+
|
56
|
+
def dummy_a_response(record_count, domain, ttl = 86400)
|
57
|
+
ip_dispenser = IpAddressDispenser.new
|
58
|
+
message = Dnsruby::Message.new
|
59
|
+
message.header.qr = true
|
60
|
+
|
61
|
+
record_count.times do
|
62
|
+
answer = Dnsruby::RR.new_from_hash(
|
63
|
+
name: domain, ttl: ttl, type: 'A', address: ip_dispenser.next, klass: 'IN')
|
64
|
+
message.add_answer(answer)
|
65
|
+
end
|
66
|
+
message
|
67
|
+
end
|
68
|
+
|
69
|
+
|
70
|
+
# Gets the serial value from the passed object; if it's a SerialNumber,
|
71
|
+
# calls its value method; if not, we assume it's a number and it's returned unchanged.
|
72
|
+
def serial_value(serial)
|
73
|
+
serial.is_a?(SerialNumber) ? serial.value : serial
|
74
|
+
end
|
75
|
+
|
76
|
+
|
77
|
+
def soa_request(name)
|
78
|
+
Dnsruby::Message.new(name, 'SOA', 'IN')
|
79
|
+
end
|
80
|
+
|
81
|
+
|
82
|
+
def soa_answer(options)
|
83
|
+
mname = options[:mname] || 'default.com'
|
84
|
+
|
85
|
+
Dnsruby::RR.create( {
|
86
|
+
name: options[:name],
|
87
|
+
ttl: options[:ttl] || 3600,
|
88
|
+
type: 'SOA',
|
89
|
+
serial: serial_value(options[:serial]),
|
90
|
+
mname: mname,
|
91
|
+
rname: 'admin.' + mname,
|
92
|
+
refresh: options[:refresh] || 3600,
|
93
|
+
retry: options[:retry] || 3600,
|
94
|
+
expire: options[:expire] || 3600,
|
95
|
+
minimum: options[:minimum] || 3600
|
96
|
+
} )
|
97
|
+
end
|
98
|
+
|
99
|
+
|
100
|
+
# Builds a Dnsruby::RR instance with the specified type, name, and rdata,
|
101
|
+
# with a hard coded TTL and class 'IN'.
|
102
|
+
def rr(type, name, rdata)
|
103
|
+
ttl = 3600
|
104
|
+
klass = 'IN'
|
105
|
+
string = [name, ttl, klass, type, rdata].join(' ')
|
106
|
+
Dnsruby::RR.new_from_string(string)
|
107
|
+
end
|
108
|
+
|
109
|
+
|
110
|
+
# Creates an NS RR record.
|
111
|
+
def ns(owner, address)
|
112
|
+
rr('NS', owner, address)
|
113
|
+
end
|
114
|
+
|
115
|
+
|
116
|
+
# Creates a Dnsruby::Update object from a hash,
|
117
|
+
# as it would be generated from a Cucumber table
|
118
|
+
# with the following headings:
|
119
|
+
# | Action | Type | Domain | RDATA |
|
120
|
+
def dns_update(zone, records)
|
121
|
+
update = Dnsruby::Update.new(zone)
|
122
|
+
records.each do |r|
|
123
|
+
if r.type.upcase == 'ADD'
|
124
|
+
s = "#{Domain} 3600 #{Type} #{RDATA}"
|
125
|
+
rr = Dnsruby::RR.create(s)
|
126
|
+
update.add(rr)
|
127
|
+
else
|
128
|
+
update.delete(r['Domain'], r['Type'], r['RDATA'])
|
129
|
+
end
|
130
|
+
end
|
131
|
+
update
|
132
|
+
end
|
133
|
+
|
134
|
+
|
135
|
+
# @param options a hash containing values for keys [:name, :serial, :mname]
|
136
|
+
# TODO: Eliminate duplication of soa_response and notify_message
|
137
|
+
def soa_response(options)
|
138
|
+
raise "Must provide zone name as options[:name]." if options[:name].nil?
|
139
|
+
message = Dnsruby::Message.new(options[:name], 'SOA', 'IN')
|
140
|
+
message.header.qr = true
|
141
|
+
message.add_answer(soa_answer(options))
|
142
|
+
message
|
143
|
+
end
|
144
|
+
|
145
|
+
|
146
|
+
# Builds a SOA RR suitable for inclusion in the authority section of an IXFR query.
|
147
|
+
def ixfr_request_soa_rr(zone, serial)
|
148
|
+
options = {
|
149
|
+
name: zone,
|
150
|
+
type: 'SOA',
|
151
|
+
ttl: 3600,
|
152
|
+
klass: 'IN',
|
153
|
+
mname: '.',
|
154
|
+
rname: '.',
|
155
|
+
serial: serial_value(serial),
|
156
|
+
refresh: 0,
|
157
|
+
retry: 0,
|
158
|
+
expire: 0,
|
159
|
+
minimum: 0
|
160
|
+
}
|
161
|
+
|
162
|
+
Dnsruby::RR.new_from_hash(options)
|
163
|
+
end
|
164
|
+
|
165
|
+
|
166
|
+
def ixfr_request(zone, serial)
|
167
|
+
query = Dnsruby::Message.new(zone, 'IXFR')
|
168
|
+
query.add_authority(ixfr_request_soa_rr(zone, serial_value(serial)))
|
169
|
+
query
|
170
|
+
end
|
171
|
+
|
172
|
+
|
173
|
+
def axfr_request(zone)
|
174
|
+
Dnsruby::Message.new(zone, 'AXFR')
|
175
|
+
end
|
176
|
+
|
177
|
+
# @param options a hash containing values for keys [:name, :serial, :mname]
|
178
|
+
def notify_message(options)
|
179
|
+
|
180
|
+
message = Dnsruby::Message.new(options[:name], 'SOA', 'IN')
|
181
|
+
message.header.opcode = Dnsruby::OpCode::Notify
|
182
|
+
|
183
|
+
mname = options[:mname] || 'default.com'
|
184
|
+
|
185
|
+
message.add_answer(Dnsruby::RR.new_from_hash( {
|
186
|
+
name: options[:name],
|
187
|
+
type: 'SOA',
|
188
|
+
serial: serial_value(options[:serial]),
|
189
|
+
mname: mname,
|
190
|
+
rname: 'admin.' + mname,
|
191
|
+
refresh: 3600,
|
192
|
+
retry: 3600,
|
193
|
+
expire: 3600,
|
194
|
+
minimum: 3600
|
195
|
+
} ))
|
196
|
+
message
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|