PageRenderTime 52ms CodeModel.GetById 23ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/cool.io/dns_resolver.rb

http://github.com/tarcieri/cool.io
Ruby | 219 lines | 121 code | 37 blank | 61 comment | 14 complexity | 5f1593e299868221380f12f580cae61c MD5 | raw file
Possible License(s): BSD-3-Clause, BSD-2-Clause
  1. #--
  2. # Copyright (C)2007-10 Tony Arcieri
  3. # You can redistribute this under the terms of the Ruby license
  4. # See file LICENSE for details
  5. #
  6. # Gimpy hacka asynchronous DNS resolver
  7. #
  8. # Word to the wise: I don't know what I'm doing here. This was cobbled together
  9. # as best I could with extremely limited knowledge of the DNS format. There's
  10. # obviously a ton of stuff it doesn't support (like IPv6 and TCP).
  11. #
  12. # If you do know what you're doing with DNS, feel free to improve this!
  13. # A good starting point my be this EventMachine Net::DNS-based asynchronous
  14. # resolver:
  15. #
  16. # http://gist.github.com/663299
  17. #
  18. #++
  19. require 'resolv'
  20. module Coolio
  21. # A non-blocking DNS resolver. It provides interfaces for querying both
  22. # /etc/hosts and nameserves listed in /etc/resolv.conf, or nameservers of
  23. # your choosing.
  24. #
  25. # Presently the client only supports UDP requests against your nameservers
  26. # and cannot resolve anything with records larger than 512-bytes. Also,
  27. # IPv6 is not presently supported.
  28. #
  29. # DNSResolver objects are one-shot. Once they resolve a domain name they
  30. # automatically detach themselves from the event loop and cannot be used
  31. # again.
  32. class DNSResolver < IOWatcher
  33. #--
  34. DNS_PORT = 53
  35. DATAGRAM_SIZE = 512
  36. TIMEOUT = 3 # Retry timeout for each datagram sent
  37. RETRIES = 4 # Number of retries to attempt
  38. # so currently total is 12s before it will err due to timeouts
  39. # if it errs due to inability to reach the DNS server [Errno::EHOSTUNREACH], same
  40. # Query /etc/hosts (or the specified hostfile) for the given host
  41. def self.hosts(host, hostfile = Resolv::Hosts::DefaultFileName)
  42. hosts = {}
  43. File.open(hostfile) do |f|
  44. f.each_line do |host_entry|
  45. entries = host_entry.gsub(/#.*$/, '').gsub(/\s+/, ' ').split(' ')
  46. addr = entries.shift
  47. entries.each { |e| hosts[e] ||= addr }
  48. end
  49. end
  50. hosts[host]
  51. end
  52. # Create a new Coolio::Watcher descended object to resolve the
  53. # given hostname. If you so desire you can also specify a
  54. # list of nameservers to query. By default the resolver will
  55. # use nameservers listed in /etc/resolv.conf
  56. def initialize(hostname, *nameservers)
  57. if nameservers.empty?
  58. nameservers = Resolv::DNS::Config.default_config_hash[:nameserver]
  59. raise RuntimeError, "no nameservers found" if nameservers.empty? # TODO just call resolve_failed, not raise [also handle Errno::ENOENT)]
  60. end
  61. @nameservers = nameservers
  62. @question = request_question hostname
  63. @socket = UDPSocket.new
  64. @timer = Timeout.new(self)
  65. super(@socket)
  66. end
  67. # Attach the DNSResolver to the given event loop
  68. def attach(evloop)
  69. send_request
  70. @timer.attach(evloop)
  71. super
  72. end
  73. # Detach the DNSResolver from the given event loop
  74. def detach
  75. @timer.detach if @timer.attached?
  76. super
  77. end
  78. # Called when the name has successfully resolved to an address
  79. def on_success(address); end
  80. event_callback :on_success
  81. # Called when we receive a response indicating the name didn't resolve
  82. def on_failure; end
  83. event_callback :on_failure
  84. # Called if we don't receive a response, defaults to calling on_failure
  85. def on_timeout
  86. on_failure
  87. end
  88. #########
  89. protected
  90. #########
  91. # Send a request to the DNS server
  92. def send_request
  93. nameserver = @nameservers.shift
  94. @nameservers << nameserver # rotate them
  95. begin
  96. @socket.send request_message, 0, @nameservers.first, DNS_PORT
  97. rescue Errno::EHOSTUNREACH # TODO figure out why it has to be wrapper here, when the other wrapper should be wrapping this one!
  98. end
  99. end
  100. # Called by the subclass when the DNS response is available
  101. def on_readable
  102. datagram = nil
  103. begin
  104. datagram = @socket.recvfrom_nonblock(DATAGRAM_SIZE).first
  105. rescue Errno::ECONNREFUSED
  106. end
  107. address = response_address datagram rescue nil
  108. address ? on_success(address) : on_failure
  109. detach
  110. end
  111. def request_question(hostname)
  112. raise ArgumentError, "hostname cannot be nil" if hostname.nil?
  113. # Query name
  114. message = hostname.split('.').map { |s| [s.size].pack('C') << s }.join + "\0"
  115. # Host address query
  116. qtype = 1
  117. # Internet query
  118. qclass = 1
  119. message << [qtype, qclass].pack('nn')
  120. end
  121. def request_message
  122. # Standard query header
  123. message = [2, 1, 0].pack('nCC')
  124. # One entry
  125. qdcount = 1
  126. # No answer, authority, or additional records
  127. ancount = nscount = arcount = 0
  128. message << [qdcount, ancount, nscount, arcount].pack('nnnn')
  129. message << @question
  130. end
  131. def response_address(message)
  132. # Confirm the ID field
  133. id = message[0..1].unpack('n').first.to_i
  134. return unless id == 2
  135. # Check the QR value and confirm this message is a response
  136. qr = message[2..2].unpack('B1').first.to_i
  137. return unless qr == 1
  138. # Check the RCODE (lower nibble) and ensure there wasn't an error
  139. rcode = message[3..3].unpack('B8').first[4..7].to_i(2)
  140. return unless rcode == 0
  141. # Extract the question and answer counts
  142. qdcount, _ancount = message[4..7].unpack('nn').map { |n| n.to_i }
  143. # We only asked one question
  144. return unless qdcount == 1
  145. message.slice!(0, 12)
  146. # Make sure it's the same question
  147. return unless message[0..(@question.size-1)] == @question
  148. message.slice!(0, @question.size)
  149. # Extract the RDLENGTH
  150. while not message.empty?
  151. type = message[2..3].unpack('n').first.to_i
  152. rdlength = message[10..11].unpack('n').first.to_i
  153. rdata = message[12..(12 + rdlength - 1)]
  154. message.slice!(0, 12 + rdlength)
  155. # Only IPv4 supported
  156. next unless rdlength == 4
  157. # If we got an Internet address back, return it
  158. return rdata.unpack('CCCC').join('.') if type == 1
  159. end
  160. nil
  161. end
  162. class Timeout < TimerWatcher
  163. def initialize(resolver)
  164. @resolver = resolver
  165. @attempts = 0
  166. super(TIMEOUT, true)
  167. end
  168. def on_timer
  169. @attempts += 1
  170. if @attempts <= RETRIES
  171. begin
  172. return @resolver.__send__(:send_request)
  173. rescue Errno::EHOSTUNREACH # if the DNS is toast try again after the timeout occurs again
  174. return nil
  175. end
  176. end
  177. @resolver.__send__(:on_timeout)
  178. @resolver.detach
  179. end
  180. end
  181. end
  182. end