diff options
Diffstat (limited to 'lib/resolv.rb')
| -rw-r--r-- | lib/resolv.rb | 289 |
1 files changed, 180 insertions, 109 deletions
diff --git a/lib/resolv.rb b/lib/resolv.rb index e36dbce259..6b58f92813 100644 --- a/lib/resolv.rb +++ b/lib/resolv.rb @@ -3,11 +3,8 @@ require 'socket' require 'timeout' require 'io/wait' - -begin - require 'securerandom' -rescue LoadError -end +require 'securerandom' +require 'rbconfig' # Resolv is a thread-aware DNS resolver library written in Ruby. Resolv can # handle multiple DNS requests concurrently without blocking the entire Ruby @@ -37,7 +34,8 @@ end class Resolv - VERSION = "0.4.0" + # The version string + VERSION = "0.7.1" ## # Looks up the first IP address for +name+. @@ -83,9 +81,22 @@ class Resolv ## # Creates a new Resolv using +resolvers+. + # + # If +resolvers+ is not given, a hash, or +nil+, uses a Hosts resolver and + # and a DNS resolver. If +resolvers+ is a hash, uses the hash as + # configuration for the DNS resolver. + + def initialize(resolvers=(arg_not_set = true; nil), use_ipv6: (keyword_not_set = true; nil)) + if !keyword_not_set && !arg_not_set + warn "Support for separate use_ipv6 keyword is deprecated, as it is ignored if an argument is provided. Do not provide a positional argument if using the use_ipv6 keyword argument.", uplevel: 1 + end - def initialize(resolvers=nil, use_ipv6: nil) - @resolvers = resolvers || [Hosts.new, DNS.new(DNS::Config.default_config_hash.merge(use_ipv6: use_ipv6))] + @resolvers = case resolvers + when Hash, nil + [Hosts.new, DNS.new(DNS::Config.default_config_hash.merge(resolvers || {}))] + else + resolvers + end end ## @@ -168,14 +179,15 @@ class Resolv # Resolv::Hosts is a hostname resolver that uses the system hosts file. class Hosts - if /mswin|mingw|cygwin/ =~ RUBY_PLATFORM and + if /mswin|cygwin|mingw|bccwin/ =~ RUBY_PLATFORM || ::RbConfig::CONFIG['host_os'] =~ /mswin/ begin - require 'win32/resolv' - DefaultFileName = Win32::Resolv.get_hosts_path || IO::NULL + require 'win32/resolv' unless defined?(Win32::Resolv) + hosts = Win32::Resolv.get_hosts_path || IO::NULL rescue LoadError end end - DefaultFileName ||= '/etc/hosts' + # The default file name for host names + DefaultFileName = hosts || '/etc/hosts' ## # Creates a new Resolv::Hosts, using +filename+ for its data source. @@ -396,13 +408,15 @@ class Resolv # be a Resolv::IPv4 or Resolv::IPv6 def each_address(name) - each_resource(name, Resource::IN::A) {|resource| yield resource.address} if use_ipv6? each_resource(name, Resource::IN::AAAA) {|resource| yield resource.address} end + each_resource(name, Resource::IN::A) {|resource| yield resource.address} end def use_ipv6? # :nodoc: + @config.lazy_initialize unless @config.instance_variable_get(:@initialized) + use_ipv6 = @config.use_ipv6? unless use_ipv6.nil? return use_ipv6 @@ -473,13 +487,18 @@ class Resolv # * Resolv::DNS::Resource::IN::A # * Resolv::DNS::Resource::IN::AAAA # * Resolv::DNS::Resource::IN::ANY + # * Resolv::DNS::Resource::IN::CAA # * Resolv::DNS::Resource::IN::CNAME # * Resolv::DNS::Resource::IN::HINFO + # * Resolv::DNS::Resource::IN::HTTPS + # * Resolv::DNS::Resource::IN::LOC # * Resolv::DNS::Resource::IN::MINFO # * Resolv::DNS::Resource::IN::MX # * Resolv::DNS::Resource::IN::NS # * Resolv::DNS::Resource::IN::PTR # * Resolv::DNS::Resource::IN::SOA + # * Resolv::DNS::Resource::IN::SRV + # * Resolv::DNS::Resource::IN::SVCB # * Resolv::DNS::Resource::IN::TXT # * Resolv::DNS::Resource::IN::WKS # @@ -511,37 +530,44 @@ class Resolv } end + # :stopdoc: + def fetch_resource(name, typeclass) lazy_initialize - begin - requester = make_udp_requester + truncated = {} + requesters = {} + udp_requester = begin + make_udp_requester rescue Errno::EACCES # fall back to TCP end senders = {} + begin - @config.resolv(name) {|candidate, tout, nameserver, port| - requester ||= make_tcp_requester(nameserver, port) + @config.resolv(name) do |candidate, tout, nameserver, port| msg = Message.new msg.rd = 1 msg.add_question(candidate, typeclass) - unless sender = senders[[candidate, nameserver, port]] + + requester = requesters.fetch([nameserver, port]) do + if !truncated[candidate] && udp_requester + udp_requester + else + requesters[[nameserver, port]] = make_tcp_requester(nameserver, port) + end + end + + unless sender = senders[[candidate, requester, nameserver, port]] sender = requester.sender(msg, candidate, nameserver, port) next if !sender - senders[[candidate, nameserver, port]] = sender + senders[[candidate, requester, 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 - requester.close # Retry via TCP: - requester = make_tcp_requester(nameserver, port) - senders = {} - # This will use TCP for all remaining candidates (assuming the - # 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. + truncated[candidate] = true redo else yield(reply, reply_name) @@ -552,9 +578,10 @@ class Resolv else raise Config::OtherResolvError.new(reply_name.to_s) end - } + end ensure - requester&.close + udp_requester&.close + requesters.each_value { |requester| requester&.close } end end @@ -569,6 +596,11 @@ class Resolv def make_tcp_requester(host, port) # :nodoc: return Requester::TCP.new(host, port) + rescue Errno::ECONNREFUSED + # Treat a refused TCP connection attempt to a nameserver like a timeout, + # as Resolv::DNS::Config#resolv considers ResolvTimeout exceptions as a + # hint to try the next nameserver: + raise ResolvTimeout end def extract_resources(msg, name, typeclass) # :nodoc: @@ -602,16 +634,10 @@ class Resolv } end - if defined? SecureRandom - def self.random(arg) # :nodoc: - begin - SecureRandom.random_number(arg) - rescue NotImplementedError - rand(arg) - end - end - else - def self.random(arg) # :nodoc: + def self.random(arg) # :nodoc: + begin + SecureRandom.random_number(arg) + rescue NotImplementedError rand(arg) end end @@ -643,8 +669,20 @@ class Resolv } end - def self.bind_random_port(udpsock, bind_host="0.0.0.0") # :nodoc: - begin + case RUBY_PLATFORM + when *[ + # https://www.rfc-editor.org/rfc/rfc6056.txt + # Appendix A. Survey of the Algorithms in Use by Some Popular Implementations + /freebsd/, /linux/, /netbsd/, /openbsd/, /solaris/, + /darwin/, # the same as FreeBSD + ] then + def self.bind_random_port(udpsock, bind_host="0.0.0.0") # :nodoc: + udpsock.bind(bind_host, 0) + end + else + # Sequential port assignment + def self.bind_random_port(udpsock, bind_host="0.0.0.0") # :nodoc: + # Ephemeral port number range recommended by RFC 6056 port = random(1024..65535) udpsock.bind(bind_host, port) rescue Errno::EADDRINUSE, # POSIX @@ -688,7 +726,8 @@ class Resolv begin reply, from = recv_reply(select_result[0]) rescue Errno::ECONNREFUSED, # GNU/Linux, FreeBSD - Errno::ECONNRESET # Windows + Errno::ECONNRESET, # Windows + EOFError # No name server running on the server? # Don't wait anymore. raise ResolvTimeout @@ -897,8 +936,11 @@ class Resolv end def recv_reply(readable_socks) - len = readable_socks[0].read(2).unpack('n')[0] + len_data = readable_socks[0].read(2) + raise EOFError if len_data.nil? || len_data.bytesize != 2 + len = len_data.unpack('n')[0] reply = @socks[0].read(len) + raise EOFError if reply.nil? || reply.bytesize != len return reply, nil end @@ -967,13 +1009,13 @@ class Resolv next unless keyword case keyword when 'nameserver' - nameserver.concat(args) + nameserver.concat(args.each(&:freeze)) when 'domain' next if args.empty? - search = [args[0]] + search = [args[0].freeze] when 'search' next if args.empty? - search = args + search = args.each(&:freeze) when 'options' args.each {|arg| case arg @@ -984,22 +1026,21 @@ class Resolv end } } - return { :nameserver => nameserver, :search => search, :ndots => ndots } + return { :nameserver => nameserver.freeze, :search => search.freeze, :ndots => ndots.freeze }.freeze end def Config.default_config_hash(filename="/etc/resolv.conf") if File.exist? filename - config_hash = Config.parse_resolv_conf(filename) + Config.parse_resolv_conf(filename) + elsif defined?(Win32::Resolv) + search, nameserver = Win32::Resolv.get_resolv_info + config_hash = {} + config_hash[:nameserver] = nameserver if nameserver + config_hash[:search] = [search].flatten if search + config_hash else - if /mswin|cygwin|mingw|bccwin/ =~ RUBY_PLATFORM - require 'win32/resolv' - search, nameserver = Win32::Resolv.get_resolv_info - config_hash = {} - config_hash[:nameserver] = nameserver if nameserver - config_hash[:search] = [search].flatten if search - end + {} end - config_hash || {} end def lazy_initialize @@ -1648,6 +1689,7 @@ class Resolv prev_index = @index save_index = nil d = [] + size = -1 while true raise DecodeError.new("limit exceeded") if @limit <= @index case @data.getbyte(@index) @@ -1668,7 +1710,10 @@ class Resolv end @index = idx else - d << self.get_label + l = self.get_label + d << l + size += 1 + l.string.bytesize + raise DecodeError.new("name label data exceed 255 octets") if size > 255 end end end @@ -1800,7 +1845,6 @@ class Resolv end end - ## # Base class for SvcParam. [RFC9460] @@ -2095,7 +2139,14 @@ class Resolv attr_reader :ttl - ClassHash = {} # :nodoc: + ClassHash = Module.new do + module_function + + def []=(type_class_value, klass) + type_value, class_value = type_class_value + Resource.const_set(:"Type#{type_value}_Class#{class_value}", klass) + end + end def encode_rdata(msg) # :nodoc: raise NotImplementedError.new @@ -2133,7 +2184,9 @@ class Resolv end def self.get_class(type_value, class_value) # :nodoc: - return ClassHash[[type_value, class_value]] || + cache = :"Type#{type_value}_Class#{class_value}" + + return (const_defined?(cache) && const_get(cache)) || Generic.create(type_value, class_value) end @@ -2499,7 +2552,6 @@ class Resolv attr_reader :altitude - def encode_rdata(msg) # :nodoc: msg.put_bytes(@version) msg.put_bytes(@ssize.scalar) @@ -2563,7 +2615,7 @@ class Resolv end ## - # Flags for this proprty: + # Flags for this property: # - Bit 0 : 0 = not critical, 1 = critical attr_reader :flags @@ -2884,15 +2936,21 @@ class Resolv class IPv4 - ## - # Regular expression IPv4 addresses must match. - Regex256 = /0 |1(?:[0-9][0-9]?)? |2(?:[0-4][0-9]?|5[0-5]?|[6-9])? - |[3-9][0-9]?/x + |[3-9][0-9]?/x # :nodoc: + + ## + # Regular expression IPv4 addresses must match. Regex = /\A(#{Regex256})\.(#{Regex256})\.(#{Regex256})\.(#{Regex256})\z/ + ## + # Creates a new IPv4 address from +arg+ which may be: + # + # IPv4:: returns +arg+. + # String:: +arg+ must match the IPv4::Regex constant + def self.create(arg) case arg when IPv4 @@ -3201,14 +3259,16 @@ class Resolv end - module LOC + module LOC # :nodoc: ## # A Resolv::LOC::Size class Size - Regex = /^(\d+\.*\d*)[m]$/ + # Regular expression LOC size must match. + + Regex = /\A0*(\d{1,8}(?:\.\d+)?)m\z/ ## # Creates a new LOC::Size from +arg+ which may be: @@ -3221,18 +3281,20 @@ class Resolv 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 + unless Regex =~ arg raise ArgumentError.new("not a properly formed Size string: " + arg) end - return Size.new(scalar) + unless (0.0...1e8) === (scalar = $1.to_f) + raise ArgumentError.new("out of range as Size: #{arg}") + end + str = (scalar * 100).to_i.to_s + return new([(str[0].to_i << 4) + (str.bytesize-1)].pack("C")) else raise ArgumentError.new("cannot interpret as Size: #{arg.inspect}") end end + # Internal use; use self.create. def initialize(scalar) @scalar = scalar end @@ -3243,8 +3305,8 @@ class Resolv 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" + s, = @scalar.unpack("C") + return "#{(s >> 4) * (10.0 ** ((s & 0xf) - 2))}m" end def inspect # :nodoc: @@ -3270,7 +3332,12 @@ class Resolv class Coord - Regex = /^(\d+)\s(\d+)\s(\d+\.\d+)\s([NESW])$/ + # Regular expression LOC Coord must match. + + Regex = /\A0*(\d{1,3})\s([0-5]?\d)\s([0-5]?\d(?:\.\d+)?)\s([NESW])\z/ + + # Bias for the equator/prime meridian, in thousandths of a second of arc. + Bias = 1 << 31 ## # Creates a new LOC::Coord from +arg+ which may be: @@ -3283,27 +3350,30 @@ class Resolv when Coord return arg when String - coordinates = '' - if Regex =~ arg && $1.to_f < 180 - m = $~ - hemi = (m[4][/[NE]/]) || (m[4][/[SW]/]) ? 1 : -1 - coordinates = [ ((m[1].to_i*(36e5)) + (m[2].to_i*(6e4)) + - (m[3].to_f*(1e3))) * hemi+(2**31) ].pack("N") - orientation = m[4][/[NS]/] ? 'lat' : 'lon' - else + unless m = Regex.match(arg) raise ArgumentError.new("not a properly formed Coord string: " + arg) end - return Coord.new(coordinates,orientation) + + arc = (m[1].to_i * 3_600_000) + (m[2].to_i * 60_000) + (m[3].to_f * 1_000).to_i + dir = m[4] + lat = dir[/[NS]/] + unless arc <= (lat ? 324_000_000 : 648_000_000) # (lat ? 90 : 180) * 3_600_000 + raise ArgumentError.new("out of range as Coord: #{arg}") + end + + hemi = dir[/[NE]/] ? 1 : -1 + return new([arc * hemi + Bias].pack("N"), lat ? "lat" : "lon") else raise ArgumentError.new("cannot interpret as Coord: #{arg.inspect}") end end + # Internal use; use self.create. def initialize(coordinates,orientation) - unless coordinates.kind_of?(String) + unless coordinates.kind_of?(String) and coordinates.bytesize == 4 raise ArgumentError.new("Coord must be a 32bit unsigned integer in hex format: #{coordinates.inspect}") end - unless orientation.kind_of?(String) && orientation[/^lon$|^lat$/] + unless orientation == "lon" || orientation == "lat" raise ArgumentError.new('Coord expects orientation to be a String argument of "lat" or "lon"') end @coordinates = coordinates @@ -3320,22 +3390,17 @@ class Resolv 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" + c, = @coordinates.unpack("N") + val = (c -= Bias).abs + val, fracsecs = val.divmod(1000) + val, secs = val.divmod(60) + degs, mins = val.divmod(60) + hemi = if c.negative? + @orientation == "lon" ? "W" : "S" else - hemi = @orientation[/^lon$/] ? "W" : "S" + @orientation == "lat" ? "N" : "E" end - return degs << " " << mins << " " << secs << "." << fracsecs << " " << hemi + format("%d %02d %02d.%03d %s", degs, mins, secs, fracsecs, hemi) end def inspect # :nodoc: @@ -3361,7 +3426,12 @@ class Resolv class Alt - Regex = /^([+-]*\d+\.*\d*)[m]$/ + # Regular expression LOC Alt must match. + + Regex = /\A([+-]?0*\d{1,8}(?:\.\d+)?)m\z/ + + # Bias to a base of 100,000m below the WGS 84 reference spheroid. + Bias = 100_000_00 ## # Creates a new LOC::Alt from +arg+ which may be: @@ -3374,18 +3444,20 @@ class Resolv when Alt return arg when String - altitude = '' - if Regex =~ arg - altitude = [($1.to_f*(1e2))+(1e7)].pack("N") - else + unless Regex =~ arg raise ArgumentError.new("not a properly formed Alt string: " + arg) end - return Alt.new(altitude) + altitude = ($1.to_f * 100).to_i + Bias + unless (0...0x1_0000_0000) === altitude + raise ArgumentError.new("out of raise as Alt: #{arg}") + end + return new([altitude].pack("N")) else raise ArgumentError.new("cannot interpret as Alt: #{arg.inspect}") end end + # Internal use; use self.create. def initialize(altitude) @altitude = altitude end @@ -3396,8 +3468,8 @@ class Resolv attr_reader :altitude def to_s # :nodoc: - a = @altitude.unpack("N").join.to_i - return ((a.to_f/1e2)-1e5).to_s + "m" + a, = @altitude.unpack("N") + return "#{(a - Bias).fdiv(100)}m" end def inspect # :nodoc: @@ -3439,4 +3511,3 @@ class Resolv AddressRegex = /(?:#{IPv4::Regex})|(?:#{IPv6::Regex})/ end - |
