class MockDnsServer::SerialHistory

Manages RR additions and deletions for multiple serials, and builds responses to AXFR and IXFR requests.

Attributes

ixfr_response_uses_axfr_style[RW]
low_serial[RW]
zone[RW]

Public Class Methods

new(zone, start_serial, initial_records = [], ixfr_response_uses_axfr_style = :never) click to toggle source

Creates the instance. @param zone @param start_serial the serial of the data set provided in the initial_records @param initial_records the starting data @param ixfr_response_uses_axfr_style when to respond to an IXFR request with an AXFR-style IXFR,

 rather than an IXFR list of changes.  Regardless of this option,
 if the requested serial >= the last known serial of this history,
 a response with a single SOA record containing the highest known serial will be sent.
 The following options apply to any other case, and are:

:never (default) - always return IXFR-style, but
    if the requested serial is not known by the server
    (i.e. if it is *not* the serial of one of the transactions in the history),
    then return 'transfer failed' rcode
:always - always return AXFR-style
:auto - if the requested serial is known by the server (i.e. if it is
     the serial of one of the transactions in the history,
     or is the initial serial of the history), then return an IXFR list;
     otherwise return an AXFR list.
Note that even when an AXFR-style list is returned, it is still an IXFR
response -- that is, the IXFR question from the query is copied into the response.
# File lib/mock_dns_server/serial_history.rb, line 33
def initialize(zone, start_serial, initial_records = [], ixfr_response_uses_axfr_style = :never)
  @zone = zone
  @low_serial = SerialNumber.object(start_serial)
  @initial_records = initial_records
  self.ixfr_response_uses_axfr_style = ixfr_response_uses_axfr_style
  @txns = ThreadSafe::Hash.new  # txns is an abbreviation of transactions
end

Public Instance Methods

axfr_records() click to toggle source
# File lib/mock_dns_server/serial_history.rb, line 162
def axfr_records
  [high_serial_soa_rr, current_data, high_serial_soa_rr].flatten
end
current_data() click to toggle source
# File lib/mock_dns_server/serial_history.rb, line 154
def current_data
  data_at_serial(:current)
end
data_at_serial(serial) click to toggle source

@return a snapshot array of the data as of a given serial number @serial if a number, must be in the range of known serials

if :current, the highest known serial will be used
# File lib/mock_dns_server/serial_history.rb, line 129
def data_at_serial(serial)

  serial = high_serial if serial == :current
  serial = SerialNumber.object(serial)

  if serial.nil? || serial > high_serial || serial < low_serial
    raise "Serial must be in range #{low_serial} to #{high_serial} inclusive."
  end
  data = @initial_records.clone

  txn_serials.each do |key|
    txn = @txns[key]
    break if txn.serial > serial
    txn.deletions.each do |d|
      data.reject! { |rr| rr_equivalent(rr, d) }
    end
    txn.additions.each do |a|
      data.reject! { |rr| rr_equivalent(rr, a) }
      data << a
    end
  end

  data
end
high_serial() click to toggle source
# File lib/mock_dns_server/serial_history.rb, line 87
def high_serial
  txn_serials.empty? ? low_serial : txn_serials.last
end
high_serial_soa_rr() click to toggle source
# File lib/mock_dns_server/serial_history.rb, line 158
def high_serial_soa_rr
  MessageBuilder.soa_answer(name: zone, serial: high_serial)
end
is_tracked_serial(serial) click to toggle source
# File lib/mock_dns_server/serial_history.rb, line 230
def is_tracked_serial(serial)
  serial = SerialNumber.object(serial)
  serials.include?(serial)
end
ixfr_records(base_serial = nil) click to toggle source

@return an array of RR's that can be used to populate an IXFR response. @base_serial the serial from which to start when building the list of changes

# File lib/mock_dns_server/serial_history.rb, line 181
def ixfr_records(base_serial = nil)
  base_serial = SerialNumber.object(base_serial)

  records = []
  records << high_serial_soa_rr

  serials = @txns.keys

  # Note that the serials in the data structure are the 'to' serials,
  # whereas the serial of this request will be the 'from' serial.
  # To compensate for this, we take the first serial *after* the
  # occurrence of base_serial in the array of serials, thus the +1 below.
  index_minus_one = serials.find_index(base_serial)
  index_is_index_other_than_last_index = index_minus_one && index_minus_one < serials.size - 1

  base_serial_index = index_is_index_other_than_last_index ? index_minus_one + 1 : 0

  serials_to_process = serials[base_serial_index..-1]
  serials_to_process.each do |serial|
    txn = @txns[serial]
    txn_records = txn.ixfr_records(previous_serial(serial))
    txn_records.each { |rec| records << rec }
  end

  records << high_serial_soa_rr
  records
