##
# dnser.rb
# Created Oct 7, 2015
# By Ron Bowes
#
# See: LICENSE.md
#
# I had nothing but trouble using rubydns (which became celluloid-dns, whose
# documentation is just flat out wrong), and I only really need a very small
# subset of DNS functionality, so I decided that I should just wrote my own.
#
# Note: after writing this, I noticed that Resolv::DNS exists in the Ruby
# language, I need to check if I can use that.
#
# There are two methods for using this library: as a client (to make a query)
# or as a server (to listen for queries and respond to them).
#
# To make a query, use DNSer.query:
#
#   DNSer.query("google.com") do |response|
#     ...
#   end
#
# `response` will be of type DNSer::Packet.
#
# To listen for queries, create a new instance of DNSer, which will begin
# listening on a port, but won't actually handle queries yet:
#
#   dnser = DNSer.new("0.0.0.0", 53)
#   dnser.on_request() do |transaction|
#     ...
#   end
#
# `transaction` is of type DNSer::Transaction, and allows you to respond to the
# request either immediately or asynchronously.
#
# DNSer currently supports the following record types: A, NS, CNAME, SOA, MX,
# TXT, and AAAA.
##

require 'ipaddr'
require 'socket'
require 'timeout'

require_relative './vash'

class DNSer
  # Create a custom error message
  class DnsException < StandardError
  end

  class Packet
    attr_accessor :trn_id, :opcode, :flags, :rcode, :questions, :answers

    # Request / response
    QR_QUERY    = 0x0000
    QR_RESPONSE = 0x0001

    QRS = {
      QR_QUERY    => "QUERY",
      QR_RESPONSE => "RESPONSE",
    }

    # Return codes
    RCODE_SUCCESS         = 0x0000
    RCODE_FORMAT_ERROR    = 0x0001
    RCODE_SERVER_FAILURE  = 0x0002 # :servfail
    RCODE_NAME_ERROR      = 0x0003 # :NXDomain
    RCODE_NOT_IMPLEMENTED = 0x0004
    RCODE_REFUSED         = 0x0005

    RCODES = {
      RCODE_SUCCESS         => ":NoError (RCODE_SUCCESS)",
      RCODE_FORMAT_ERROR    => ":FormErr (RCODE_FORMAT_ERROR)",
      RCODE_SERVER_FAILURE  => ":ServFail (RCODE_SERVER_FAILURE)",
      RCODE_NAME_ERROR      => ":NXDomain (RCODE_NAME_ERROR)",
      RCODE_NOT_IMPLEMENTED => ":NotImp (RCODE_NOT_IMPLEMENTED)",
      RCODE_REFUSED         => ":Refused (RCODE_REFUSED)",
    }

    # Opcodes - only QUERY is typically used
    OPCODE_QUERY  = 0x0000
    OPCODE_IQUERY = 0x0800
    OPCODE_STATUS = 0x1000

    OPCODES = {
      OPCODE_QUERY  => "OPCODE_QUERY",
      OPCODE_IQUERY => "OPCODE_IQUERY",
      OPCODE_STATUS => "OPCODE_STATUS",
    }

    # The types that we support
    TYPE_A     = 0x0001
    TYPE_NS    = 0x0002
    TYPE_CNAME = 0x0005
    TYPE_SOA   = 0x0006
    TYPE_MX    = 0x000f
    TYPE_TXT   = 0x0010
    TYPE_AAAA  = 0x001c
    TYPE_ANY   = 0x00FF

    TYPES = {
      TYPE_A     => "A",
      TYPE_NS    => "NS",
      TYPE_CNAME => "CNAME",
      TYPE_SOA   => "SOA",
      TYPE_MX    => "MX",
      TYPE_TXT   => "TXT",
      TYPE_AAAA  => "AAAA",
      TYPE_ANY   => "ANY",
    }

    # The DNS flags
    FLAG_AA = 0x0008 # Authoritative answer
    FLAG_TC = 0x0004 # Truncated
    FLAG_RD = 0x0002 # Recursion desired
    FLAG_RA = 0x0001 # Recursion available

    # This converts a set of flags, as an integer, into a string
    def Packet.FLAGS(flags)
      result = []
      if((flags & FLAG_AA) == FLAG_AA)
        result << "AA"
      end
      if((flags & FLAG_TC) == FLAG_TC)
        result << "TC"
      end
      if((flags & FLAG_RD) == FLAG_RD)
        result << "RD"
      end
      if((flags & FLAG_RA) == FLAG_RA)
        result << "RA"
      end

      return result.join("|")
    end

    # Classes - we only define IN (Internet)
    CLS_IN                = 0x0001 # Internet

    CLSES = {
      CLS_IN => "IN",
    }

    class FormatException < StandardError
    end

    # DNS has some unusual properties that we have to handle, which is why I
    # wrote this class. It handles building / parsing DNS packets and keeping
    # track of where in the packet we currently are. The advantage, besides
    # simpler unpacking, is that encoded names (with pointers to other parts
    # of the packet) can be trivially handled.
    class DnsUnpacker
      attr_accessor :data

      # Create a new instance, initialized with the given data
      def initialize(data)
        @data = data.force_encoding("ASCII-8BIT")
        @offset = 0
      end

