Class: MockDnsServer::Server

Inherits:
Object
  • Object
show all
Extended by:
Forwardable
Defined in:
lib/mock_dns_server/server.rb

Overview

Starts a UDP and TCP server that listens for DNS and/or other messages.

Constant Summary collapse

DEFAULT_PORT =
53
DEFAULT_TIMEOUT =
1.0

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(options = {}) ⇒ Server

Returns a new instance of Server.



26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# File 'lib/mock_dns_server/server.rb', line 26

def initialize(options = {})

  @closed = false
  defaults = {
      port: DEFAULT_PORT,
      timeout_secs: DEFAULT_TIMEOUT,
      verbose: false
  }
  options = defaults.merge(options)

  @context = ServerContext.new(self, options)

  self.class.open_servers << self
  create_sockets
end

Instance Attribute Details

#contextObject (readonly)

Do we want serials to be attributes of the server, or configured in conditional actions?



20
21
22
# File 'lib/mock_dns_server/server.rb', line 20

def context
  @context
end

#control_queueObject (readonly)

Do we want serials to be attributes of the server, or configured in conditional actions?



20
21
22
# File 'lib/mock_dns_server/server.rb', line 20

def control_queue
  @control_queue
end

#serialsObject (readonly)

Do we want serials to be attributes of the server, or configured in conditional actions?



20
21
22
# File 'lib/mock_dns_server/server.rb', line 20

def serials
  @serials
end

#socketsObject (readonly)

Do we want serials to be attributes of the server, or configured in conditional actions?



20
21
22
# File 'lib/mock_dns_server/server.rb', line 20

def sockets
  @sockets
end

Class Method Details

.close_all_serversObject



390
391
392
# File 'lib/mock_dns_server/server.rb', line 390

def self.close_all_servers
  open_servers.clone.each { |server| server.close }
end

.eligible_interfacesObject

Returns the IP addresses (as strings) of the host on which this is running that are eligible to be used for a Server instance. Eligibility is defined as IPV4, not loopback, and not multicast.



415
416
417
418
419
420
# File 'lib/mock_dns_server/server.rb', line 415

def self.eligible_interfaces
  addrinfos = Socket.ip_address_list.select do |intf|
    intf.ipv4? && !intf.ipv4_loopback? && !intf.ipv4_multicast?
  end
  addrinfos.map(&:ip_address)
end

.kill_all_serversObject



395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
# File 'lib/mock_dns_server/server.rb', line 395

def self.kill_all_servers
  threads_needing_exit = ServerThread.all.select { |thread| ['sleep', 'run'].include?(thread.status) }

  threads_needing_exit.each do |thread|
    server = thread.server
    # If we can get a handle on the server, close it; else, just exit the thread.
    if server
      server.close
      raise "Sockets not closed." unless server.closed?
    else
      raise "Could not get server reference"
    end
    thread.join
  end
end

.open_serversObject



373
374
375
# File 'lib/mock_dns_server/server.rb', line 373

def self.open_servers
  @servers ||= ThreadSafe::Array.new
end

.with_new_server(options = {}) ⇒ Object

Creates a new server, yields to the passed block, then closes the server.



379
380
381
382
383
384
385
386
387
# File 'lib/mock_dns_server/server.rb', line 379

def self.with_new_server(options = {})
  begin
    server = self.new(options)
    yield(server)
  ensure
    server.close if server
  end
  nil  # don't want to return server because it should no longer be used
end

Instance Method Details

#add_conditional_action(conditional_action) ⇒ Object



88
89
90
# File 'lib/mock_dns_server/server.rb', line 88

def add_conditional_action(conditional_action)
  conditional_actions.add(conditional_action)
end

#add_conditional_actions(conditional_actions) ⇒ Object



93
94
95
# File 'lib/mock_dns_server/server.rb', line 93

def add_conditional_actions(conditional_actions)
  conditional_actions.each { |ca| add_conditional_action(ca) }
end

#closeObject

Closes the sockets and exits the server thread if it has already been created.



58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# File 'lib/mock_dns_server/server.rb', line 58

def close
  return if closed?
  puts "Closing #{self}..." if verbose
  @closed = true

  sockets.each { |socket| socket.close unless socket.closed? }

  self.class.open_servers.delete(self)

  if @server_thread
    @server_thread.exit
    @server_thread.join
    @server_thread = nil
  end
end

#closed?Boolean

Returns:

  • (Boolean)


75
76
77
# File 'lib/mock_dns_server/server.rb', line 75

def closed?
  @closed
end

#conditional_action_countObject



118
119
120
# File 'lib/mock_dns_server/server.rb', line 118

def conditional_action_count
  context.conditional_actions.size
end

#create_socketsObject

Creates a server, executes the passed block, and then closes the server.



44
45
46
47
48
49
50
51
52
53
54
# File 'lib/mock_dns_server/server.rb', line 44

