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,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