# -*- coding: binary -*-
require 'logger'
require 'net/dns/names/names'
require 'net/dns/dns'
require 'net/dns/header'
require 'net/dns/question'
require 'net/dns/rr'

module Net # :nodoc:
  module DNS 
    
    # =Name
    #
    # Net::DNS::Packet - DNS packet object class
    #
    # =Synopsis
    # 
    #   require 'net/dns/packet'
    #
    # =Description
    # 
    # The Net::DNS::Packet class represents an entire DNS packet,
    # divided in his main section: 
    # 
    # * Header (instance of Net::DNS::Header)
    # * Question (array of Net::DNS::Question objects)
    # * Answer, Authority, Additional (each formed by an array of Net::DNS::RR 
    #   objects)
    #
    # You can use this class whenever you need to create a DNS packet, whether
    # in an user application, in a resolver instance (have a look, for instance,
    # at the Net::DNS::Resolver#send method) or for a nameserver.
    #
    # Some example:
    #
    #   # Create a packet
    #   packet = Net::DNS::Packet.new("www.example.com")
    #   mx = Net::DNS::Packet.new("example.com", Net::DNS::MX)
    #
    #   # Getting packet binary data, suitable for network transmission
    #   data = packet.data
    #
    # A packet object can be created from binary data too, like an 
    # answer packet just received from a network stream:
    #
    #   packet = Net::DNS::Packet::parse(data)
    #
    # Each part of a packet can be gotten by the right accessors:
    #
    #   header = packet.header     # Instance of Net::DNS::Header class
    #   question = packet.question # Instance of Net::DNS::Question class
    #   
    #   # Iterate over additional RRs
    #   packet.additional.each do |rr|
    #     puts "Got an #{rr.type} record"
    #   end
    #
    # Some iterators have been written to easy the access of those RRs, 
    # which are often the most important. So instead of doing:
    #
    #   packet.answer.each do |rr|
    #     if rr.type == Net::DNS::RR::Types::A
    #       # do something with +rr.address+
    #     end
    #   end
    #
    # we can do:
    #
    #   packet.each_address do |ip|
    #     # do something with +ip+
    #   end
    #
    # Be sure you don't miss all the iterators in the class documentation.
    #
    # =Logging facility
    # 
    # As Net::DNS::Resolver class, Net::DNS::Packet class has its own logging
    # facility too. It work in the same way the other one do, so you can 
    # maybe want to override it or change the file descriptor.
    #
    #   packet = Net::DNS::Packet.new("www.example.com")
    #   packet.logger = $stderr
    #
    #   # or even
    #   packet.logger = Logger.new("/tmp/packet.log")
    #
    # If the Net::DNS::Packet class is directly instantiated by the Net::DNS::Resolver
    # class, like the great majority of the time, it will use the same logger facility.
    #
    # Logger level will be set to Logger::Debug if $DEBUG variable is set.
    #
    # =Error classes
    #
    # Some error classes has been defined for the Net::DNS::Packet class,
    # which are listed here to keep a light and browsable main documentation.
    # We have:
    #
    # * PacketArgumentError: Generic argument error for class Net::DNS::Packet
    # * PacketError: Generic Packet error
    #
    # =Copyright
    # 
    # Copyright (c) 2006 Marco Ceresa
    #
    # All rights reserved. This program is free software; you may redistribute 
    # it and/or modify it under the same terms as Ruby itself.
    #
    class Packet

      include Names
      
      attr_reader :header, :question, :answer, :authority, :additional
      attr_reader :answerfrom, :answersize

      # Create a new instance of Net::DNS::Packet class. Arguments are the
      # canonical name of the resource, an optional type field and an optional
      # class field. The record type and class can be omitted; they default 
      # to +A+ and +IN+.
      #
      #   packet = Net::DNS::Packet.new("www.example.com")
      #   packet = Net::DNS::Packet.new("example.com", Net::DNS::MX)
      #   packet = Net::DNS::Packet.new("example.com",Net::DNS::TXT,Net::DNS::CH)
      #
      # This class no longer instantiate object from binary data coming from
      # network streams. Please use Net::DNS::Packet.new_from_data instead.
      # 
      def initialize(name,type=Net::DNS::A,cls=Net::DNS::IN)
        @header = Net::DNS::Header.new(:qdCount => 1)
        @question = [Net::DNS::Question.new(name,type,cls)]
        @answer = []
        @authority = []
        @additional = []
        @logger = Logger.new $stdout
        @logger.level = $DEBUG ? Logger::DEBUG : Logger::WARN
      end

      # Create a new instance of Net::DNS::Packet class from binary data, taken
      # out by a network stream. For example:
      #
      #   # udp_socket is an UDPSocket waiting for a response
      #   ans = udp_socket.recvfrom(1500)
      #   packet = Net::DNS::Packet::parse(ans)
      #
      # An optional +from+ argument can be used to specify the information
      # of the sender. If data is passed as is from a Socket#recvfrom call,
      # the method will accept it.
      #
      # Be sure that your network data is clean from any UDP/TCP header, 
      # especially when using RAW sockets.
      # 
      def Packet.parse(*args)
        o = allocate
        o.send(:new_from_data, *args)
        o
      end
      

      # Checks if the packet is a QUERY packet
      def query?
        @header.opCode == Net::DNS::Header::QUERY
      end

      # Return the packet object in binary data, suitable
      # for sending across a network stream.
      #
      #   packet_data = packet.data
      #   puts "Packet is #{packet_data.size} bytes long"
      #
      def data
        qdcount=ancount=nscount=arcount=0
        data = @header.data
        headerlength = data.length
        
        @question.each do |question|
          data += question.data
          qdcount += 1
        end
        @answer.each do |rr|
          data += rr.data#(data.length)
          ancount += 1
        end
        @authority.each do |rr|
          data += rr.data#(data.length)
          nscount += 1
        end
        @additional.each do |rr|
          next if rr.nil?
          data += rr.data#(data.length)
          arcount += 1
        end
        
        @header.qdCount = qdcount
        @header.anCount = ancount
        @header.nsCount = nscount
        @header.arCount = arcount

        @header.data + data[Net::DNS::HFIXEDSZ..data.size]
      end

      # Same as Net::DNS::Packet#data, but implements name compression
      # (see RFC1025) for a considerable save of bytes.
      #
      #   packet = Net::DNS::Packet.new("www.example.com")
      #   puts "Size normal is #{packet.data.size} bytes"
      #   puts "Size compressed is #{packet.data_comp.size} bytes"
      #   
      def data_comp
        offset = 0
        compnames = {}
        qdcount=ancount=nscount=arcount=0
        data = @header.data
        headerlength = data.length

        @question.each do |question|
          str,offset,names = question.data
          data += str
          compnames.update(names)
          qdcount += 1
        end
        
        @answer.each do |rr|
          str,offset,names = rr.data(offset,compnames)
          data += str
          compnames.update(names)
          ancount += 1
        end
        
        @authority.each do |rr|
          str,offset,names = rr.data(offset,compnames)
          data += str
          compnames.update(names)
          nscount += 1
        end
        
        @additional.each do |rr|
          str,offset,names = rr.data(offset,compnames)
          data += str
          compnames.update(names)
          arcount += 1
        end
        
        @header.qdCount = qdcount
        @header.anCount = ancount
        @header.nsCount = nscount
        @header.arCount = arcount
        
        @header.data + data[Net::DNS::HFIXEDSZ..data.size]
      end
      
      # Inspect method
      def inspect
        retval = ""
        if @answerfrom != "0.0.0.0:0" and @answerfrom
          retval << ";; Answer received from #@answerfrom (#{@answersize} bytes)\n;;\n"
        end
        
        retval << ";; HEADER SECTION\n"
        retval << @header.inspect
        
        retval << "\n"
        section = (@header.opCode == "UPDATE") ? "ZONE" : "QUESTION"
        retval << ";; #{section} SECTION (#{@header.qdCount} record#{@header.qdCount == 1 ? '' : 's'}):\n"
        @question.each do |qr|
          retval << ";; " + qr.inspect + "\n"
        end

        unless @answer.size == 0
          retval << "\n"
          section = (@header.opCode == "UPDATE") ? "PREREQUISITE" : "ANSWER"
          retval << ";; #{section} SECTION (#{@header.anCount} record#{@header.anCount == 1 ? '' : 's'}):\n"
          @answer.each do |rr|
            retval << rr.inspect + "\n"
          end
        end

        unless @authority.size == 0
          retval << "\n"
          section = (@header.opCode == "UPDATE") ? "UPDATE" : "AUTHORITY"
          retval << ";; #{section} SECTION (#{@header.nsCount} record#{@header.nsCount == 1 ? '' : 's'}):\n"
          @authority.each do |rr|
            retval << rr.inspect + "\n"
          end
        end
        
        unless @additional.size == 0
          retval << "\n"
          retval << ";; ADDITIONAL SECTION (#{@header.arCount} record#{@header.arCount == 1 ? '' : 's'}):\n"
          @additional.each do |rr|
            retval << rr.inspect + "\n"
          end
        end
        
        retval
      end

      
      # Wrapper to Header#truncated?
      #
      def truncated?
        @header.truncated?
      end
            
      # Assign a Net::DNS::Header object to a Net::DNS::Packet 
      # instance.
      #
      def header=(object)
        if object.kind_of? Net::DNS::Header
          @header = object
        else
          raise PacketArgumentError, "Argument must be a Net::DNS::Header object"
        end
      end
      
      # Assign a Net::DNS::Question object, or an array of 
      # Questions objects, to a Net::DNS::Packet instance.
      #
      def question=(object)
        case object
        when Array
          if object.all? {|x| x.kind_of? Net::DNS::Question}
            @question = object
          else
            raise PacketArgumentError, "Some of the elements is not an Net::DNS::Question object"
          end
        when Net::DNS::Question
          @question = [object]
        else
          raise PacketArgumentError, "Invalid argument, not a Question object nor an array of objects"
        end
      end

      # Assign a Net::DNS::RR object, or an array of 
      # RR objects, to a Net::DNS::Packet instance answer 
      # section.
      #
      def answer=(object)
        case object
        when Array
          if object.all? {|x| x.kind_of? Net::DNS::RR}
            @answer = object
          else
            raise PacketArgumentError, "Some of the elements is not an Net::DNS::RR object"
          end
        when Net::DNS::RR
          @answer = [object]
        else
          raise PacketArgumentError, "Invalid argument, not a RR object nor an array of objects"
        end
      end

      # Assign a Net::DNS::RR object, or an array of 
      # RR objects, to a Net::DNS::Packet instance additional 
      # section.
      #
      def additional=(object)
        case object
        when Array
          if object.all? {|x| x.kind_of? Net::DNS::RR}
            @additional = object
          else
            raise PacketArgumentError, "Some of the elements is not an Net::DNS::RR object"
          end
        when Net::DNS::RR
          @additional = [object]
        else
          raise PacketArgumentError, "Invalid argument, not a RR object nor an array of objects"
        end
      end

      # Assign a Net::DNS::RR object, or an array of 
      # RR objects, to a Net::DNS::Packet instance authority 
      # section.
      #
      def authority=(object)
        case object
        when Array
          if object.all? {|x| x.kind_of? Net::DNS::RR}
            @authority = object
          else
            raise PacketArgumentError, "Some of the elements is not an Net::DNS::RR object"
          end
        when Net::DNS::RR
          @authority = [object]
        else
          raise PacketArgumentError, "Invalid argument, not a RR object nor an array of objects"
        end
      end
      
      # Iterate for every address in the +answer+ section of a 
      # Net::DNS::Packet object.
      #
      #   packet.each_address do |ip|
      #     ping ip.to_s
      #   end
      #
      # As you can see in the documentation for Net::DNS::RR::A class,
      # the address returned is an instance of IPAddr class. 
      #
      def each_address
        @answer.each do |elem|
          next unless elem.class == Net::DNS::RR::A
          yield elem.address
        end
      end
      
      # Iterate for every nameserver in the +answer+ section of a 
      # Net::DNS::Packet object.
      #
      #   packet.each_nameserver do |ns|
      #     puts "Nameserver found: #{ns}"
      #   end
      #
      def each_nameserver
        @answer.each do |elem|
          next unless elem.class == Net::DNS::RR::NS
          yield elem.nsdname
        end
      end
      
      # Iterate for every exchange record in the +answer+ section 
      # of a Net::DNS::Packet object.
      #
      #   packet.each_mx do |pref,name|
      #     puts "Mail exchange #{name} has preference #{pref}"
      #   end
      #
      def each_mx
        @answer.each do |elem|
          next unless elem.class == Net::DNS::RR::MX
          yield elem.preference,elem.exchange
        end
      end
      
      # Iterate for every canonical name in the +answer+ section 
      # of a Net::DNS::Packet object.
      #
      #   packet.each_cname do |cname|
      #     puts "Canonical name: #{cname}"
      #   end
      #
      def each_cname
        @answer.each do |elem|
          next unless elem.class == Net::DNS::RR::CNAME
          yield elem.cname
        end
      end
      
      # Iterate for every pointer in the +answer+ section of a 
      # Net::DNS::Packet object.
      #
      #   packet.each_ptr do |ptr|
      #     puts "Pointer for resource: #{ptr}"
      #   end
      #
      def each_ptr
        @answer.each do |elem|
          next unless elem.class == Net::DNS::RR::PTR
          yield elem.ptrdname
        end
      end

      # Chacks whether a query has returned a NXDOMAIN error,
      # meaning the domain name queried doesn't exists.
      #
      #   %w[a.com google.com ibm.com d.com].each do |domain|
      #     response = Net::DNS::Resolver.new.send(domain)
      #     puts "#{domain} doesn't exist" if response.nxdomain?
      #   end
      #     #=> a.com doesn't exist
      #     #=> d.com doesn't exist
      #
      def nxdomain?
        header.rCode == Net::DNS::Header::NAME
      end
      
      private

      # New packet from binary data
      def new_from_data(data, from = nil)
        unless from
          if data.kind_of? Array
            data,from = data
          else
            from = [0,0,"0.0.0.0","unknown"]
          end
        end
          
        @answerfrom = from[2] + ":" + from[1].to_s
        @answersize = data.size
        @logger = Logger.new $stdout
        @logger.level = $DEBUG ? Logger::DEBUG : Logger::WARN
        
        #------------------------------------------------------------
        # Header section
        #------------------------------------------------------------
        offset = Net::DNS::HFIXEDSZ 
        @header = Net::DNS::Header.parse(data[0..offset-1])

        @logger.debug ";; HEADER SECTION"
        @logger.debug @header.inspect

        #------------------------------------------------------------
        # Question section
        #------------------------------------------------------------
        section = @header.opCode == "UPDATE" ? "ZONE" : "QUESTION"
        @logger.debug ";; #{section} SECTION (#{@header.qdCount} record#{@header.qdCount == 1 ? '': 's'})"

        @question = []
        @header.qdCount.times do
          qobj,offset = parse_question(data,offset)
          @question << qobj
          @logger.debug ";; #{qobj.inspect}"
        end

        #------------------------------------------------------------
        # Answer/prerequisite section
        #------------------------------------------------------------
        section = @header.opCode == "UPDATE" ? "PREREQUISITE" : "ANSWER"
        @logger.debug ";; #{section} SECTION (#{@header.qdCount} record#{@header.qdCount == 1 ? '': 's'})"
        
        @answer = []
        @header.anCount.times do
          if (rrobj, new_offset = Net::DNS::RR.parse_packet(data, offset))
            @answer << rrobj
            @logger.debug rrobj.inspect
            offset = new_offset
          else
            @logger.warn "Failed to parse RR packet from offset: #{offset}"
            _, offset = dn_expand(data, offset)
            _, _, _, rdlength = data.unpack("@#{offset} n2 N n")
            offset += RRFIXEDSZ + rdlength
          end
        end

        #------------------------------------------------------------
        # Authority/update section
        #------------------------------------------------------------
        section = @header.opCode == "UPDATE" ? "UPDATE" : "AUTHORITY"
        @logger.debug ";; #{section} SECTION (#{@header.nsCount} record#{@header.nsCount == 1 ? '': 's'})"
        
        @authority = []
        @header.nsCount.times do
          rrobj,offset = Net::DNS::RR.parse_packet(data,offset)
          @authority << rrobj
          @logger.debug rrobj.inspect          
        end
        
        #------------------------------------------------------------
        # Additional section
        #------------------------------------------------------------
        @logger.debug ";; ADDITIONAL SECTION (#{@header.arCount} record#{@header.arCount == 1 ? '': 's'})"    
        
        @additional = []
        @header.arCount.times do
          rrobj,offset = Net::DNS::RR.parse_packet(data,offset)
          @additional << rrobj
          @logger.debug rrobj.inspect
        end
        
      end # new_from_data
      
      
      # Parse question section
      def parse_question(data,offset)
        size = (dn_expand(data,offset)[1]-offset) + 2*Net::DNS::INT16SZ
        return [Net::DNS::Question.parse(data[offset,size]), offset+size]
      rescue StandardError => err
        raise PacketError, "Caught exception, maybe packet malformed => #{err}"
      end

    end # class Packet
    
  end # module DNS
end # module Net

class PacketError < StandardError # :nodoc:
end
class PacketArgumentError < ArgumentError # :nodoc:
end