def create_sockets
  @tcp_listener_socket = TCPServer.new(host, port)
  @tcp_listener_socket.setsockopt(:SOCKET, :REUSEADDR, true)

  @udp_socket = UDPSocket.new
  @udp_socket.bind(host, port)

  @control_queue = SizedQueue.new(1000)

  @sockets = [@tcp_listener_socket, @udp_socket]
end

#do_then_closeObject

For an already initialized server, perform the passed block and ensure that the server will be closed, even if an error is raised.



303
304
305
306
307
308
309
310
# File 'lib/mock_dns_server/server.rb', line 303

def do_then_close
  begin
    start
    yield
  ensure
    close
  end
end

#handle_read(block, read_socket) ⇒ Object

Handles the receiving of a single message.



174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
# File 'lib/mock_dns_server/server.rb', line 174

def handle_read(block, read_socket)

  request = nil
  sender = nil
  protocol = nil

  if read_socket == @tcp_listener_socket
    sockets << @tcp_listener_socket.accept
    puts "Got new TCP socket: #{sockets.last}" if verbose

  elsif read_socket == @udp_socket
    protocol = :udp
    request, sender = udp_recvfrom_with_timeout(read_socket)
    request = MessageHelper.to_dns_message(request)
    puts "Got incoming message from UDP socket:\n#{request}\n" if verbose

  else # it must be a spawned TCP read socket
    if read_socket.eof? # we're here because it closed on the client side
      sockets.delete(read_socket)
      puts "received EOF from socket #{read_socket}...deleted it from listener list." if verbose
    else # read from it
      protocol = :tcp

      # Read message size:
      request = MessageHelper.read_tcp_message(read_socket)
      sender = read_socket

      if verbose
        if request.nil? || request == ''
          puts "Got no request."
        else
          puts "Got incoming message from TCP socket:\n#{request}\n"
        end
      end
    end
  end
  handle_request(request, sender, protocol, &block) if request
end

#handle_request(request, sender, protocol) ⇒ Object



103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/mock_dns_server/server.rb', line 103

def handle_request(request, sender, protocol)

  request = MessageHelper.to_dns_message(request)

  context.with_mutex do
    if block_given?
      yield(request, sender, protocol)
    else
      record_receipt(request, sender, protocol)
      context.conditional_actions.respond_to(request, sender, protocol)
    end
  end
end

#history_copyObject



235
236
237
238
239
# File 'lib/mock_dns_server/server.rb', line 235

def history_copy
  copy = nil
  context.with_mutex { copy = history.copy }
  copy
end

#is_dns_packet?(packet, protocol) ⇒ Boolean

Returns:

  • (Boolean)


257
258
259
260
261
262
263
# File 'lib/mock_dns_server/server.rb', line 257

def is_dns_packet?(packet, protocol)
  raise "protocol must be :tcp or :udp" unless [:tcp, :udp].include?(protocol)

  encoded_message = :udp ? packet : packet[2..-1]
  message = MessageHelper.to_dns_message(encoded_message)
  message.is_a?(Dnsruby::Message)
end

#load_zone(options) ⇒ Object

Sets up the SOA and records to serve on IXFR/AXFR queries. mname is set to “default.#zone

Parameters:

  • options

    hash containing the following keys: zone serial (SOA) dns_records array of RR’s times times for the action to be performed before removal (optional, defaults to forever) zts_hosts array of ZTS hosts zts_port (optional, defaults to 53)



339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
# File 'lib/mock_dns_server/server.rb', line 339

def load_zone(options)

  validate_options = ->() do
    required_options = [:zone, :serial_history]
    missing_options = required_options.select { |o| options[o].nil? }
    unless missing_options.empty?
      raise "Options required for load_zone were missing: #{missing_options.join(', ')}."
    end
  end

  validate_options.()

  serial_history = options[:serial_history]
  zone           = serial_history.zone
  zts_hosts      = Array(options[:zts_hosts])
  zts_port       = options[:zts_port] || 53
  times          = options[:times] || 0
  mname          = "default.#{zone}"

  cond_action = ConditionalActionFactory.new.zone_load(serial_history, times)
  conditional_actions.add(cond_action)

  notify_options = { name: zone, serial: serial_history.high_serial, mname: mname }
  zts_hosts.each do |zts_host|
    send_notify(zts_host, zts_port, notify_options)
  end
end

#occurred?(inspection) ⇒ Boolean

Returns:

  • (Boolean)


247
248
249
# File 'lib/mock_dns_server/server.rb', line 247

def occurred?(inspection)
  history.occurred?(inspection)
end

#ready?Boolean

Determines whether the server has reached the point in its lifetime when it is ready, but it may still be true after the server is closed. Intended to be used during server startup and not thereafter.

Returns:

  • (Boolean)


83
84
85
# File 'lib/mock_dns_server/server.rb', line 83

def ready?
  !! @ready
end

#record_receipt(request, sender, protocol) ⇒ Object



