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