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