lib/rubysl/resolv/resolv.rb in rubysl-resolv-2.0.0 vs lib/rubysl/resolv/resolv.rb in rubysl-resolv-2.1.0
- old
+ new
@@ -7,11 +7,11 @@
require 'securerandom'
rescue LoadError
end
# Resolv is a thread-aware DNS resolver library written in Ruby. Resolv can
-# handle multiple DNS requests concurrently without blocking the entire ruby
+# handle multiple DNS requests concurrently without blocking the entire Ruby
# interpreter.
#
# See also resolv-replace.rb to replace the libc resolver with Resolv.
#
# Resolv can look up various DNS resources using the DNS module directly.
@@ -163,14 +163,15 @@
##
# Resolv::Hosts is a hostname resolver that uses the system hosts file.
class Hosts
- if /mswin|mingw|bccwin/ =~ RUBY_PLATFORM
+ begin
+ raise LoadError unless /mswin|mingw|cygwin/ =~ RUBY_PLATFORM
require 'win32/resolv'
DefaultFileName = Win32::Resolv.get_hosts_path
- else
+ rescue LoadError
DefaultFileName = '/etc/hosts'
end
##
# Creates a new Resolv::Hosts, using +filename+ for its data source.
@@ -184,11 +185,11 @@
def lazy_initialize # :nodoc:
@mutex.synchronize {
unless @initialized
@name2addr = {}
@addr2name = {}
- open(@filename) {|f|
+ open(@filename, 'rb') {|f|
f.each {|line|
line.sub!(/#.*/, '')
addr, hostname, *aliases = line.split(/\s+/)
next unless addr
addr.untaint
@@ -504,21 +505,28 @@
##
# Iterates over all +typeclass+ DNS resources for +name+. See
# #getresource for argument details.
def each_resource(name, typeclass, &proc)
+ fetch_resource(name, typeclass) {|reply, reply_name|
+ extract_resources(reply, reply_name, typeclass, &proc)
+ }
+ end
+
+ def fetch_resource(name, typeclass)
lazy_initialize
requester = make_udp_requester
senders = {}
begin
@config.resolv(name) {|candidate, tout, nameserver, port|
msg = Message.new
msg.rd = 1
msg.add_question(candidate, typeclass)
unless sender = senders[[candidate, nameserver, port]]
- sender = senders[[candidate, nameserver, port]] =
- requester.sender(msg, candidate, nameserver, port)
+ sender = requester.sender(msg, candidate, nameserver, port)
+ next if !sender
+ senders[[candidate, nameserver, port]] = sender
end
reply, reply_name = requester.request(sender, tout)
case reply.rcode
when RCode::NoError
if reply.tc == 1 and not Requester::TCP === requester
@@ -530,11 +538,11 @@
# current candidate does not already respond successfully via
# TCP). This makes sense because we already know the full
# response will not fit in an untruncated UDP packet.
redo
else
- extract_resources(reply, reply_name, typeclass, &proc)
+ yield(reply, reply_name)
end
return
when RCode::NXDomain
raise Config::NXDomain.new(reply_name.to_s)
else
@@ -643,11 +651,13 @@
def self.bind_random_port(udpsock, bind_host="0.0.0.0") # :nodoc:
begin
port = rangerand(1024..65535)
udpsock.bind(bind_host, port)
- rescue Errno::EADDRINUSE
+ rescue Errno::EADDRINUSE, # POSIX
+ Errno::EACCES, # SunOS: See PRIV_SYS_NFS in privileges(5)
+ Errno::EPERM # FreeBSD: security.mac.portacl.port_high is configurable. See mac_portacl(4).
retry
end
end
class Requester # :nodoc:
@@ -657,11 +667,16 @@
end
def request(sender, tout)
start = Time.now
timelimit = start + tout
- sender.send
+ begin
+ sender.send
+ rescue Errno::EHOSTUNREACH
+ # multi-homed IPv6 may generate this
+ raise ResolvTimeout
+ end
while true
before_select = Time.now
timeout = timelimit - before_select
if timeout <= 0
raise ResolvTimeout
@@ -683,19 +698,23 @@
begin
msg = Message.decode(reply)
rescue DecodeError
next # broken DNS message ignored
end
- if s = @senders[[from,msg.id]]
+ if s = sender_for(from, msg)
break
else
# unexpected DNS message ignored
end
end
return msg, s.data
end
+ def sender_for(addr, msg)
+ @senders[[addr,msg.id]]
+ end
+
def close
socks = @socks
@socks = nil
if socks
socks.each {|sock| sock.close }
@@ -723,13 +742,16 @@
else
bind_host = "0.0.0.0"
af = Socket::AF_INET
end
next if @socks_hash[bind_host]
- sock = UDPSocket.new(af)
+ begin
+ sock = UDPSocket.new(af)
+ rescue Errno::EAFNOSUPPORT
+ next # The kernel doesn't support the address family.
+ end
sock.do_not_reverse_lookup = true
- sock.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC) if defined? Fcntl::F_SETFD
DNS.bind_random_port(sock, bind_host)
@socks << sock
@socks_hash[bind_host] = sock
}
end
@@ -738,15 +760,16 @@
reply, from = readable_socks[0].recvfrom(UDPSize)
return reply, [from[3],from[1]]
end
def sender(msg, data, host, port=Port)
+ sock = @socks_hash[host.index(':') ? "::" : "0.0.0.0"]
+ return nil if !sock
service = [host, port]
id = DNS.allocate_request_id(host, port)
request = msg.encode
request[0,2] = [id].pack('n')
- sock = @socks_hash[host.index(':') ? "::" : "0.0.0.0"]
return @senders[[service, id]] =
Sender.new(request, data, sock, host, port)
end
def close
@@ -763,10 +786,11 @@
@port = port
end
attr_reader :data
def send
+ raise "@sock is nil." if @sock.nil?
@sock.send(@msg, 0, @host, @port)
end
end
end
@@ -777,11 +801,10 @@
@port = port
is_ipv6 = host.index(':')
sock = UDPSocket.new(is_ipv6 ? Socket::AF_INET6 : Socket::AF_INET)
@socks = [sock]
sock.do_not_reverse_lookup = true
- sock.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC) if defined? Fcntl::F_SETFD
DNS.bind_random_port(sock, is_ipv6 ? "::" : "0.0.0.0")
sock.connect(host, port)
end
def recv_reply(readable_socks)
@@ -806,24 +829,39 @@
}
end
class Sender < Requester::Sender # :nodoc:
def send
+ raise "@sock is nil." if @sock.nil?
@sock.send(@msg, 0)
end
attr_reader :data
end
end
+ class MDNSOneShot < UnconnectedUDP # :nodoc:
+ def sender(msg, data, host, port=Port)
+ id = DNS.allocate_request_id(host, port)
+ request = msg.encode
+ request[0,2] = [id].pack('n')
+ sock = @socks_hash[host.index(':') ? "::" : "0.0.0.0"]
+ return @senders[id] =
+ UnconnectedUDP::Sender.new(request, data, sock, host, port)
+ end
+
+ def sender_for(addr, msg)
+ @senders[msg.id]
+ end
+ end
+
class TCP < Requester # :nodoc:
def initialize(host, port=Port)
super()
@host = host
@port = port
sock = TCPSocket.new(@host, @port)
@socks = [sock]
- sock.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC) if defined? Fcntl::F_SETFD
@senders = {}
end
def recv_reply(readable_socks)
len = readable_socks[0].read(2).unpack('n')[0]
@@ -875,11 +913,11 @@
def timeouts=(values)
if values
values = Array(values)
values.each do |t|
Numeric === t or raise ArgumentError, "#{t.inspect} is not numeric"
- t > 0.0 or raise ArgumentError, "timeout=#{t} must be postive"
+ t > 0.0 or raise ArgumentError, "timeout=#{t} must be positive"
end
@timeouts = values
else
@timeouts = nil
end
@@ -887,11 +925,11 @@
def Config.parse_resolv_conf(filename)
nameserver = []
search = nil
ndots = 1
- open(filename) {|f|
+ open(filename, 'rb') {|f|
f.each {|line|
line.sub!(/[#;].*/, '')
keyword, *args = line.split(/\s+/)
args.each { |arg|
arg.untaint
@@ -1487,10 +1525,11 @@
@limit = save_limit
return d
end
def get_bytes(len = @limit - @index)
+ raise DecodeError.new("limit exceeded") if @limit < @index + len
d = @data[@index, len]
@index += len
return d
end
@@ -1514,10 +1553,11 @@
@index += len
return arr
end
def get_string
+ raise DecodeError.new("limit exceeded") if @limit <= @index
len = @data[@index].ord
raise DecodeError.new("limit exceeded") if @limit < @index + 1 + len
d = @data[@index + 1, len]
@index += 1 + len
return d
@@ -1533,33 +1573,37 @@
def get_name
return Name.new(self.get_labels)
end
- def get_labels(limit=nil)
- limit = @index if !limit || @index < limit
+ def get_labels
+ prev_index = @index
+ save_index = nil
d = []
while true
+ raise DecodeError.new("limit exceeded") if @limit <= @index
case @data[@index].ord
when 0
@index += 1
+ if save_index
+ @index = save_index
+ end
return d
when 192..255
idx = self.get_unpack('n')[0] & 0x3fff
- if limit <= idx
+ if prev_index <= idx
raise DecodeError.new("non-backward name pointer")
end
- save_index = @index
+ prev_index = idx
+ if !save_index
+ save_index = @index
+ end
@index = idx
- d += self.get_labels(limit)
- @index = save_index
- return d
else
d << self.get_label
end
end
- return d
end
def get_label
return Label::Str.new(self.get_string)
end
@@ -1930,14 +1974,14 @@
# Returns an Array of Strings for this TXT record.
attr_reader :strings
##
- # Returns the first string from +strings+.
+ # Returns the concatenated string from +strings+.
def data
- @strings[0]
+ @strings.join("")
end
def encode_rdata(msg) # :nodoc:
msg.put_string_list(@strings)
end
@@ -1947,18 +1991,109 @@
return self.new(*strings)
end
end
##
+ # Location resource
+
+ class LOC < Resource
+
+ TypeValue = 29 # :nodoc:
+
+ def initialize(version, ssize, hprecision, vprecision, latitude, longitude, altitude)
+ @version = version
+ @ssize = Resolv::LOC::Size.create(ssize)
+ @hprecision = Resolv::LOC::Size.create(hprecision)
+ @vprecision = Resolv::LOC::Size.create(vprecision)
+ @latitude = Resolv::LOC::Coord.create(latitude)
+ @longitude = Resolv::LOC::Coord.create(longitude)
+ @altitude = Resolv::LOC::Alt.create(altitude)
+ end
+
+ ##
+ # Returns the version value for this LOC record which should always be 00
+
+ attr_reader :version
+
+ ##
+ # The spherical size of this LOC
+ # in meters using scientific notation as 2 integers of XeY
+
+ attr_reader :ssize
+
+ ##
+ # The horizontal precision using ssize type values
+ # in meters using scientific notation as 2 integers of XeY
+ # for precision use value/2 e.g. 2m = +/-1m
+
+ attr_reader :hprecision
+
+ ##
+ # The vertical precision using ssize type values
+ # in meters using scientific notation as 2 integers of XeY
+ # for precision use value/2 e.g. 2m = +/-1m
+
+ attr_reader :vprecision
+
+ ##
+ # The latitude for this LOC where 2**31 is the equator
+ # in thousandths of an arc second as an unsigned 32bit integer
+
+ attr_reader :latitude
+
+ ##
+ # The longitude for this LOC where 2**31 is the prime meridian
+ # in thousandths of an arc second as an unsigned 32bit integer
+
+ attr_reader :longitude
+
+ ##
+ # The altitude of the LOC above a reference sphere whose surface sits 100km below the WGS84 spheroid
+ # in centimeters as an unsigned 32bit integer
+
+ attr_reader :altitude
+
+
+ def encode_rdata(msg) # :nodoc:
+ msg.put_bytes(@version)
+ msg.put_bytes(@ssize.scalar)
+ msg.put_bytes(@hprecision.scalar)
+ msg.put_bytes(@vprecision.scalar)
+ msg.put_bytes(@latitude.coordinates)
+ msg.put_bytes(@longitude.coordinates)
+ msg.put_bytes(@altitude.altitude)
+ end
+
+ def self.decode_rdata(msg) # :nodoc:
+ version = msg.get_bytes(1)
+ ssize = msg.get_bytes(1)
+ hprecision = msg.get_bytes(1)
+ vprecision = msg.get_bytes(1)
+ latitude = msg.get_bytes(4)
+ longitude = msg.get_bytes(4)
+ altitude = msg.get_bytes(4)
+ return self.new(
+ version,
+ Resolv::LOC::Size.new(ssize),
+ Resolv::LOC::Size.new(hprecision),
+ Resolv::LOC::Size.new(vprecision),
+ Resolv::LOC::Coord.new(latitude,"lat"),
+ Resolv::LOC::Coord.new(longitude,"lon"),
+ Resolv::LOC::Alt.new(altitude)
+ )
+ end
+ end
+
+ ##
# A Query type requesting any RR.
class ANY < Query
TypeValue = 255 # :nodoc:
end
ClassInsensitiveTypes = [ # :nodoc:
- NS, CNAME, SOA, PTR, HINFO, MINFO, MX, TXT, ANY
+ NS, CNAME, SOA, PTR, HINFO, MINFO, MX, TXT, LOC, ANY
]
##
# module IN contains ARPA Internet specific RRs.
@@ -2380,16 +2515,317 @@
return @address.hash
end
end
##
+ # Resolv::MDNS is a one-shot Multicast DNS (mDNS) resolver. It blindly
+ # makes queries to the mDNS addresses without understanding anything about
+ # multicast ports.
+ #
+ # Information taken form the following places:
+ #
+ # * RFC 6762
+
+ class MDNS < DNS
+
+ ##
+ # Default mDNS Port
+
+ Port = 5353
+
+ ##
+ # Default IPv4 mDNS address
+
+ AddressV4 = '224.0.0.251'
+
+ ##
+ # Default IPv6 mDNS address
+
+ AddressV6 = 'ff02::fb'
+
+ ##
+ # Default mDNS addresses
+
+ Addresses = [
+ [AddressV4, Port],
+ [AddressV6, Port],
+ ]
+
+ ##
+ # Creates a new one-shot Multicast DNS (mDNS) resolver.
+ #
+ # +config_info+ can be:
+ #
+ # nil::
+ # Uses the default mDNS addresses
+ #
+ # Hash::
+ # Must contain :nameserver or :nameserver_port like
+ # Resolv::DNS#initialize.
+
+ def initialize(config_info=nil)
+ if config_info then
+ super({ nameserver_port: Addresses }.merge(config_info))
+ else
+ super(nameserver_port: Addresses)
+ end
+ end
+
+ ##
+ # Iterates over all IP addresses for +name+ retrieved from the mDNS
+ # resolver, provided name ends with "local". If the name does not end in
+ # "local" no records will be returned.
+ #
+ # +name+ can be a Resolv::DNS::Name or a String. Retrieved addresses will
+ # be a Resolv::IPv4 or Resolv::IPv6
+
+ def each_address(name)
+ name = Resolv::DNS::Name.create(name)
+
+ return unless name.to_a.last == 'local'
+
+ super(name)
+ end
+
+ def make_udp_requester # :nodoc:
+ nameserver_port = @config.nameserver_port
+ Requester::MDNSOneShot.new(*nameserver_port)
+ end
+
+ end
+
+ module LOC
+
+ ##
+ # A Resolv::LOC::Size
+
+ class Size
+
+ Regex = /^(\d+\.*\d*)[m]$/
+
+ ##
+ # Creates a new LOC::Size from +arg+ which may be:
+ #
+ # LOC::Size:: returns +arg+.
+ # String:: +arg+ must match the LOC::Size::Regex constant
+
+ def self.create(arg)
+ case arg
+ when Size
+ return arg
+ when String
+ scalar = ''
+ if Regex =~ arg
+ scalar = [(($1.to_f*(1e2)).to_i.to_s[0].to_i*(2**4)+(($1.to_f*(1e2)).to_i.to_s.length-1))].pack("C")
+ else
+ raise ArgumentError.new("not a properly formed Size string: " + arg)
+ end
+ return Size.new(scalar)
+ else
+ raise ArgumentError.new("cannot interpret as Size: #{arg.inspect}")
+ end
+ end
+
+ def initialize(scalar)
+ @scalar = scalar
+ end
+
+ ##
+ # The raw size
+
+ attr_reader :scalar
+
+ def to_s # :nodoc:
+ s = @scalar.unpack("H2").join.to_s
+ return ((s[0].to_i)*(10**(s[1].to_i-2))).to_s << "m"
+ end
+
+ def inspect # :nodoc:
+ return "#<#{self.class} #{self.to_s}>"
+ end
+
+ def ==(other) # :nodoc:
+ return @scalar == other.scalar
+ end
+
+ def eql?(other) # :nodoc:
+ return self == other
+ end
+
+ def hash # :nodoc:
+ return @scalar.hash
+ end
+
+ end
+
+ ##
+ # A Resolv::LOC::Coord
+
+ class Coord
+
+ Regex = /^(\d+)\s(\d+)\s(\d+\.\d+)\s([NESW])$/
+
+ ##
+ # Creates a new LOC::Coord from +arg+ which may be:
+ #
+ # LOC::Coord:: returns +arg+.
+ # String:: +arg+ must match the LOC::Coord::Regex constant
+
+ def self.create(arg)
+ case arg
+ when Coord
+ return arg
+ when String
+ coordinates = ''
+ if Regex =~ arg && $1<180
+ hemi = ($4[/([NE])/,1]) || ($4[/([SW])/,1]) ? 1 : -1
+ coordinates = [(($1.to_i*(36e5))+($2.to_i*(6e4))+($3.to_f*(1e3)))*hemi+(2**31)].pack("N")
+ (orientation ||= '') << $4[[/NS/],1] ? 'lat' : 'lon'
+ else
+ raise ArgumentError.new("not a properly formed Coord string: " + arg)
+ end
+ return Coord.new(coordinates,orientation)
+ else
+ raise ArgumentError.new("cannot interpret as Coord: #{arg.inspect}")
+ end
+ end
+
+ def initialize(coordinates,orientation)
+ unless coordinates.kind_of?(String)
+ raise ArgumentError.new("Coord must be a 32bit unsigned integer in hex format: #{coordinates.inspect}")
+ end
+ unless orientation.kind_of?(String) && orientation[/^lon$|^lat$/]
+ raise ArgumentError.new('Coord expects orientation to be a String argument of "lat" or "lon"')
+ end
+ @coordinates = coordinates
+ @orientation = orientation
+ end
+
+ ##
+ # The raw coordinates
+
+ attr_reader :coordinates
+
+ ## The orientation of the hemisphere as 'lat' or 'lon'
+
+ attr_reader :orientation
+
+ def to_s # :nodoc:
+ c = @coordinates.unpack("N").join.to_i
+ val = (c - (2**31)).abs
+ fracsecs = (val % 1e3).to_i.to_s
+ val = val / 1e3
+ secs = (val % 60).to_i.to_s
+ val = val / 60
+ mins = (val % 60).to_i.to_s
+ degs = (val / 60).to_i.to_s
+ posi = (c >= 2**31)
+ case posi
+ when true
+ hemi = @orientation[/^lat$/] ? "N" : "E"
+ else
+ hemi = @orientation[/^lon$/] ? "W" : "S"
+ end
+ return degs << " " << mins << " " << secs << "." << fracsecs << " " << hemi
+ end
+
+ def inspect # :nodoc:
+ return "#<#{self.class} #{self.to_s}>"
+ end
+
+ def ==(other) # :nodoc:
+ return @coordinates == other.coordinates
+ end
+
+ def eql?(other) # :nodoc:
+ return self == other
+ end
+
+ def hash # :nodoc:
+ return @coordinates.hash
+ end
+
+ end
+
+ ##
+ # A Resolv::LOC::Alt
+
+ class Alt
+
+ Regex = /^([+-]*\d+\.*\d*)[m]$/
+
+ ##
+ # Creates a new LOC::Alt from +arg+ which may be:
+ #
+ # LOC::Alt:: returns +arg+.
+ # String:: +arg+ must match the LOC::Alt::Regex constant
+
+ def self.create(arg)
+ case arg
+ when Alt
+ return arg
+ when String
+ altitude = ''
+ if Regex =~ arg
+ altitude = [($1.to_f*(1e2))+(1e7)].pack("N")
+ else
+ raise ArgumentError.new("not a properly formed Alt string: " + arg)
+ end
+ return Alt.new(altitude)
+ else
+ raise ArgumentError.new("cannot interpret as Alt: #{arg.inspect}")
+ end
+ end
+
+ def initialize(altitude)
+ @altitude = altitude
+ end
+
+ ##
+ # The raw altitude
+
+ attr_reader :altitude
+
+ def to_s # :nodoc:
+ a = @altitude.unpack("N").join.to_i
+ return ((a.to_f/1e2)-1e5).to_s + "m"
+ end
+
+ def inspect # :nodoc:
+ return "#<#{self.class} #{self.to_s}>"
+ end
+
+ def ==(other) # :nodoc:
+ return @altitude == other.altitude
+ end
+
+ def eql?(other) # :nodoc:
+ return self == other
+ end
+
+ def hash # :nodoc:
+ return @altitude.hash
+ end
+
+ end
+
+ end
+
+ ##
# Default resolver to use for Resolv class methods.
DefaultResolver = self.new
##
+ # Replaces the resolvers in the default resolver with +new_resolvers+. This
+ # allows resolvers to be changed for resolv-replace.
+
+ def DefaultResolver.replace_resolvers new_resolvers
+ @resolvers = new_resolvers
+ end
+
+ ##
# Address Regexp to use for matching IP addresses.
AddressRegex = /(?:#{IPv4::Regex})|(?:#{IPv6::Regex})/
end
-