end
ixfr_response_style(serial) click to toggle source

When handling an IXFR request, use the following logic:

if the serial number requested >= the current serial number (highest_serial), return a single SOA record (at the current serial number).

Otherwise, given the current value of ixfr_response_uses_axfr_style:

:always - always return an AXFR-style IXFR response

:never (default) - if we have that serial in our history, return an IXFR response,

else return a Transfer Failed error message

:auto - if we have that serial in our history, return an IXFR response,

else return an AXFR style response.

@return the type of response appropriate to this serial and request

# File lib/mock_dns_server/serial_history.rb, line 259
def ixfr_response_style(serial)
  serial = SerialNumber.object(serial)

  if serial >= high_serial
    :single_soa
  else
    case ixfr_response_uses_axfr_style
      when :never
        is_tracked_serial(serial) ? :ixfr : :xfer_failed
      when :auto
        is_tracked_serial(serial) ? :ixfr : :axfr_style_ixfr
      when :always
        :axfr_style_ixfr
    end
  end
end
ixfr_response_uses_axfr_style=(mode) click to toggle source
# File lib/mock_dns_server/serial_history.rb, line 41
def ixfr_response_uses_axfr_style=(mode)

  validate_input = ->() do
    valid_modes = [:never, :always, :auto]
    unless valid_modes.include?(mode)
      valid_modes_as_string = valid_modes.map(&:inspect).join(', ')
      raise "ixfr_response_uses_axfr_style mode must be one of the following: #{valid_modes_as_string}"
    end
  end

  validate_input.()
  @ixfr_response_uses_axfr_style = mode
end
next_serial_value() click to toggle source

Returns the next serial value that could be added to the history, i.e. the successor to the highest serial we now have.

# File lib/mock_dns_server/serial_history.rb, line 238
def next_serial_value
  SerialNumber.next_serial_value(high_serial.to_i)
end
previous_serial(serial) click to toggle source

Finds the serial previous to that of this transaction. @return If txn is the first txn, returns start_serial of the history else the serial of the previous transaction

# File lib/mock_dns_server/serial_history.rb, line 170
def previous_serial(serial)
  serial = SerialNumber.object(serial)
  return nil if serial <= low_serial || serial > high_serial

  txn_index = txn_serials.find_index(serial)
  txn_index > 0 ? txn_serials[txn_index - 1] : @low_serial
end
rr_compare(rr1, rr2) click to toggle source

Although Dnsruby has a <=> operator on RR's, we need a comparison that looks only at the type, name, and rdata (and not the TTL, for example), for purposes of detecting records that need be deleted.

# File lib/mock_dns_server/serial_history.rb, line 98
def rr_compare(rr1, rr2)

  rrs = [rr1, rr2]

  name1, name2 = rrs.map { |rr| rr.name.to_s.downcase }
  if name1 != name2
    return name1 > name2 ? 1 : -1
  end

  type1, type2 = rrs.map { |rr| rr.type.to_s.downcase }
  if type1 != type2
    return type1 > type2 ? 1 : -1
  end

  rdata1, rdata2 = rrs.map(&:rdata)
  if rdata1 != rdata2
    rdata1 > rdata2 ? 1 : -1
  else
    0
  end
end
rr_equivalent(rr1, rr2) click to toggle source
# File lib/mock_dns_server/serial_history.rb, line 121
def rr_equivalent(rr1, rr2)
  rr_compare(rr1, rr2) == 0
end
serial_additions(serial) click to toggle source
# File lib/mock_dns_server/serial_history.rb, line 62
def serial_additions(serial)
  serial = SerialNumber.object(serial)
  @txns[serial] ? @txns[serial].additions : nil
end
serial_deletions(serial) click to toggle source
# File lib/mock_dns_server/serial_history.rb, line 74
def serial_deletions(serial)
  serial = SerialNumber.object(serial)
  @txns[serial] ? @txns[serial].deletions : nil
end
serials() click to toggle source
# File lib/mock_dns_server/serial_history.rb, line 83
def serials
  [low_serial] + txn_serials
end
set_serial_additions(serial, additions) click to toggle source
# File lib/mock_dns_server/serial_history.rb, line 55
def set_serial_additions(serial, additions)
  serial = SerialNumber.object(serial)
  additions = Array(additions)
  serial_transaction(serial).additions = additions
  self
