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 -