98
99
100
# File 'lib/mock_dns_server/server.rb', line 98

def record_receipt(request, sender, protocol)
  history.add_incoming(request, sender, protocol)
end

#send_notify(zts_host, zts_port, message_options, notify_message_override = nil, wait_for_response = true) ⇒ Object

Parameters:

  • message_options
    • name (zone), serial, mname



267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
# File 'lib/mock_dns_server/server.rb', line 267

def send_notify(zts_host, zts_port, message_options, notify_message_override = nil, wait_for_response = true)
  notify_message = notify_message_override ? notify_message_override : MessageBuilder::notify_message(message_options)

  socket = UDPSocket.new

  puts "Sending notify message to host #{zts_host}, port #{zts_port}" if verbose
  socket.send(notify_message.encode, 0, zts_host, zts_port)

  if wait_for_response
    response_wire_data, _ = udp_recvfrom_with_timeout(socket)
    response = MessageHelper.to_dns_message(response_wire_data)
    context.with_mutex { history.add_notify_response(response, zts_host, zts_port, :udp) }
    response
  else
    nil
  end
end

#send_response(sender, content, protocol) ⇒ Object



226
227
228
229
230
231
232
# File 'lib/mock_dns_server/server.rb', line 226

def send_response(sender, content, protocol)
  if protocol == :tcp
    send_tcp_response(sender, content)
  elsif protocol == :udp
    send_udp_response(sender, content)
  end
end

#send_tcp_response(socket, content) ⇒ Object



214
215
216
# File 'lib/mock_dns_server/server.rb', line 214

def send_tcp_response(socket, content)
  socket.write(MessageHelper.tcp_message_package_for_write(content))
end

#send_udp_response(sender, content) ⇒ Object



219
220
221
222
223
# File 'lib/mock_dns_server/server.rb', line 219

def send_udp_response(sender, content)
  send_data = MessageHelper.udp_message_package_for_write(content)
  _, client_port, ip_addr, _ = sender
  @udp_socket.send(send_data, 0, ip_addr, client_port)
end

#start(&block) ⇒ Object

Starts this server and returns the new thread in which it will run. If a block is passed, it will be passed in turn to handle_request to be executed (on the server’s thread).



126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
# File 'lib/mock_dns_server/server.rb', line 126

def start(&block)
  raise "Server already started." if @server_thread

  puts "Starting server on host #{host}:#{port}..." if verbose
  @server_thread = ServerThread.new do
    begin

      Thread.current.server = self

      loop do
        unless @control_queue.empty?
          action = @control_queue.pop
          action.()
        end

        @ready = true
        reads, _, errors = IO.select(sockets, nil, sockets, timeout_secs)

        error_occurred = errors && errors.first  # errors.first will be nil on timeout
        if error_occurred
          puts errors if verbose
          break
        end

        if reads
          reads.each do |read_socket|
            handle_read(block, read_socket)
            #if conditional_actions.empty?
            #  puts "No more conditional actions.  Closing server..." if verbose
            #  break
            #end
          end
        else
          # TODO: This is where we can put things to do periodically when the server is not busy
        end
      end
    rescue => e
      self.close
      # Errno::EBADF is raised when the server object is closed normally,
      # so we don't want to report it.  All other errors should be reported.
      raise e unless e.is_a?(Errno::EBADF)
    end
  end
  self  # for chaining, especially with wait_until_ready
end

#to_sObject



368
369
370
# File 'lib/mock_dns_server/server.rb', line 368

def to_s
  "#{self.class.name}: host: #{host}, port: #{port}, ready: #{ready?}, closed: #{closed?}"
end

#udp_recvfrom_with_timeout(udp_socket, timeout_secs = 10, max_data_size = 10_000) ⇒ Object

TODO: better default max?



286
287
288
289
290
291
292
293
294
295
296
297
298
299
# File 'lib/mock_dns_server/server.rb', line 286

def udp_recvfrom_with_timeout(udp_socket, timeout_secs = 10, max_data_size = 10_000) # TODO: better default max?
  request = nil
  sender = nil

  recv_thread = Thread.new do
    request, sender = udp_socket.recvfrom(max_data_size)
  end
  timeout_expired = recv_thread.join(timeout_secs).nil?
  if timeout_expired
    recv_thread.exit
    raise "Response not received from UDP socket."
  end
  [request, sender]
end

#wait_until_ready(sleep_duration = 0.000_02) ⇒ Object

Waits until the server is ready, sleeping between calls to ready.

Returns:

  • elapsed time until ready



315
316
317
318
319
320
321
322
323
324
325
# File 'lib/mock_dns_server/server.rb', line 315

def wait_until_ready(sleep_duration = 0.000_02)

  if Thread.current == @server_thread
    raise "This method must not be called in the server's thread."
  end

  start = Time.now
  sleep(sleep_duration) until ready?
  duration = Time.now - start
  duration
end