eventmachine/eventmachine

View on GitHub
lib/em/resolver.rb

Summary

Maintainability
A
2 hrs
Test Coverage
module EventMachine
  module DNS
    class Resolver

      def self.windows?
        if RUBY_PLATFORM =~ /mswin32|cygwin|mingw|bccwin/
          require 'win32/resolv'
          true
        else
          false
        end
      end

      HOSTS_FILE = windows? ? Win32::Resolv.get_hosts_path : '/etc/hosts'

      @hosts = nil
      @nameservers = nil
      @socket = nil

      def self.resolve(hostname)
        Request.new(socket, hostname)
      end

      def self.socket
        if @socket && @socket.error?
          @socket = Socket.open
        else
          @socket ||= Socket.open
        end
      end

      def self.nameservers=(ns)
        @nameservers = ns
      end

      def self.nameservers
        return @nameservers if @nameservers

        if windows?
          _, ns = Win32::Resolv.get_resolv_info
          return @nameservers = ns || []
        end

        @nameservers = []
        IO.readlines('/etc/resolv.conf').each do |line|
          if line =~ /^nameserver (.+)$/
            @nameservers << $1.split(/\s+/).first
          end
        end

        @nameservers
      rescue
        @nameservers = []
      end

      def self.nameserver
        nameservers.shuffle.first
      end

      def self.hosts
        return @hosts if @hosts

        @hosts = {}
        IO.readlines(HOSTS_FILE).each do |line|
          next if line =~ /^#/
          addr, host = line.split(/\s+/)

          next unless addr && host
          @hosts[host] ||= []
          @hosts[host] << addr
        end

        @hosts
      rescue
        @hosts = {}
      end
    end

    class RequestIdAlreadyUsed < RuntimeError; end

    class Socket < EventMachine::Connection
      def self.open
        EventMachine::open_datagram_socket('0.0.0.0', 0, self)
      end

      def initialize
        @nameserver = nil
      end

      def post_init
        @requests = {}
      end

      def start_timer
        @timer ||= EM.add_periodic_timer(0.1, &method(:tick))
      end

      def stop_timer
        EM.cancel_timer(@timer)
        @timer = nil
      end

      def unbind
      end

      def tick
        @requests.each do |id,req|
          req.tick
        end
      end

      def register_request(id, req)
        if @requests.has_key?(id)
          raise RequestIdAlreadyUsed
        else
          @requests[id] = req
        end

        start_timer
      end

      def deregister_request(id, req)
        @requests.delete(id)
        stop_timer if @requests.length == 0
      end

      def send_packet(pkt)
        send_datagram(pkt, nameserver, 53)
      end

      def nameserver=(ns)
        @nameserver = ns
      end

      def nameserver
        @nameserver || Resolver.nameserver
      end

      # Decodes the packet, looks for the request and passes the
      # response over to the requester
      def receive_data(data)
        msg = nil
        begin
          msg = Resolv::DNS::Message.decode data
        rescue
        else
          req = @requests[msg.id]
          if req
            @requests.delete(msg.id)
            stop_timer if @requests.length == 0
            req.receive_answer(msg)
          end
        end
      end
    end

    class Request
      include Deferrable
      attr_accessor :retry_interval, :max_tries

      def initialize(socket, hostname)
        @socket = socket
        @hostname = hostname
        @tries = 0
        @last_send = Time.at(0)
        @retry_interval = 3
        @max_tries = 5

        if addrs = Resolver.hosts[hostname]
          succeed addrs
        else
          EM.next_tick { tick }
        end
      end

      def tick
        # Break early if nothing to do
        return if @last_send + @retry_interval > Time.now
        if @tries < @max_tries
          send
        else
          @socket.deregister_request(@id, self)
          fail 'retries exceeded'
        end
      end

      def receive_answer(msg)
        addrs = []
        msg.each_answer do |name,ttl,data|
          if data.kind_of?(Resolv::DNS::Resource::IN::A) ||
              data.kind_of?(Resolv::DNS::Resource::IN::AAAA)
            addrs << data.address.to_s
          end
        end

        if addrs.empty?
          fail "rcode=#{msg.rcode}"
        else
          succeed addrs
        end
      end

      private

        def send
          @tries += 1
          @last_send = Time.now
          @socket.send_packet(packet.encode)
        end

        def id
          begin
            @id = rand(65535)
            @socket.register_request(@id, self)
          rescue RequestIdAlreadyUsed
            retry
          end unless defined?(@id)

          @id
        end

        def packet
          msg = Resolv::DNS::Message.new
          msg.id = id
          msg.rd = 1
          msg.add_question @hostname, Resolv::DNS::Resource::IN::A
          msg
        end

    end
  end
end