#      def remaining()
#        return @data[@offset..-1]
#      end

      # Unpack from the string, exactly like the normal `String#Unpack` method
      # in Ruby, except that an offset into the string is maintained and updated.
      def unpack(format, offset = nil)
        # If there's an offset, unpack starting there
        if(!offset.nil?)
          results = @data[offset..-1].unpack(format)
        else
          results = @data[@offset..-1].unpack(format + "a*")
          remaining = results.pop
          @offset = @data.length - remaining.length
        end

        if(!results.index(nil).nil?)
          raise(DNSer::Packet::FormatException, "DNS packet was truncated (or we messed up parsing it)!")
        end

        return *results
      end

      # This temporarily changes the offset that we're reading from, runs the
      # given block, then changes it back. This is used internally while
      # unpacking names.
      def _move_offset(offset)
        old_offset = @offset
        @offset = offset
        yield
        @offset = old_offset
      end

      # Unpack a name from the packet. Names are special, because they're
      # encoded as:
      # * A series of length-prefixed blocks, each indicating a segment
      # * Blocks with a length the starts with two '1' bits (11xxxxx...), which
      #   contains a pointer to another name elsewhere in the packet
      def unpack_name(depth = 0)
        segments = []

        if(depth > 16)
          raise(DNSer::Packet::FormatException, "It looks like this packet contains recursive pointers!")
        end

        loop do
          # If no offset is given, just eat data from the normal source
          len = unpack("C").pop()

          # Stop at the null terminator
          if(len == 0)
            break
          end
          # Handle "pointer" records by updating the offset
          if((len & 0xc0) == 0xc0)
            # If the first two bits are 1 (ie, 0xC0), the next
            # 10 bits are an offset, so we have to mask out the first two bits
            # with 0x3F (00111111)
            offset = ((len << 8) | unpack("C").pop()) & 0x3FFF

            _move_offset(offset) do
              segments << unpack_name(depth+1).split(/\./)
            end

            break
          end

          # It's normal, just unpack what we need to!
          segments << [unpack("a#{len}")[0].upcase]
        end

        return segments.join('.')
      end

      def verify_length(len)
        start_length = @offset
        yield
        end_length   = @offset

        if(end_length - start_length != len)
          raise(FormatException, "A resource record's length didn't match its actual length; something is funky")
        end
      end

      # Take a name, as a dotted string ("google.com") and return it as length-
      # prefixed segments ("\x06google\x03com\x00").
      #
      # TODO: Compress the name properly, if we can ("\xc0\x0c")
      def DnsUnpacker.pack_name(name)
        result = ''

        name.upcase.split(/\./).each do |segment|
          result += [segment.length(), segment].pack("Ca*")
        end

        result += "\0"
        return result
      end

      # Shows where in the string we're currently editing. Mostly usefulu for
      # debugging.
      def to_s()
        if(@offset == 0)
          return @data.unpack("H*").pop
        else
          return "#{@data[0..@offset-1].unpack("H*")}|#{@data[@offset..-1].unpack("H*")}"
        end
      end
    end

    class A
      attr_accessor :address

      def initialize(address)
        @address = IPAddr.new(address)

        if(!@address.ipv4?())
          raise(FormatException, "IPv4 address required!")
        end
      end

      def A.parse(data)
        address = data.unpack("A4").pop()
        return A.new(IPAddr.ntop(address))
      end

      def serialize()
        return @address.hton()
      end

      def to_s()
        return "#{@address} [A]"
      end
    end

    class NS
      attr_accessor :name

      def initialize(name)
        @name = name
      end

      def NS.parse(data)
        return NS.new(data.unpack_name())
      end

      def serialize()
        return DNSer::Packet::DnsUnpacker.pack_name(@name)
      end

      def to_s()
        return "#{@name} [NS]"
      end
    end

    class CNAME
      attr_accessor :name

      def initialize(name)
        @name = name
      end

      def CNAME.parse(data)
        return CNAME.new(data.unpack_name())
      end

      def serialize()
        return DNSer::Packet::DnsUnpacker.pack_name(@name)
      end

      def to_s()
        return "#{@name} [CNAME]"
      end
    end

    class SOA
      attr_accessor :primary, :responsible, :serial, :refresh, :retry_interval, :expire, :ttl

      def initialize(primary, responsible, serial, refresh, retry_interval, expire, ttl)
        @primary = primary
        @responsible = responsible
        @serial = serial
        @refresh = refresh
        @retry_interval = retry_interval
        @expire = expire
        @ttl = ttl
      end

      def SOA.parse(data)
        primary = data.unpack_name()
        responsible = data.unpack_name()
        serial, refresh, retry_interval, expire, ttl = data.unpack("NNNNN")

        return SOA.new(primary, responsible, serial, refresh, retry_interval, expire, ttl)
      end

      def serialize()
        return [
          DNSer::Packet::DnsUnpacker.pack_name(@primary),
          DNSer::Packet::DnsUnpacker.pack_name(@responsible),
          @serial,
          @refresh,
          @retry_interval,
          @expire,
          @ttl
        ].pack("a*a*NNNNN")
      end

      def to_s()
        return "Primary name server = #{@primary}, responsible authority's mailbox: #{@responsible}, serial number: #{@serial}, refresh interval: #{@refresh}, retry interval: #{@retry_interval}, expire limit: #{@expire}, min_ttl: #{@ttl} [SOA]"
      end
    end

    class MX
      attr_accessor :preference, :name

      def initialize(name, preference = 10)
        if(!name.is_a?(String) || !preference.is_a?(Fixnum))
          raise ArgumentError("Creating an MX record wrong! Please file a bug!")
        end
        @name = name
        @preference = preference
      end

      def MX.parse(data)
        preference = data.unpack("n").pop()
        name = data.unpack_name()

        return MX.new(name, preference)
      end

      def serialize()
        name = DNSer::Packet::DnsUnpacker.pack_name(@name)
        return [@preference, name].pack("na*")
      end

      def to_s()
        return "#{@preference} #{@name} [MX]"
      end
    end

    class TXT
      attr_accessor :data

      def initialize(data)
        @data = data
      end

      def TXT.parse(data)
        len = data.unpack("C").pop()
        bytes = data.unpack("A#{len}").pop()

        return TXT.new(bytes)
      end

      def serialize()
        return [@data.length, data].pack("Ca*")
      end

      def to_s()
        return "#{@data} [TXT]"
      end
    end

    class AAAA
      attr_accessor :address

      def initialize(address)
        @address = IPAddr.new(address)

        if(!@address.ipv6?())
          raise(FormatException, "IPv6 address required!")
        end
      end

      def AAAA.parse(data)
        address = data.unpack("A16").pop()
        return AAAA.new(IPAddr.ntop(address))
      end

      def serialize()
        return @address.hton()
      end

      def to_s()
        return "#{@address} [A]"
      end
    end

    class RRUnknown
      def initialize(type, data)
        @type = type
        @data = data
      end

      def RRUnknown.parse(type, data, length)
        data = data.unpack("A#{length}").pop()
        return RRUnknown.new(type, data)
      end

      def serialize()
        return @data
      end

      def to_s()
        return "(Unknown record type #{@type}): #{@data.unpack("H*")}"
      end
    end

    # This defines a DNS question. One question is sent in outgoing packets,
    # and one question is also sent in the response - generally, the same as
    # the question that was asked.
    class Question
      attr_reader :name, :type, :cls

      def initialize(name, type = DNSer::Packet::TYPE_ANY, cls = DNSer::Packet::CLS_IN)
        @name  = name
        @type  = type
        @cls  = cls
      end

      def Question.parse(data)
        name = data.unpack_name()
        type, cls = data.unpack("nn")

        return Question.new(name, type, cls)
      end

      def serialize()
        return [DNSer::Packet::DnsUnpacker.pack_name(@name), type, cls].pack("a*nn")
      end

      def type_s()
        return DNSer::Packet::TYPES[@type] || "<unknown>"
      end

      def cls_s()
        return DNSer::Packet::CLSES[@cls] || "<unknown>"
      end

      def to_s()
        return "#{name} [#{type_s()} #{cls_s()}]"
      end

      def answer(ttl, *args)
        case @type
        when DNSer::Packet::TYPE_A
          record = DNSer::Packet::A.new(*args)
        when DNSer::Packet::TYPE_NS
          record = DNSer::Packet::NS.new(*args)
        when DNSer::Packet::TYPE_CNAME
          record = DNSer::Packet::CNAME.new(*args)
        when DNSer::Packet::TYPE_MX
          record = DNSer::Packet::MX.new(*args)
        when DNSer::Packet::TYPE_TXT
          record = DNSer::Packet::TXT.new(*args)
        when DNSer::Packet::TYPE_AAAA
          record = DNSer::Packet::AAAA.new(*args)
        when DNSer::Packet::TYPE_ANY
          raise(DNSer::Packet::FormatException, "We can't automatically create a response for an 'ANY' request :(")
        else
          raise(DNSer::Packet::FormatException, "We don't know how to answer that type of request!")
        end

        return Answer.new(@name, @type, @cls, ttl, record)
      end

      def ==(other)
        if(!other.is_a?(Question))
          return false
        end

        return (@name == other.name) && (@type == other.type) && (@cls == other.cls)
      end
    end

    # A DNS answer. A DNS response packet contains zero or more Answer records
    # (defined by the 'ancount' value in the header). An answer contains the
    # name of the domain from the question, followed by a resource record.
    class Answer
      attr_reader :name, :type, :cls, :ttl, :rr

      def initialize(name, type, cls, ttl, rr)
        @name = name
        @type = type
        @cls  = cls
        @ttl  = ttl
        @rr   = rr

        if(rr.is_a?(String))
          raise(ArgumentError, "'rr' can't be a string!")
        end
      end

      def ==(other)
        if(!other.is_a?(Answer))
          return false
        end

        # Note: we don't check TTL here, and checking RR probably doesn't work (but we don't actually need it)
        return (@name == other.name) && (@type == other.type) && (@cls == other.cls) && (@rr == other.rr)
      end

      def Answer.parse(data)
        name = data.unpack_name()
        type, cls, ttl, rr_length = data.unpack("nnNn")

        rr = nil
        data.verify_length(rr_length) do
          case type
          when TYPE_A
            rr = A.parse(data)
          when TYPE_NS
            rr = NS.parse(data)
          when TYPE_CNAME
            rr = CNAME.parse(data)
          when TYPE_SOA
            rr = SOA.parse(data)
          when TYPE_MX
            rr = MX.parse(data)
          when TYPE_TXT
            rr = TXT.parse(data)
          when TYPE_AAAA
            rr = AAAA.parse(data)
          else
            puts("Warning: Unknown record type: #{type}")
            rr = RRUnknown.parse(type, data, rr_length)
          end
        end

        return Answer.new(name, type, cls, ttl, rr)
      end

      def serialize()
        # Hardcoding 0xc00c is kind of ugly, but it always works
        rr = @rr.serialize()
        return [0xc00c, @type, @cls, @ttl, rr.length(), rr].pack("nnnNna*")
      end

      def type_s()
        return DNSer::Packet::TYPES[@type]
      end

      def cls_s()
        return DNSer::Packet::CLSES[@cls]
      end

      def to_s()
        return "#{@name} [#{type_s()} #{cls_s()}]: #{@rr} [TTL = #{@ttl}]"
      end
    end

    def initialize(trn_id, qr, opcode, flags, rcode)
      @trn_id    = trn_id
      @qr        = qr
      @opcode    = opcode
      @flags     = flags
      @rcode     = rcode
      @questions = []
      @answers   = []
    end

    def add_question(question)
      @questions << question
    end

    def add_answer(answer)
      @answers << answer
    end

    def Packet.parse(data)
      data = DnsUnpacker.new(data)
      trn_id, full_flags, qdcount, ancount, _, _ = data.unpack("nnnnnn")

      qr     = (full_flags >> 15) & 0x0001
      opcode = (full_flags >> 11) & 0x000F
      flags  = (full_flags >> 7)  & 0x000F
      rcode  = (full_flags >> 0)  & 0x000F

      packet = Packet.new(trn_id, qr, opcode, flags, rcode)

      0.upto(qdcount - 1) do
        question = Question.parse(data)
        packet.add_question(question)
      end

      0.upto(ancount - 1) do
        answer = Answer.parse(data)
        packet.add_answer(answer)
      end

      return packet
    end

    def get_error(rcode)
      return Packet.new(@trn_id, DNSer::Packet::QR_RESPONSE, DNSer::Packet::OPCODE_QUERY, DNSer::Packet::FLAG_RD | DNSer::Packet::FLAG_RA, rcode)
    end

    def serialize()
      result = ''

      full_flags = ((@qr     << 15) & 0x8000) |
                   ((@opcode << 11) & 0x7800) |
                   ((@flags  <<  7) & 0x0780) |
                   ((@rcode  <<  0) & 0x000F)

      result += [
                  @trn_id,             # trn_id
                  full_flags,          # qr, opcode, flags, rcode
                  @questions.length(), # qdcount
                  @answers.length(),   # ancount
                  0,                   # nscount (ignored)
                  0                    # arcount (ignored)
                ].pack("nnnnnn")

      questions.each do |q|
        result += q.serialize()
      end

      answers.each do |a|
        result += a.serialize()
      end

      return result
    end

    def to_s(brief = false)
      if(brief)
        question = @questions[0] || '<unknown>'

        # Print error packets more clearly
        if(@rcode != DNSer::Packet::RCODE_SUCCESS)
          return "Request for #{question}: error: #{DNSer::Packet::RCODES[@rcode]}"
        end

        if(@qr == DNSer::Packet::QR_QUERY)
          return "Request for #{question}"
        else
          if(@answers.length == 0)
            return "Response for #{question}: n/a"
          else
            return "Response for #{question}: #{@answers[0]} (and #{@answers.length - 1} others)"
          end
        end
      end

      results = ["DNS #{QRS[@qr] || "unknown"}: id=#{@trn_id}, opcode=#{OPCODES[@opcode]}, flags=#{Packet.FLAGS(@flags)}, rcode=#{RCODES[@rcode] || "unknown"}, qdcount=#{@questions.length}, ancount=#{@answers.length}"]

      @questions.each do |q|
        results << "    Question: #{q}"
      end

      @answers.each do |a|
        results << "    Answer: #{a}"
      end

      return results.join("\n")
    end

    def ==(other)
      if(!other.is_a?(Packet))
        return false
      end

      return (@trn_id == other.trn_id) && (@opcode == other.opcode) && (@flags == other.flags) && (@questions == other.questions) && (@answers == other.answers)
    end
  end

  # When a request comes in, a transaction is created and sent to the callback.
  # The transaction can be used to respond to the request at any point in the
  # future.
  #
  # Any methods with a bang ('!') in front will send the response back to the
  # requester. Only one bang method can be called, any subsequent calls will
  # throw an exception.
  class Transaction
    attr_reader :request, :response, :sent

    def initialize(s, request, host, port, cache = nil)
      @s       = s
      @request = request
      @host    = host
      @port    = port
      @sent    = false
      @cache   = cache

      @response = DNSer::Packet.new(
        @request.trn_id,
        DNSer::Packet::QR_RESPONSE,
        @request.opcode,
        DNSer::Packet::FLAG_RD | DNSer::Packet::FLAG_RA,
        DNSer::Packet::RCODE_SUCCESS
      )

      @response.add_question(@request.questions[0])
    end

    def add_answer(answer)
      raise ArgumentError("Already sent!") if(@sent)

      @response.add_answer(answer)
    end

    def error(rcode)
      raise ArgumentError("Already sent!") if(@sent)

      @response.rcode = rcode
    end

    def error!(rcode)
      raise ArgumentError("Already sent!") if(@sent)

      @response.rcode = rcode
      reply!()
    end

    def passthrough!(pt_host, pt_port, callback = nil)
      raise ArgumentError("Already sent!") if(@sent)

      DNSer.query(@request.questions[0].name, {
          :server  => pt_host,
          :port    => pt_port,
          :type    => @request.questions[0].type,
          :cls     => @request.questions[0].cls,
          :timeout => 3,
        }
      ) do |response|
        # If there was a timeout, handle it
        if(response.nil?)
          response = @response
          response.rcode = DNSer::Packet::RCODE_SERVER_FAILURE
        end

        response.trn_id = @request.trn_id
        @s.send(response.serialize(), 0, @host, @port)

        # Let the callback know if anybody registered one
        if(callback)
          callback.call(response)
        end
      end

      @sent = true
    end

    def reply!(_="")
      raise ArgumentError("Already sent!") if(@sent)

      # Cache it if we have a cache
      if(@cache)
        @cache[@request.trn_id, 3] = {
          :request  => @request,
          :response => @response,
        }
      end

      # Send the response
      @s.send(@response.serialize(), 0, @host, @port)
      @sent = true
    end
  end

  # Create a new DNSer and listen on the given host/port. This will throw an
  # exception if we aren't allowed to bind to the given port.
  def initialize(host, port, cache=false)
    @s = UDPSocket.new()
    @s.bind(host, port)
    @thread = nil

    # Create a cache if the user wanted one
    if(cache)
      @cache = Vash.new()
    end
  end

  # This method returns immediately, but spawns a background thread. The thread
  # will recveive and parse DNS packets, create a transaction, and pass it to
  # the caller's block.
  def on_request()
    @thread = Thread.new() do |t|
      begin
        loop do
          data = @s.recvfrom(65536)

          # Data is an array where the first element is the actual data, and the second is the host/port
          request = DNSer::Packet.parse(data[0])

          # Create a transaction object, which we can use to respond
          transaction = Transaction.new(@s, request, data[1][3], data[1][1], @cache)

          # If caching is enabled, deal with it
          if(@cache)
            # This is somewhat expensive, but we aren't using the cache for performance
            @cache.cleanup!()

            # See if the transaction is cached
            cached = @cache[request.trn_id]

            # Verify it deeper (for security reasons)
            if(!cached.nil?)
              puts("POTENTIAL CACHE HIT")
              if(request == cached[:request])
                puts("CACHE HIT")
                transaction.reply!(cached[:response])
              end
            end
          end

          if(!transaction.sent)
            begin
              proc.call(transaction)
            rescue StandardError => e
              puts("Caught an error: #{e}")
              puts(e.backtrace())
              transaction.reply!(transaction.response_template({:rcode => DNSer::Packet::RCODE_SERVER_FAILURE}))
            end
          end
        end
      ensure
        @s.close
      end
    end
  end

  # Kill the listener
  def stop()
    if(@thread.nil?)
      puts("Tried to stop a listener that wasn't listening!")
      return
    end

    @thread.kill()
    @thread = nil
  end

  # After calling on_request(), this can be called to halt the program's
  # execution until the thread is stopped.
  def wait()
    if(@thread.nil?)
      puts("Tried to wait on a DNSer instance that wasn't listening!")
      return
    end

    @thread.join()
  end

  # Send out a query, asynchronously. This immediately returns, then, when the
  # query is finished, the callback block is called with a DNSer::Packet that
  # represents the response (or nil, if there was a timeout).
  def DNSer.query(hostname, params = {})
    server   = params[:server]   || "8.8.8.8"
    port     = params[:port]     || 53
    type     = params[:type]     || DNSer::Packet::TYPE_A
    cls      = params[:cls]      || DNSer::Packet::CLS_IN
    timeout  = params[:timeout]  || 3

    packet = DNSer::Packet.new(rand(65535), DNSer::Packet::QR_QUERY, DNSer::Packet::OPCODE_QUERY, DNSer::Packet::FLAG_RD, DNSer::Packet::RCODE_SUCCESS)
    packet.add_question(DNSer::Packet::Question.new(hostname, type, cls))

    s = UDPSocket.new()

    return Thread.new() do
      begin
        s.send(packet.serialize(), 0, server, port)

        timeout(timeout) do
          response = s.recv(65536)
          proc.call(DNSer::Packet.parse(response))
        end
      rescue Timeout::Error
        proc.call(nil)
      rescue Exception => e
        puts("There was an exception sending a query for #{hostname} to #{server}:#{port}: #{e}")
      ensure
        if(s)
          s.close()
        end
      end
    end
  end
end