end
set_serial_deletions(serial, deletions) click to toggle source
# File lib/mock_dns_server/serial_history.rb, line 67
def set_serial_deletions(serial, deletions)
  serial = SerialNumber.object(serial)
  deletions = Array(deletions)
  serial_transaction(serial).deletions = deletions
  self
end
to_s() click to toggle source
# File lib/mock_dns_server/serial_history.rb, line 91
def to_s
  "#{self.class.name}: zone: #{zone}, initial serial: #{low_serial}, high_serial: #{high_serial}, records:\n#{ixfr_records}\n"
end
txn_serials() click to toggle source
# File lib/mock_dns_server/serial_history.rb, line 79
def txn_serials
  @txns.keys
end
xfr_array_type(records) click to toggle source

Determines whether a given record array is AXFR- or IXFR-style. @param records array of IXFR or AXFR records @return :ixfr, :axfr, :error

# File lib/mock_dns_server/serial_history.rb, line 213
def xfr_array_type(records)
  begin
    for num_consecutive_soas in (0..records.size)
      break unless records[num_consecutive_soas].is_a?(Dnsruby::RR::SOA)
    end
    case num_consecutive_soas
      when nil; :error
      when 0;   :error
      when 1;   :axfr
      else;     :ixfr
    end
  rescue => e
    :error
  end
end
xfr_response(incoming_message) click to toggle source

Creates a response message based on the type and serial of the incoming message. @param incoming_message an AXFR or IXFR request @return a Dnsruby message containing the response, either or AXFR or IXFR

# File lib/mock_dns_server/serial_history.rb, line 280
def xfr_response(incoming_message)

  mt           = MessageTransformer.new(incoming_message)
  query_zone   = mt.qname
  query_type   = mt.qtype.downcase.to_sym  # :axfr or :ixfr
  query_serial = mt.serial(:authority)  # ixfr requests only, else will be nil

  validate_inputs = ->() {
    if query_zone.downcase != zone.downcase
      raise "Query zone (#{query_zone}) differs from history zone (#{zone})."
    end

    unless [:axfr, :ixfr].include?(query_type)
      raise "Invalid qtype (#{query_type}), must be AXFR or IXFR."
    end

    if query_type == :ixfr && query_serial.nil?
      raise 'IXFR request did not specify serial in authority section.'
    end
  }

  build_standard_response = ->(rrs = nil) do
    response = Dnsruby::Message.new
    response.header.qr = true
    response.header.aa = true
    rrs.each { |record| response.add_answer!(record) } if rrs
    incoming_message.question.each { |q| response.add_question(q) }
    response
  end

  build_error_response = ->() {
    response = build_standard_response.()
    response.header.rcode = Dnsruby::RCode::REFUSED
    response
  }

  build_single_soa_response = ->() {
    build_standard_response.([high_serial_soa_rr])
  }

  validate_inputs.()
  xfr_response = nil

  case query_type

    when :axfr
      xfr_response = build_standard_response.(axfr_records)
    when :ixfr
      response_style = ixfr_response_style(query_serial)

      case response_style
        when :axfr_style_ixfr
          xfr_response = build_standard_response.(axfr_records)
        when :ixfr
          xfr_response = build_standard_response.(ixfr_records(query_serial))
        when :single_soa
          xfr_response = build_single_soa_response.()
        when :error
          xfr_response = build_error_response.()
      end
  end

xfr_response
end

Private Instance Methods

check_new_serial(new_serial) click to toggle source

Checks to see that a new serial whose transactions will be added to the history has a valid serial value in the context of the data already there. Raises an error if the serial is bad, else does nothing.

# File lib/mock_dns_server/serial_history.rb, line 350
def check_new_serial(new_serial)
  if new_serial < low_serial
    raise "New serial of #{new_serial} must not be lower than initial serial of #{low_serial}."
  elsif new_serial < high_serial
    raise "New serial of #{new_serial} must not be lower than highest preexisting serial of #{high_serial}."
  end
end
serial_transaction(serial) click to toggle source

Returns the SerialTransaction instance associated with this serial value, creating it if it does not already exist.

# File lib/mock_dns_server/serial_history.rb, line 361
def serial_transaction(serial)
  unless @txns[serial]
    check_new_serial(serial)
    @txns[serial] ||= SerialTransaction.new(zone, serial)

    # As long as we prohibit adding serials out of order, there is no need for this:
    # recreate_hash
  end

  @txns[serial]
end