diff options
Diffstat (limited to 'lib/resolv.rb')
| -rw-r--r-- | lib/resolv.rb | 1669 |
1 files changed, 1460 insertions, 209 deletions
diff --git a/lib/resolv.rb b/lib/resolv.rb index b201fcfabf..6b58f92813 100644 --- a/lib/resolv.rb +++ b/lib/resolv.rb @@ -1,18 +1,16 @@ +# frozen_string_literal: true + require 'socket' -require 'fcntl' require 'timeout' -require 'thread' - -begin - require 'securerandom' -rescue LoadError -end +require 'io/wait' +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 ruby +# handle multiple DNS requests concurrently without blocking the entire Ruby # interpreter. # -# See also resolv-replace.rb to replace the libc resolver with # Resolv. +# See also resolv-replace.rb to replace the libc resolver with Resolv. # # Resolv can look up various DNS resources using the DNS module directly. # @@ -23,7 +21,7 @@ end # # Resolv::DNS.open do |dns| # ress = dns.getresources "www.ruby-lang.org", Resolv::DNS::Resource::IN::A -# p ress.map { |r| r.address } +# p ress.map(&:address) # ress = dns.getresources "ruby-lang.org", Resolv::DNS::Resource::IN::MX # p ress.map { |r| [r.exchange.to_s, r.preference] } # end @@ -36,6 +34,9 @@ end class Resolv + # The version string + VERSION = "0.7.1" + ## # Looks up the first IP address for +name+. @@ -80,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=[Hosts.new, DNS.new]) - @resolvers = resolvers + 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 + + @resolvers = case resolvers + when Hash, nil + [Hosts.new, DNS.new(DNS::Config.default_config_hash.merge(resolvers || {}))] + else + resolvers + end end ## @@ -159,25 +173,28 @@ class Resolv ## # Indicates a timeout resolving a name or address. - class ResolvTimeout < TimeoutError; end + class ResolvTimeout < Timeout::Error; end ## - # DNS::Hosts is a hostname resolver that uses the system hosts file. + # Resolv::Hosts is a hostname resolver that uses the system hosts file. class Hosts - if /mswin|mingw|bccwin/ =~ RUBY_PLATFORM - require 'win32/resolv' - DefaultFileName = Win32::Resolv.get_hosts_path - else - DefaultFileName = '/etc/hosts' + if /mswin|cygwin|mingw|bccwin/ =~ RUBY_PLATFORM || ::RbConfig::CONFIG['host_os'] =~ /mswin/ + begin + require 'win32/resolv' unless defined?(Win32::Resolv) + hosts = Win32::Resolv.get_hosts_path || IO::NULL + rescue LoadError + end end + # The default file name for host names + DefaultFileName = hosts || '/etc/hosts' ## - # Creates a new DNS::Hosts, using +filename+ for its data source. + # Creates a new Resolv::Hosts, using +filename+ for its data source. def initialize(filename = DefaultFileName) @filename = filename - @mutex = Mutex.new + @mutex = Thread::Mutex.new @initialized = nil end @@ -186,23 +203,13 @@ class Resolv unless @initialized @name2addr = {} @addr2name = {} - open(@filename) {|f| + File.open(@filename, 'rb') {|f| f.each {|line| line.sub!(/#.*/, '') - addr, hostname, *aliases = line.split(/\s+/) + addr, *hostnames = line.split(/\s+/) next unless addr - addr.untaint - hostname.untaint - @addr2name[addr] = [] unless @addr2name.include? addr - @addr2name[addr] << hostname - @addr2name[addr] += aliases - @name2addr[hostname] = [] unless @name2addr.include? hostname - @name2addr[hostname] << addr - aliases.each {|n| - n.untaint - @name2addr[n] = [] unless @name2addr.include? n - @name2addr[n] << addr - } + (@addr2name[addr] ||= []).concat(hostnames) + hostnames.each {|hostname| (@name2addr[hostname] ||= []) << addr} } } @name2addr.each {|name, arr| arr.reverse!} @@ -234,9 +241,7 @@ class Resolv def each_address(name, &proc) lazy_initialize - if @name2addr.include?(name) - @name2addr[name].each(&proc) - end + @name2addr[name]&.each(&proc) end ## @@ -261,9 +266,7 @@ class Resolv def each_name(address, &proc) lazy_initialize - if @addr2name.include?(address) - @addr2name[address].each(&proc) - end + @addr2name[address]&.each(&proc) end end @@ -313,6 +316,18 @@ class Resolv # nil:: Uses /etc/resolv.conf. # String:: Path to a file using /etc/resolv.conf's format. # Hash:: Must contain :nameserver, :search and :ndots keys. + # :nameserver_port can be used to specify port number of nameserver address. + # :raise_timeout_errors can be used to raise timeout errors + # as exceptions instead of treating the same as an NXDOMAIN response. + # + # The value of :nameserver should be an address string or + # an array of address strings. + # - :nameserver => '8.8.8.8' + # - :nameserver => ['8.8.8.8', '8.8.4.4'] + # + # The value of :nameserver_port should be an array of + # pair of nameserver address and port number. + # - :nameserver_port => [['8.8.8.8', 53], ['8.8.4.4', 53]] # # Example: # @@ -321,11 +336,26 @@ class Resolv # :ndots => 1) def initialize(config_info=nil) - @mutex = Mutex.new + @mutex = Thread::Mutex.new @config = Config.new(config_info) @initialized = nil end + # Sets the resolver timeouts. This may be a single positive number + # or an array of positive numbers representing timeouts in seconds. + # If an array is specified, a DNS request will retry and wait for + # each successive interval in the array until a successful response + # is received. Specifying +nil+ reverts to the default timeouts: + # [ 5, second = 5 * 2 / nameserver_count, 2 * second, 4 * second ] + # + # Example: + # + # dns.timeouts = 3 + # + def timeouts=(values) + @config.timeouts = values + end + def lazy_initialize # :nodoc: @mutex.synchronize { unless @initialized @@ -378,10 +408,29 @@ class Resolv # be a Resolv::IPv4 or Resolv::IPv6 def each_address(name) + if use_ipv6? + each_resource(name, Resource::IN::AAAA) {|resource| yield resource.address} + end each_resource(name, Resource::IN::A) {|resource| yield resource.address} - each_resource(name, Resource::IN::AAAA) {|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 + end + + begin + list = Socket.ip_address_list + rescue NotImplementedError + return true + end + list.any? {|a| a.ipv6? && !a.ipv6_loopback? && !a.ipv6_linklocal? } + end + private :use_ipv6? + ## # Gets the hostname for +address+ from the DNS resolver. # @@ -416,6 +465,8 @@ class Resolv case address when Name ptr = address + when IPv4, IPv6 + ptr = address.to_name when IPv4::Regex ptr = IPv4.create(address).to_name when IPv6::Regex @@ -436,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 # @@ -469,52 +525,94 @@ class Resolv # #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 + + # :stopdoc: + + def fetch_resource(name, typeclass) lazy_initialize - requester = make_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| + @config.resolv(name) do |candidate, tout, nameserver, port| msg = Message.new msg.rd = 1 msg.add_question(candidate, typeclass) - unless sender = senders[[candidate, nameserver]] - sender = senders[[candidate, nameserver]] = - requester.sender(msg, candidate, nameserver) + + 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, requester, nameserver, port]] = sender end reply, reply_name = requester.request(sender, tout) case reply.rcode when RCode::NoError - extract_resources(reply, reply_name, typeclass, &proc) + if reply.tc == 1 and not Requester::TCP === requester + # Retry via TCP: + truncated[candidate] = true + redo + else + yield(reply, reply_name) + end return when RCode::NXDomain raise Config::NXDomain.new(reply_name.to_s) 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 - def make_requester # :nodoc: - if nameserver = @config.single? - Requester::ConnectedUDP.new(nameserver) + def make_udp_requester # :nodoc: + nameserver_port = @config.nameserver_port + if nameserver_port.length == 1 + Requester::ConnectedUDP.new(*nameserver_port[0]) else - Requester::UnconnectedUDP.new + Requester::UnconnectedUDP.new(*nameserver_port) end end + 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: if typeclass < Resource::ANY n0 = Name.create(name) - msg.each_answer {|n, ttl, data| + msg.each_resource {|n, ttl, data| yield data if n0 == n } end yielded = false n0 = Name.create(name) - msg.each_answer {|n, ttl, data| + msg.each_resource {|n, ttl, data| if n0 == n case data when typeclass @@ -526,7 +624,7 @@ class Resolv end } return if yielded - msg.each_answer {|n, ttl, data| + msg.each_resource {|n, ttl, data| if n0 == n case data when typeclass @@ -536,39 +634,23 @@ 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 - - def self.rangerand(range) # :nodoc: - base = range.begin - len = range.end - range.begin - if !range.exclude_end? - len += 1 - end - base + random(len) - end - - RequestID = {} - RequestIDMutex = Mutex.new + RequestID = {} # :nodoc: + RequestIDMutex = Thread::Mutex.new # :nodoc: def self.allocate_request_id(host, port) # :nodoc: id = nil RequestIDMutex.synchronize { h = (RequestID[[host, port]] ||= {}) begin - id = rangerand(0x0000..0xffff) + id = random(0x0000..0xffff) end while h[id] h[id] = true } @@ -587,11 +669,25 @@ class Resolv } end - def self.bind_random_port(udpsock) # :nodoc: - begin - port = rangerand(1024..65535) - udpsock.bind("", port) - rescue Errno::EADDRINUSE + 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 + 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 @@ -599,36 +695,65 @@ class Resolv class Requester # :nodoc: def initialize @senders = {} - @sock = nil + @socks = nil end def request(sender, tout) - timelimit = Time.now + tout - sender.send - while (now = Time.now) < timelimit - timeout = timelimit - now - if !IO.select([@sock], nil, nil, timeout) + start = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timelimit = start + tout + begin + sender.send + rescue Errno::EHOSTUNREACH, # multi-homed IPv6 may generate this + Errno::ENETUNREACH + raise ResolvTimeout + end + while true + before_select = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout = timelimit - before_select + if timeout <= 0 + raise ResolvTimeout + end + if @socks.size == 1 + select_result = @socks[0].wait_readable(timeout) ? [ @socks ] : nil + else + select_result = IO.select(@socks, nil, nil, timeout) + end + if !select_result + after_select = Process.clock_gettime(Process::CLOCK_MONOTONIC) + next if after_select < timelimit + raise ResolvTimeout + end + begin + reply, from = recv_reply(select_result[0]) + rescue Errno::ECONNREFUSED, # GNU/Linux, FreeBSD + Errno::ECONNRESET, # Windows + EOFError + # No name server running on the server? + # Don't wait anymore. raise ResolvTimeout end - reply, from = recv_reply begin msg = Message.decode(reply) rescue DecodeError next # broken DNS message ignored end - if s = @senders[[from,msg.id]] + if sender == sender_for(from, msg) break else # unexpected DNS message ignored end end - return msg, s.data + return msg, sender.data + end + + def sender_for(addr, msg) + @senders[[addr,msg.id]] end def close - sock = @sock - @sock = nil - sock.close if sock + socks = @socks + @socks = nil + socks&.each(&:close) end class Sender # :nodoc: @@ -640,31 +765,70 @@ class Resolv end class UnconnectedUDP < Requester # :nodoc: - def initialize + def initialize(*nameserver_port) super() - @sock = UDPSocket.new - @sock.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC) if defined? Fcntl::F_SETFD - DNS.bind_random_port(@sock) + @nameserver_port = nameserver_port + @initialized = false + @mutex = Thread::Mutex.new + end + + def lazy_initialize + @mutex.synchronize { + next if @initialized + @initialized = true + @socks_hash = {} + @socks = [] + @nameserver_port.each {|host, port| + if host.index(':') + bind_host = "::" + af = Socket::AF_INET6 + else + bind_host = "0.0.0.0" + af = Socket::AF_INET + end + next if @socks_hash[bind_host] + begin + sock = UDPSocket.new(af) + rescue Errno::EAFNOSUPPORT, Errno::EPROTONOSUPPORT + next # The kernel doesn't support the address family. + end + @socks << sock + @socks_hash[bind_host] = sock + sock.do_not_reverse_lookup = true + DNS.bind_random_port(sock, bind_host) + } + } + self end - def recv_reply - reply, from = @sock.recvfrom(UDPSize) + def recv_reply(readable_socks) + lazy_initialize + reply, from = readable_socks[0].recvfrom(UDPSize) return reply, [from[3],from[1]] end def sender(msg, data, host, port=Port) + host = Addrinfo.ip(host).ip_address + lazy_initialize + 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') return @senders[[service, id]] = - Sender.new(request, data, @sock, host, port) + Sender.new(request, data, sock, host, port) end def close - super - @senders.each_key {|service, id| - DNS.free_request_id(service[0], service[1], id) + @mutex.synchronize { + if @initialized + super + @senders.each_key {|service, id| + DNS.free_request_id(service[0], service[1], id) + } + @initialized = false + end } end @@ -677,6 +841,7 @@ class Resolv attr_reader :data def send + raise "@sock is nil." if @sock.nil? @sock.send(@msg, 0, @host, @port) end end @@ -687,55 +852,95 @@ class Resolv super() @host = host @port = port - @sock = UDPSocket.new(host.index(':') ? Socket::AF_INET6 : Socket::AF_INET) - DNS.bind_random_port(@sock) - @sock.connect(host, port) - @sock.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC) if defined? Fcntl::F_SETFD + @mutex = Thread::Mutex.new + @initialized = false + end + + def lazy_initialize + @mutex.synchronize { + next if @initialized + @initialized = true + is_ipv6 = @host.index(':') + sock = UDPSocket.new(is_ipv6 ? Socket::AF_INET6 : Socket::AF_INET) + @socks = [sock] + sock.do_not_reverse_lookup = true + DNS.bind_random_port(sock, is_ipv6 ? "::" : "0.0.0.0") + sock.connect(@host, @port) + } + self end - def recv_reply - reply = @sock.recv(UDPSize) + def recv_reply(readable_socks) + lazy_initialize + reply = readable_socks[0].recv(UDPSize) return reply, nil end def sender(msg, data, host=@host, port=@port) + lazy_initialize unless host == @host && port == @port raise RequestError.new("host/port don't match: #{host}:#{port}") end id = DNS.allocate_request_id(@host, @port) request = msg.encode request[0,2] = [id].pack('n') - return @senders[[nil,id]] = Sender.new(request, data, @sock) + return @senders[[nil,id]] = Sender.new(request, data, @socks[0]) end def close - super - @senders.each_key {|from, id| - DNS.free_request_id(@host, @port, id) - } + @mutex.synchronize do + if @initialized + super + @senders.each_key {|from, id| + DNS.free_request_id(@host, @port, id) + } + @initialized = false + end + end 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) + lazy_initialize + 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) + lazy_initialize + @senders[msg.id] + end + end + class TCP < Requester # :nodoc: def initialize(host, port=Port) super() @host = host @port = port - @sock = TCPSocket.new(@host, @port) - @sock.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC) if defined? Fcntl::F_SETFD + sock = TCPSocket.new(@host, @port) + @socks = [sock] @senders = {} end - def recv_reply - len = @sock.read(2).unpack('n')[0] - reply = @sock.read(len) + def recv_reply(readable_socks) + 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 @@ -746,7 +951,7 @@ class Resolv id = DNS.allocate_request_id(@host, @port) request = msg.encode request[0,2] = [request.length, id].pack('nn') - return @senders[[nil,id]] = Sender.new(request, data, @sock) + return @senders[[nil,id]] = Sender.new(request, data, @socks[0]) end class Sender < Requester::Sender # :nodoc: @@ -774,32 +979,43 @@ class Resolv class Config # :nodoc: def initialize(config_info=nil) - @mutex = Mutex.new + @mutex = Thread::Mutex.new @config_info = config_info @initialized = nil + @timeouts = nil + end + + 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 positive" + end + @timeouts = values + else + @timeouts = nil + end end def Config.parse_resolv_conf(filename) nameserver = [] search = nil ndots = 1 - open(filename) {|f| + File.open(filename, 'rb') {|f| f.each {|line| line.sub!(/[#;].*/, '') keyword, *args = line.split(/\s+/) - args.each { |arg| - arg.untaint - } next unless keyword case keyword when 'nameserver' - nameserver += 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 @@ -810,28 +1026,28 @@ 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 @mutex.synchronize { unless @initialized - @nameserver = [] + @nameserver_port = [] + @use_ipv6 = nil @search = nil @ndots = 1 case @config_info @@ -850,11 +1066,22 @@ class Resolv else raise ArgumentError.new("invalid resolv configuration: #{@config_info.inspect}") end - @nameserver = config_hash[:nameserver] if config_hash.include? :nameserver + if config_hash.include? :nameserver + @nameserver_port = config_hash[:nameserver].map {|ns| [ns, Port] } + end + if config_hash.include? :nameserver_port + @nameserver_port = config_hash[:nameserver_port].map {|ns, port| [ns, (port || Port)] } + end + if config_hash.include? :use_ipv6 + @use_ipv6 = config_hash[:use_ipv6] + end @search = config_hash[:search] if config_hash.include? :search @ndots = config_hash[:ndots] if config_hash.include? :ndots + @raise_timeout_errors = config_hash[:raise_timeout_errors] - @nameserver = ['0.0.0.0'] if @nameserver.empty? + if @nameserver_port.empty? + @nameserver_port << ['0.0.0.0', Port] + end if @search @search = @search.map {|arg| Label.split(arg) } else @@ -866,9 +1093,14 @@ class Resolv end end - if !@nameserver.kind_of?(Array) || - !@nameserver.all? {|ns| String === ns } - raise ArgumentError.new("invalid nameserver config: #{@nameserver.inspect}") + if !@nameserver_port.kind_of?(Array) || + @nameserver_port.any? {|ns_port| + !(Array === ns_port) || + ns_port.length != 2 + !(String === ns_port[0]) || + !(Integer === ns_port[1]) + } + raise ArgumentError.new("invalid nameserver config: #{@nameserver_port.inspect}") end if !@search.kind_of?(Array) || @@ -888,13 +1120,21 @@ class Resolv def single? lazy_initialize - if @nameserver.length == 1 - return @nameserver[0] + if @nameserver_port.length == 1 + return @nameserver_port[0] else return nil end end + def nameserver_port + @nameserver_port + end + + def use_ipv6? + @use_ipv6 + end + def generate_candidates(name) candidates = nil name = Name.create(name) @@ -907,6 +1147,10 @@ class Resolv candidates = [] end candidates.concat(@search.map {|domain| Name.new(name.to_a + domain)}) + fname = Name.create("#{name}.") + if !candidates.include?(fname) + candidates << fname + end end return candidates end @@ -915,7 +1159,7 @@ class Resolv def generate_timeouts ts = [InitialTimeout] - ts << ts[-1] * 2 / @nameserver.length + ts << ts[-1] * 2 / @nameserver_port.length ts << ts[-1] * 2 ts << ts[-1] * 2 return ts @@ -923,23 +1167,26 @@ class Resolv def resolv(name) candidates = generate_candidates(name) - timeouts = generate_timeouts + timeouts = @timeouts || generate_timeouts + timeout_error = false begin candidates.each {|candidate| begin timeouts.each {|tout| - @nameserver.each {|nameserver| + @nameserver_port.each {|nameserver, port| begin - yield candidate, tout, nameserver + yield candidate, tout, nameserver, port rescue ResolvTimeout end } } + timeout_error = true raise ResolvError.new("DNS resolv timeout: #{name}") rescue NXDomain end } rescue ResolvError + raise if @raise_timeout_errors && timeout_error end end @@ -1007,7 +1254,9 @@ class Resolv class Str # :nodoc: def initialize(string) @string = string - @downcase = string.downcase + # case insensivity of DNS labels doesn't apply non-ASCII characters. [RFC 4343] + # This assumes @string is given in ASCII compatible encoding. + @downcase = string.b.downcase end attr_reader :string, :downcase @@ -1016,11 +1265,11 @@ class Resolv end def inspect - return "#<#{self.class} #{self.to_s}>" + return "#<#{self.class} #{self}>" end def ==(other) - return @downcase == other.downcase + return self.class == other.class && @downcase == other.downcase end def eql?(other) @@ -1056,12 +1305,20 @@ class Resolv end def initialize(labels, absolute=true) # :nodoc: + labels = labels.map {|label| + case label + when String then Label::Str.new(label) + when Label::Str then label + else + raise ArgumentError, "unexpected label: #{label.inspect}" + end + } @labels = labels @absolute = absolute end def inspect # :nodoc: - "#<#{self.class}: #{self.to_s}#{@absolute ? '.' : ''}>" + "#<#{self.class}: #{self}#{@absolute ? '.' : ''}>" end ## @@ -1073,7 +1330,8 @@ class Resolv def ==(other) # :nodoc: return false unless Name === other - return @labels.join == other.to_a.join && @absolute == other.absolute? + return false unless @absolute == other.absolute? + return @labels == other.to_a end alias eql? == # :nodoc: @@ -1247,7 +1505,7 @@ class Resolv class MessageEncoder # :nodoc: def initialize - @data = '' + @data = ''.dup @names = {} yield self end @@ -1284,18 +1542,20 @@ class Resolv } end - def put_name(d) - put_labels(d.to_a) + def put_name(d, compress: true) + put_labels(d.to_a, compress: compress) end - def put_labels(d) + def put_labels(d, compress: true) d.each_index {|i| domain = d[i..-1] - if idx = @names[domain] + if compress && idx = @names[domain] self.put_pack("n", 0xc000 | idx) return else - @names[domain] = @data.length + if @data.length < 0x4000 + @names[domain] = @data.length + end self.put_label(d[i]) end } @@ -1313,13 +1573,15 @@ class Resolv id, flag, qdcount, ancount, nscount, arcount = msg.get_unpack('nnnnnn') o.id = id + o.tc = (flag >> 9) & 1 + o.rcode = flag & 15 + return o unless o.tc.zero? + o.qr = (flag >> 15) & 1 o.opcode = (flag >> 11) & 15 o.aa = (flag >> 10) & 1 - o.tc = (flag >> 9) & 1 o.rd = (flag >> 8) & 1 o.ra = (flag >> 7) & 1 - o.rcode = flag & 15 (1..qdcount).each { name, typeclass = msg.get_question o.add_question(name, typeclass) @@ -1344,10 +1606,14 @@ class Resolv def initialize(data) @data = data @index = 0 - @limit = data.length + @limit = data.bytesize yield self end + def inspect + "\#<#{self.class}: #{@data.byteslice(0, @index).inspect} #{@data.byteslice(@index..-1).inspect}>" + end + def get_length16 len, = self.get_unpack('n') save_limit = @limit @@ -1363,7 +1629,8 @@ class Resolv end def get_bytes(len = @limit - @index) - d = @data[@index, len] + raise DecodeError.new("limit exceeded") if @limit < @index + len + d = @data.byteslice(@index, len) @index += len return d end @@ -1390,9 +1657,10 @@ class Resolv end def get_string - len = @data[@index].ord + raise DecodeError.new("limit exceeded") if @limit <= @index + len = @data.getbyte(@index) raise DecodeError.new("limit exceeded") if @limit < @index + 1 + len - d = @data[@index + 1, len] + d = @data.byteslice(@index + 1, len) @index += 1 + len return d end @@ -1405,33 +1673,49 @@ class Resolv strings end + def get_list + [].tap do |values| + while @index < @limit + values << yield + end + end + end + 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 = [] + size = -1 while true - case @data[@index].ord + raise DecodeError.new("limit exceeded") if @limit <= @index + case @data.getbyte(@index) 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 + 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 - return d end def get_label @@ -1448,7 +1732,13 @@ class Resolv name = self.get_name type, klass, ttl = self.get_unpack('nnN') typeclass = Resource.get_class(type, klass) - res = self.get_length16 { typeclass.decode_rdata self } + res = self.get_length16 do + begin + typeclass.decode_rdata self + rescue => e + raise DecodeError, e.message, e.backtrace + end + end res.instance_variable_set :@ttl, ttl return name, ttl, res end @@ -1456,6 +1746,377 @@ class Resolv end ## + # SvcParams for service binding RRs. [RFC9460] + + class SvcParams + include Enumerable + + ## + # Create a list of SvcParams with the given initial content. + # + # +params+ has to be an enumerable of +SvcParam+s. + # If its content has +SvcParam+s with the duplicate key, + # the one appears last takes precedence. + + def initialize(params = []) + @params = {} + + params.each do |param| + add param + end + end + + ## + # Get SvcParam for the given +key+ in this list. + + def [](key) + @params[canonical_key(key)] + end + + ## + # Get the number of SvcParams in this list. + + def count + @params.count + end + + ## + # Get whether this list is empty. + + def empty? + @params.empty? + end + + ## + # Add the SvcParam +param+ to this list, overwriting the existing one with the same key. + + def add(param) + @params[param.class.key_number] = param + end + + ## + # Remove the +SvcParam+ with the given +key+ and return it. + + def delete(key) + @params.delete(canonical_key(key)) + end + + ## + # Enumerate the +SvcParam+s in this list. + + def each(&block) + return enum_for(:each) unless block + @params.each_value(&block) + end + + def encode(msg) # :nodoc: + @params.keys.sort.each do |key| + msg.put_pack('n', key) + msg.put_length16 do + @params.fetch(key).encode(msg) + end + end + end + + def self.decode(msg) # :nodoc: + params = msg.get_list do + key, = msg.get_unpack('n') + msg.get_length16 do + SvcParam::ClassHash[key].decode(msg) + end + end + + return self.new(params) + end + + private + + def canonical_key(key) # :nodoc: + case key + when Integer + key + when /\Akey(\d+)\z/ + Integer($1) + when Symbol + SvcParam::ClassHash[key].key_number + else + raise TypeError, 'key must be either String or Symbol' + end + end + end + + ## + # Base class for SvcParam. [RFC9460] + + class SvcParam + + ## + # Get the presentation name of the SvcParamKey. + + def self.key_name + const_get(:KeyName) + end + + ## + # Get the registered number of the SvcParamKey. + + def self.key_number + const_get(:KeyNumber) + end + + ClassHash = Hash.new do |h, key| # :nodoc: + case key + when Integer + Generic.create(key) + when /\Akey(?<key>\d+)\z/ + Generic.create(key.to_int) + when Symbol + raise KeyError, "unknown key #{key}" + else + raise TypeError, 'key must be either String or Symbol' + end + end + + ## + # Generic SvcParam abstract class. + + class Generic < SvcParam + + ## + # SvcParamValue in wire-format byte string. + + attr_reader :value + + ## + # Create generic SvcParam + + def initialize(value) + @value = value + end + + def encode(msg) # :nodoc: + msg.put_bytes(@value) + end + + def self.decode(msg) # :nodoc: + return self.new(msg.get_bytes) + end + + def self.create(key_number) + c = Class.new(Generic) + key_name = :"key#{key_number}" + c.const_set(:KeyName, key_name) + c.const_set(:KeyNumber, key_number) + self.const_set(:"Key#{key_number}", c) + ClassHash[key_name] = ClassHash[key_number] = c + return c + end + end + + ## + # "mandatory" SvcParam -- Mandatory keys in service binding RR + + class Mandatory < SvcParam + KeyName = :mandatory + KeyNumber = 0 + ClassHash[KeyName] = ClassHash[KeyNumber] = self # :nodoc: + + ## + # Mandatory keys. + + attr_reader :keys + + ## + # Initialize "mandatory" ScvParam. + + def initialize(keys) + @keys = keys.map(&:to_int) + end + + def encode(msg) # :nodoc: + @keys.sort.each do |key| + msg.put_pack('n', key) + end + end + + def self.decode(msg) # :nodoc: + keys = msg.get_list { msg.get_unpack('n')[0] } + return self.new(keys) + end + end + + ## + # "alpn" SvcParam -- Additional supported protocols + + class ALPN < SvcParam + KeyName = :alpn + KeyNumber = 1 + ClassHash[KeyName] = ClassHash[KeyNumber] = self # :nodoc: + + ## + # Supported protocol IDs. + + attr_reader :protocol_ids + + ## + # Initialize "alpn" ScvParam. + + def initialize(protocol_ids) + @protocol_ids = protocol_ids.map(&:to_str) + end + + def encode(msg) # :nodoc: + msg.put_string_list(@protocol_ids) + end + + def self.decode(msg) # :nodoc: + return self.new(msg.get_string_list) + end + end + + ## + # "no-default-alpn" SvcParam -- No support for default protocol + + class NoDefaultALPN < SvcParam + KeyName = :'no-default-alpn' + KeyNumber = 2 + ClassHash[KeyName] = ClassHash[KeyNumber] = self # :nodoc: + + def encode(msg) # :nodoc: + # no payload + end + + def self.decode(msg) # :nodoc: + return self.new + end + end + + ## + # "port" SvcParam -- Port for alternative endpoint + + class Port < SvcParam + KeyName = :port + KeyNumber = 3 + ClassHash[KeyName] = ClassHash[KeyNumber] = self # :nodoc: + + ## + # Port number. + + attr_reader :port + + ## + # Initialize "port" ScvParam. + + def initialize(port) + @port = port.to_int + end + + def encode(msg) # :nodoc: + msg.put_pack('n', @port) + end + + def self.decode(msg) # :nodoc: + port, = msg.get_unpack('n') + return self.new(port) + end + end + + ## + # "ipv4hint" SvcParam -- IPv4 address hints + + class IPv4Hint < SvcParam + KeyName = :ipv4hint + KeyNumber = 4 + ClassHash[KeyName] = ClassHash[KeyNumber] = self # :nodoc: + + ## + # Set of IPv4 addresses. + + attr_reader :addresses + + ## + # Initialize "ipv4hint" ScvParam. + + def initialize(addresses) + @addresses = addresses.map {|address| IPv4.create(address) } + end + + def encode(msg) # :nodoc: + @addresses.each do |address| + msg.put_bytes(address.address) + end + end + + def self.decode(msg) # :nodoc: + addresses = msg.get_list { IPv4.new(msg.get_bytes(4)) } + return self.new(addresses) + end + end + + ## + # "ipv6hint" SvcParam -- IPv6 address hints + + class IPv6Hint < SvcParam + KeyName = :ipv6hint + KeyNumber = 6 + ClassHash[KeyName] = ClassHash[KeyNumber] = self # :nodoc: + + ## + # Set of IPv6 addresses. + + attr_reader :addresses + + ## + # Initialize "ipv6hint" ScvParam. + + def initialize(addresses) + @addresses = addresses.map {|address| IPv6.create(address) } + end + + def encode(msg) # :nodoc: + @addresses.each do |address| + msg.put_bytes(address.address) + end + end + + def self.decode(msg) # :nodoc: + addresses = msg.get_list { IPv6.new(msg.get_bytes(16)) } + return self.new(addresses) + end + end + + ## + # "dohpath" SvcParam -- DNS over HTTPS path template [RFC9461] + + class DoHPath < SvcParam + KeyName = :dohpath + KeyNumber = 7 + ClassHash[KeyName] = ClassHash[KeyNumber] = self # :nodoc: + + ## + # URI template for DoH queries. + + attr_reader :template + + ## + # Initialize "dohpath" ScvParam. + + def initialize(template) + @template = template.encode('utf-8') + end + + def encode(msg) # :nodoc: + msg.put_bytes(@template) + end + + def self.decode(msg) # :nodoc: + template = msg.get_bytes.force_encoding('utf-8') + return self.new(template) + end + end + end + + ## # A DNS query abstract class. class Query @@ -1478,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 @@ -1492,10 +2160,10 @@ class Resolv return false unless self.class == other.class s_ivars = self.instance_variables s_ivars.sort! - s_ivars.delete "@ttl" + s_ivars.delete :@ttl o_ivars = other.instance_variables o_ivars.sort! - o_ivars.delete "@ttl" + o_ivars.delete :@ttl return s_ivars == o_ivars && s_ivars.collect {|name| self.instance_variable_get name} == o_ivars.collect {|name| other.instance_variable_get name} @@ -1508,7 +2176,7 @@ class Resolv def hash # :nodoc: h = 0 vars = self.instance_variables - vars.delete "@ttl" + vars.delete :@ttl vars.each {|name| h ^= self.instance_variable_get(name).hash } @@ -1516,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 @@ -1806,10 +2476,10 @@ class Resolv 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: @@ -1823,14 +2493,166 @@ class Resolv 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 + ## + # CAA resource record defined in RFC 8659 + # + # These records identify certificate authority allowed to issue + # certificates for the given domain. + + class CAA < Resource + TypeValue = 257 + + ## + # Creates a new CAA for +flags+, +tag+ and +value+. + + def initialize(flags, tag, value) + unless (0..255) === flags + raise ArgumentError.new('flags must be an Integer between 0 and 255') + end + unless (1..15) === tag.bytesize + raise ArgumentError.new('length of tag must be between 1 and 15') + end + + @flags = flags + @tag = tag + @value = value + end + + ## + # Flags for this property: + # - Bit 0 : 0 = not critical, 1 = critical + + attr_reader :flags + + ## + # Property tag ("issue", "issuewild", "iodef"...). + + attr_reader :tag + + ## + # Property value. + + attr_reader :value + + ## + # Whether the critical flag is set on this property. + + def critical? + flags & 0x80 != 0 + end + + def encode_rdata(msg) # :nodoc: + msg.put_pack('C', @flags) + msg.put_string(@tag) + msg.put_bytes(@value) + end + + def self.decode_rdata(msg) # :nodoc: + flags, = msg.get_unpack('C') + tag = msg.get_string + value = msg.get_bytes + self.new flags, tag, value + end + end + ClassInsensitiveTypes = [ # :nodoc: - NS, CNAME, SOA, PTR, HINFO, MINFO, MX, TXT, ANY + NS, CNAME, SOA, PTR, HINFO, MINFO, MX, TXT, LOC, ANY, CAA ] ## @@ -2016,7 +2838,7 @@ class Resolv msg.put_pack("n", @priority) msg.put_pack("n", @weight) msg.put_pack("n", @port) - msg.put_name(@target) + msg.put_name(@target, compress: false) end def self.decode_rdata(msg) # :nodoc: @@ -2027,6 +2849,84 @@ class Resolv return self.new(priority, weight, port, target) end end + + ## + # Common implementation for SVCB-compatible resource records. + + class ServiceBinding + + ## + # Create a service binding resource record. + + def initialize(priority, target, params = []) + @priority = priority.to_int + @target = Name.create(target) + @params = SvcParams.new(params) + end + + ## + # The priority of this target host. + # + # The range is 0-65535. + # If set to 0, this RR is in AliasMode. Otherwise, it is in ServiceMode. + + attr_reader :priority + + ## + # The domain name of the target host. + + attr_reader :target + + ## + # The service parameters for the target host. + + attr_reader :params + + ## + # Whether this RR is in AliasMode. + + def alias_mode? + self.priority == 0 + end + + ## + # Whether this RR is in ServiceMode. + + def service_mode? + !alias_mode? + end + + def encode_rdata(msg) # :nodoc: + msg.put_pack("n", @priority) + msg.put_name(@target, compress: false) + @params.encode(msg) + end + + def self.decode_rdata(msg) # :nodoc: + priority, = msg.get_unpack("n") + target = msg.get_name + params = SvcParams.decode(msg) + return self.new(priority, target, params) + end + end + + ## + # SVCB resource record [RFC9460] + + class SVCB < ServiceBinding + TypeValue = 64 + ClassValue = IN::ClassValue + ClassHash[[TypeValue, ClassValue]] = self # :nodoc: + end + + ## + # HTTPS resource record [RFC9460] + + class HTTPS < ServiceBinding + TypeValue = 65 + ClassValue = IN::ClassValue + ClassHash[[TypeValue, ClassValue]] = self # :nodoc: + end end end end @@ -2036,10 +2936,20 @@ class Resolv class IPv4 + Regex256 = /0 + |1(?:[0-9][0-9]?)? + |2(?:[0-4][0-9]?|5[0-5]?|[6-9])? + |[3-9][0-9]?/x # :nodoc: + ## # Regular expression IPv4 addresses must match. + Regex = /\A(#{Regex256})\.(#{Regex256})\.(#{Regex256})\.(#{Regex256})\z/ - Regex = /\A(\d+)\.(\d+)\.(\d+)\.(\d+)\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 @@ -2060,8 +2970,11 @@ class Resolv end def initialize(address) # :nodoc: - unless address.kind_of?(String) && address.length == 4 - raise ArgumentError.new('IPv4 address must be 4 bytes') + unless address.kind_of?(String) + raise ArgumentError, 'IPv4 address must be a string' + end + unless address.length == 4 + raise ArgumentError, "IPv4 address expects 4 bytes but #{address.length} bytes" end @address = address end @@ -2079,7 +2992,7 @@ class Resolv end def inspect # :nodoc: - return "#<#{self.class} #{self.to_s}>" + return "#<#{self.class} #{self}>" end ## @@ -2141,13 +3054,38 @@ class Resolv \z/x ## + # IPv6 link local address format fe80:b:c:d:e:f:g:h%em1 + Regex_8HexLinkLocal = /\A + [Ff][Ee]80 + (?::[0-9A-Fa-f]{1,4}){7} + %[-0-9A-Za-z._~]+ + \z/x + + ## + # Compressed IPv6 link local address format fe80::b%em1 + + Regex_CompressedHexLinkLocal = /\A + [Ff][Ee]80: + (?: + ((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?) :: + ((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?) + | + :((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?) + )? + :[0-9A-Fa-f]{1,4}%[-0-9A-Za-z._~]+ + \z/x + + ## # A composite IPv6 address Regexp. Regex = / (?:#{Regex_8Hex}) | (?:#{Regex_CompressedHex}) | (?:#{Regex_6Hex4Dec}) | - (?:#{Regex_CompressedHex4Dec})/x + (?:#{Regex_CompressedHex4Dec}) | + (?:#{Regex_8HexLinkLocal}) | + (?:#{Regex_CompressedHexLinkLocal}) + /x ## # Creates a new IPv6 address from +arg+ which may be: @@ -2160,14 +3098,14 @@ class Resolv when IPv6 return arg when String - address = '' + address = ''.b if Regex_8Hex =~ arg arg.scan(/[0-9A-Fa-f]+/) {|hex| address << [hex.hex].pack('n')} elsif Regex_CompressedHex =~ arg prefix = $1 suffix = $2 - a1 = '' - a2 = '' + a1 = ''.b + a2 = ''.b prefix.scan(/[0-9A-Fa-f]+/) {|hex| a1 << [hex.hex].pack('n')} suffix.scan(/[0-9A-Fa-f]+/) {|hex| a2 << [hex.hex].pack('n')} omitlen = 16 - a1.length - a2.length @@ -2183,8 +3121,8 @@ class Resolv elsif Regex_CompressedHex4Dec =~ arg prefix, suffix, a, b, c, d = $1, $2, $3.to_i, $4.to_i, $5.to_i, $6.to_i if (0..255) === a && (0..255) === b && (0..255) === c && (0..255) === d - a1 = '' - a2 = '' + a1 = ''.b + a2 = ''.b prefix.scan(/[0-9A-Fa-f]+/) {|hex| a1 << [hex.hex].pack('n')} suffix.scan(/[0-9A-Fa-f]+/) {|hex| a2 << [hex.hex].pack('n')} omitlen = 12 - a1.length - a2.length @@ -2214,15 +3152,11 @@ class Resolv attr_reader :address def to_s # :nodoc: - address = sprintf("%X:%X:%X:%X:%X:%X:%X:%X", *@address.unpack("nnnnnnnn")) - unless address.sub!(/(^|:)0(:0)+(:|$)/, '::') - address.sub!(/(^|:)0(:|$)/, '::') - end - return address + sprintf("%x:%x:%x:%x:%x:%x:%x:%x", *@address.unpack("nnnnnnnn")).sub(/(^|:)0(:0)+(:|$)/, '::') end def inspect # :nodoc: - return "#<#{self.class} #{self.to_s}>" + return "#<#{self.class} #{self}>" end ## @@ -2249,14 +3183,331 @@ class Resolv 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[-1].to_s == 'local' + + super(name) + end + + def make_udp_requester # :nodoc: + nameserver_port = @config.nameserver_port + Requester::MDNSOneShot.new(*nameserver_port) + end + + end + + module LOC # :nodoc: + + ## + # A Resolv::LOC::Size + + class Size + + # Regular expression LOC size must match. + + Regex = /\A0*(\d{1,8}(?:\.\d+)?)m\z/ + + ## + # 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 + unless Regex =~ arg + raise ArgumentError.new("not a properly formed Size string: " + arg) + end + 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 + + ## + # The raw size + + attr_reader :scalar + + def to_s # :nodoc: + s, = @scalar.unpack("C") + return "#{(s >> 4) * (10.0 ** ((s & 0xf) - 2))}m" + end + + def inspect # :nodoc: + return "#<#{self.class} #{self}>" + 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 + + # 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: + # + # 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 + unless m = Regex.match(arg) + raise ArgumentError.new("not a properly formed Coord string: " + arg) + end + + 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) and coordinates.bytesize == 4 + raise ArgumentError.new("Coord must be a 32bit unsigned integer in hex format: #{coordinates.inspect}") + end + unless orientation == "lon" || orientation == "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") + 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 + @orientation == "lat" ? "N" : "E" + end + format("%d %02d %02d.%03d %s", degs, mins, secs, fracsecs, hemi) + end + + def inspect # :nodoc: + return "#<#{self.class} #{self}>" + 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 + + # 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: + # + # 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 + unless Regex =~ arg + raise ArgumentError.new("not a properly formed Alt string: " + arg) + end + 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 + + ## + # The raw altitude + + attr_reader :altitude + + def to_s # :nodoc: + a, = @altitude.unpack("N") + return "#{(a - Bias).fdiv(100)}m" + end + + def inspect # :nodoc: + return "#<#{self.class} #{self}>" + 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 - |
