summaryrefslogtreecommitdiff
path: root/lib/resolv.rb
diff options
context:
space:
mode:
Diffstat (limited to 'lib/resolv.rb')
-rw-r--r--lib/resolv.rb2885
1 files changed, 2344 insertions, 541 deletions
diff --git a/lib/resolv.rb b/lib/resolv.rb
index edc17c2f19..6b58f92813 100644
--- a/lib/resolv.rb
+++ b/lib/resolv.rb
@@ -1,231 +1,124 @@
-=begin
-= resolv library
-resolv.rb is a resolver library written in Ruby.
-Since it is written in Ruby, it is thread-aware.
-I.e. it can resolv many hostnames concurrently.
-
-It is possible to lookup various resources of DNS using DNS module directly.
-
-== example
- Resolv.getaddress("www.ruby-lang.org")
- Resolv.getname("210.251.121.214")
-
- dns = Resolv::DNS.new
- dns.getresources("www.ruby-lang.org", Resolv::DNS::Resource::IN::A).collect {|r| r.address}
- dns.getresources("ruby-lang.org", Resolv::DNS::Resource::IN::MX).collect {|r| [r.exchange.to_s, r.preference]}
-
-== Resolv class
-
-=== class methods
---- Resolv.getaddress(name)
---- Resolv.getaddresses(name)
---- Resolv.each_address(name) {|address| ...}
- They lookups IP addresses of ((|name|)) which represents a hostname
- as a string by default resolver.
-
- getaddress returns first entry of lookupped addresses.
- getaddresses returns lookupped addresses as an array.
- each_address iterates over lookupped addresses.
-
---- Resolv.getname(address)
---- Resolv.getnames(address)
---- Resolv.each_name(address) {|name| ...}
- lookups hostnames of ((|address|)) which represents IP address as a string.
-
- getname returns first entry of lookupped names.
- getnames returns lookupped names as an array.
- each_names iterates over lookupped names.
-
-== Resolv::Hosts class
-hostname resolver using /etc/hosts format.
-
-=== class methods
---- Resolv::Hosts.new(hosts='/etc/hosts')
-
-=== methods
---- Resolv::Hosts#getaddress(name)
---- Resolv::Hosts#getaddresses(name)
---- Resolv::Hosts#each_address(name) {|address| ...}
- address lookup methods.
-
---- Resolv::Hosts#getname(address)
---- Resolv::Hosts#getnames(address)
---- Resolv::Hosts#each_name(address) {|name| ...}
- hostnames lookup methods.
-
-== Resolv::DNS class
-DNS stub resolver.
-
-=== class methods
---- Resolv::DNS.new(resolv_conf='/etc/resolv.conf')
-
---- Resolv::DNS.open(resolv_conf='/etc/resolv.conf')
---- Resolv::DNS.open(resolv_conf='/etc/resolv.conf') {|dns| ...}
-
-=== methods
---- Resolv::DNS#close
-
---- Resolv::DNS#getaddress(name)
---- Resolv::DNS#getaddresses(name)
---- Resolv::DNS#each_address(name) {|address| ...}
- address lookup methods.
-
- ((|name|)) must be a instance of Resolv::DNS::Name or String. Lookupped
- address is represented as an instance of Resolv::IPv4 or Resolv::IPv6.
-
---- Resolv::DNS#getname(address)
---- Resolv::DNS#getnames(address)
---- Resolv::DNS#each_name(address) {|name| ...}
- hostnames lookup methods.
-
- ((|address|)) must be a instance of Resolv::IPv4, Resolv::IPv6 or String.
- Lookupped name is represented as an instance of Resolv::DNS::Name.
-
---- Resolv::DNS#getresource(name, typeclass)
---- Resolv::DNS#getresources(name, typeclass)
---- Resolv::DNS#each_resource(name, typeclass) {|resource| ...}
- They lookup DNS resources of ((|name|)).
- ((|name|)) must be a instance of Resolv::Name or String.
-
- ((|typeclass|)) should be one of follows:
- * Resolv::DNS::Resource::IN::ANY
- * Resolv::DNS::Resource::IN::NS
- * Resolv::DNS::Resource::IN::CNAME
- * Resolv::DNS::Resource::IN::SOA
- * Resolv::DNS::Resource::IN::HINFO
- * Resolv::DNS::Resource::IN::MINFO
- * Resolv::DNS::Resource::IN::MX
- * Resolv::DNS::Resource::IN::TXT
- * Resolv::DNS::Resource::IN::ANY
- * Resolv::DNS::Resource::IN::A
- * Resolv::DNS::Resource::IN::WKS
- * Resolv::DNS::Resource::IN::PTR
- * Resolv::DNS::Resource::IN::AAAA
-
- Lookupped resource is represented as an instance of (a subclass of)
- Resolv::DNS::Resource.
- (Resolv::DNS::Resource::IN::A, etc.)
-
-== Resolv::DNS::Resource::IN::NS class
---- name
-== Resolv::DNS::Resource::IN::CNAME class
---- name
-== Resolv::DNS::Resource::IN::SOA class
---- mname
---- rname
---- serial
---- refresh
---- retry
---- expire
---- minimum
-== Resolv::DNS::Resource::IN::HINFO class
---- cpu
---- os
-== Resolv::DNS::Resource::IN::MINFO class
---- rmailbx
---- emailbx
-== Resolv::DNS::Resource::IN::MX class
---- preference
---- exchange
-== Resolv::DNS::Resource::IN::TXT class
---- data
-== Resolv::DNS::Resource::IN::A class
---- address
-== Resolv::DNS::Resource::IN::WKS class
---- address
---- protocol
---- bitmap
-== Resolv::DNS::Resource::IN::PTR class
---- name
-== Resolv::DNS::Resource::IN::AAAA class
---- address
-
-== Resolv::DNS::Name class
-
-=== class methods
---- Resolv::DNS::Name.create(name)
-
-=== methods
---- Resolv::DNS::Name#to_s
-
-== Resolv::DNS::Resource class
-
-== Resolv::IPv4 class
-=== class methods
---- Resolv::IPv4.create(address)
-
-=== methods
---- Resolv::IPv4#to_s
---- Resolv::IPv4#to_name
-
-=== constants
---- Resolv::IPv4::Regex
- regular expression for IPv4 address.
-
-== Resolv::IPv6 class
-=== class methods
---- Resolv::IPv6.create(address)
-
-=== methods
---- Resolv::IPv6#to_s
---- Resolv::IPv6#to_name
-
-=== constants
---- Resolv::IPv6::Regex
- regular expression for IPv6 address.
-
-== Bugs
-* NIS is not supported.
-* /etc/nsswitch.conf is not supported.
-* IPv6 is not supported.
-
-=end
+# frozen_string_literal: true
require 'socket'
-require 'fcntl'
require 'timeout'
-require 'thread'
+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 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.
+#
+# Examples:
+#
+# p Resolv.getaddress "www.ruby-lang.org"
+# p Resolv.getname "210.251.121.214"
+#
+# Resolv::DNS.open do |dns|
+# ress = dns.getresources "www.ruby-lang.org", Resolv::DNS::Resource::IN::A
+# 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
+#
+#
+# == Bugs
+#
+# * NIS is not supported.
+# * /etc/nsswitch.conf is not supported.
class Resolv
+
+ # The version string
+ VERSION = "0.7.1"
+
+ ##
+ # Looks up the first IP address for +name+.
+
def self.getaddress(name)
DefaultResolver.getaddress(name)
end
+ ##
+ # Looks up all IP address for +name+.
+
def self.getaddresses(name)
DefaultResolver.getaddresses(name)
end
+ ##
+ # Iterates over all IP addresses for +name+.
+
def self.each_address(name, &block)
DefaultResolver.each_address(name, &block)
end
+ ##
+ # Looks up the hostname of +address+.
+
def self.getname(address)
DefaultResolver.getname(address)
end
+ ##
+ # Looks up all hostnames for +address+.
+
def self.getnames(address)
DefaultResolver.getnames(address)
end
+ ##
+ # Iterates over all hostnames for +address+.
+
def self.each_name(address, &proc)
DefaultResolver.each_name(address, &proc)
end
- def initialize(resolvers=[Hosts.new, DNS.new])
- @resolvers = resolvers
+ ##
+ # 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
+
+ @resolvers = case resolvers
+ when Hash, nil
+ [Hosts.new, DNS.new(DNS::Config.default_config_hash.merge(resolvers || {}))]
+ else
+ resolvers
+ end
end
+ ##
+ # Looks up the first IP address for +name+.
+
def getaddress(name)
each_address(name) {|address| return address}
raise ResolvError.new("no address for #{name}")
end
+ ##
+ # Looks up all IP address for +name+.
+
def getaddresses(name)
ret = []
each_address(name) {|address| ret << address}
return ret
end
+ ##
+ # Iterates over all IP addresses for +name+.
+
def each_address(name)
if AddressRegex =~ name
yield name
@@ -241,17 +134,26 @@ class Resolv
}
end
+ ##
+ # Looks up the hostname of +address+.
+
def getname(address)
each_name(address) {|name| return name}
raise ResolvError.new("no name for #{address}")
end
+ ##
+ # Looks up all hostnames for +address+.
+
def getnames(address)
ret = []
each_name(address) {|name| ret << name}
return ret
end
+ ##
+ # Iterates over all hostnames for +address+.
+
def each_name(address)
yielded = false
@resolvers.each {|r|
@@ -263,101 +165,138 @@ class Resolv
}
end
- class ResolvError < StandardError
- end
+ ##
+ # Indicates a failure to resolve a name or address.
- class ResolvTimeout < TimeoutError
- end
+ class ResolvError < StandardError; end
+
+ ##
+ # Indicates a timeout resolving a name or address.
+
+ class ResolvTimeout < Timeout::Error; end
+
+ ##
+ # Resolv::Hosts is a hostname resolver that uses the system hosts file.
class Hosts
- if /mswin32|cygwin|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 Resolv::Hosts, using +filename+ for its data source.
def initialize(filename = DefaultFileName)
@filename = filename
- @mutex = Mutex.new
+ @mutex = Thread::Mutex.new
@initialized = nil
end
- def lazy_initialize
+ def lazy_initialize # :nodoc:
@mutex.synchronize {
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!}
@initialized = true
end
}
+ self
end
+ ##
+ # Gets the IP address of +name+ from the hosts file.
+
def getaddress(name)
each_address(name) {|address| return address}
raise ResolvError.new("#{@filename} has no name: #{name}")
end
+ ##
+ # Gets all IP addresses for +name+ from the hosts file.
+
def getaddresses(name)
ret = []
each_address(name) {|address| ret << address}
return ret
end
+ ##
+ # Iterates over all IP addresses for +name+ retrieved from the hosts file.
+
def each_address(name, &proc)
lazy_initialize
- if @name2addr.include?(name)
- @name2addr[name].each(&proc)
- end
+ @name2addr[name]&.each(&proc)
end
+ ##
+ # Gets the hostname of +address+ from the hosts file.
+
def getname(address)
each_name(address) {|name| return name}
raise ResolvError.new("#{@filename} has no address: #{address}")
end
+ ##
+ # Gets all hostnames for +address+ from the hosts file.
+
def getnames(address)
ret = []
each_name(address) {|name| ret << name}
return ret
end
+ ##
+ # Iterates over all hostnames for +address+ retrieved from the hosts file.
+
def each_name(address, &proc)
lazy_initialize
- if @addr2name.include?(address)
- @addr2name[address].each(&proc)
- end
+ @addr2name[address]&.each(&proc)
end
end
+ ##
+ # Resolv::DNS is a DNS stub resolver.
+ #
+ # Information taken from the following places:
+ #
+ # * STD0013
+ # * RFC 1035
+ # * ftp://ftp.isi.edu/in-notes/iana/assignments/dns-parameters
+ # * etc.
+
class DNS
- # STD0013 (RFC 1035, etc.)
- # ftp://ftp.isi.edu/in-notes/iana/assignments/dns-parameters
+
+ ##
+ # Default DNS Port
Port = 53
+
+ ##
+ # Default DNS UDP packet size
+
UDPSize = 512
- DNSThreadGroup = ThreadGroup.new
+ ##
+ # Creates a new DNS resolver. See Resolv::DNS.new for argument details.
+ #
+ # Yields the created DNS resolver to the block, if given, otherwise
+ # returns it.
def self.open(*args)
dns = new(*args)
@@ -369,68 +308,165 @@ class Resolv
end
end
- def initialize(config="/etc/resolv.conf")
- @mutex = Mutex.new
- @config = Config.new(config)
+ ##
+ # Creates a new DNS resolver.
+ #
+ # +config_info+ can be:
+ #
+ # 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:
+ #
+ # Resolv::DNS.new(:nameserver => ['210.251.121.21'],
+ # :search => ['ruby-lang.org'],
+ # :ndots => 1)
+
+ def initialize(config_info=nil)
+ @mutex = Thread::Mutex.new
+ @config = Config.new(config_info)
@initialized = nil
end
- def lazy_initialize
+ # 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
@config.lazy_initialize
-
- if nameserver = @config.single?
- @requester = Requester::ConnectedUDP.new(nameserver)
- else
- @requester = Requester::UnconnectedUDP.new
- end
-
@initialized = true
end
}
+ self
end
+ ##
+ # Closes the DNS resolver.
+
def close
@mutex.synchronize {
if @initialized
- @requester.close if @requester
- @requester = nil
@initialized = false
end
}
end
+ ##
+ # Gets the IP address of +name+ from the DNS resolver.
+ #
+ # +name+ can be a Resolv::DNS::Name or a String. Retrieved address will
+ # be a Resolv::IPv4 or Resolv::IPv6
+
def getaddress(name)
each_address(name) {|address| return address}
raise ResolvError.new("DNS result has no information for #{name}")
end
+ ##
+ # Gets all IP addresses for +name+ from the DNS resolver.
+ #
+ # +name+ can be a Resolv::DNS::Name or a String. Retrieved addresses will
+ # be a Resolv::IPv4 or Resolv::IPv6
+
def getaddresses(name)
ret = []
each_address(name) {|address| ret << address}
return ret
end
+ ##
+ # Iterates over all IP addresses for +name+ retrieved from the DNS
+ # resolver.
+ #
+ # +name+ can be a Resolv::DNS::Name or a String. Retrieved addresses will
+ # 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}
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.
+ #
+ # +address+ must be a Resolv::IPv4, Resolv::IPv6 or a String. Retrieved
+ # name will be a Resolv::DNS::Name.
+
def getname(address)
each_name(address) {|name| return name}
raise ResolvError.new("DNS result has no information for #{address}")
end
+ ##
+ # Gets all hostnames for +address+ from the DNS resolver.
+ #
+ # +address+ must be a Resolv::IPv4, Resolv::IPv6 or a String. Retrieved
+ # names will be Resolv::DNS::Name instances.
+
def getnames(address)
ret = []
each_name(address) {|name| ret << name}
return ret
end
+ ##
+ # Iterates over all hostnames for +address+ retrieved from the DNS
+ # resolver.
+ #
+ # +address+ must be a Resolv::IPv4, Resolv::IPv6 or a String. Retrieved
+ # names will be Resolv::DNS::Name instances.
+
def each_name(address)
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
@@ -441,58 +477,142 @@ class Resolv
each_resource(ptr, Resource::IN::PTR) {|resource| yield resource.name}
end
+ ##
+ # Look up the +typeclass+ DNS resource of +name+.
+ #
+ # +name+ must be a Resolv::DNS::Name or a String.
+ #
+ # +typeclass+ should be one of the following:
+ #
+ # * 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
+ #
+ # Returned resource is represented as a Resolv::DNS::Resource instance,
+ # i.e. Resolv::DNS::Resource::IN::A.
+
def getresource(name, typeclass)
each_resource(name, typeclass) {|resource| return resource}
raise ResolvError.new("DNS result has no information for #{name}")
end
+ ##
+ # Looks up all +typeclass+ DNS resources for +name+. See #getresource for
+ # argument details.
+
def getresources(name, typeclass)
ret = []
each_resource(name, typeclass) {|resource| ret << resource}
return ret
end
+ ##
+ # 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
+
+ # :stopdoc:
+
+ def fetch_resource(name, typeclass)
lazy_initialize
- q = Queue.new
+ 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, q, 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
- sender.send
- reply = reply_name = nil
- timeout(tout, ResolvTimeout) { reply, reply_name = q.pop }
+
+ 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.delete(q)
+ udp_requester&.close
+ requesters.each_value { |requester| requester&.close }
end
end
- def extract_resources(msg, name, typeclass)
+ 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(*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
@@ -504,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
@@ -514,237 +634,457 @@ class Resolv
}
end
- class Requester
+ def self.random(arg) # :nodoc:
+ begin
+ SecureRandom.random_number(arg)
+ rescue NotImplementedError
+ rand(arg)
+ end
+ end
+
+ 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 = random(0x0000..0xffff)
+ end while h[id]
+ h[id] = true
+ }
+ id
+ end
+
+ def self.free_request_id(host, port, id) # :nodoc:
+ RequestIDMutex.synchronize {
+ key = [host, port]
+ if h = RequestID[key]
+ h.delete id
+ if h.empty?
+ RequestID.delete key
+ end
+ end
+ }
+ end
+
+ 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
+
+ class Requester # :nodoc:
def initialize
@senders = {}
+ @socks = nil
end
- def close
- thread, sock, @thread, @sock = @thread, @sock
+ def request(sender, tout)
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
+ timelimit = start + tout
begin
- if thread
- thread.kill
- thread.join
+ 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
+ begin
+ msg = Message.decode(reply)
+ rescue DecodeError
+ next # broken DNS message ignored
+ end
+ if sender == sender_for(from, msg)
+ break
+ else
+ # unexpected DNS message ignored
end
- ensure
- sock.close if sock
end
+ return msg, sender.data
end
- def delete(arg)
- case arg
- when Sender
- @senders.delete_if {|k, s| s == arg }
- when Queue
- @senders.delete_if {|k, s| s.queue == arg }
- else
- raise ArgumentError.new("neither Sender or Queue: #{arg}")
- end
+ def sender_for(addr, msg)
+ @senders[[addr,msg.id]]
+ end
+
+ def close
+ socks = @socks
+ @socks = nil
+ socks&.each(&:close)
end
- class Sender
- def initialize(msg, data, sock, queue)
+ class Sender # :nodoc:
+ def initialize(msg, data, sock)
@msg = msg
@data = data
@sock = sock
- @queue = queue
- end
- attr_reader :queue
-
- def recv(msg)
- @queue.push([msg, @data])
end
end
- class UnconnectedUDP < Requester
- def initialize
+ class UnconnectedUDP < Requester # :nodoc:
+ def initialize(*nameserver_port)
super()
- @sock = UDPSocket.new
- @sock.fcntl(Fcntl::F_SETFD, 1) if defined? Fcntl::F_SETFD
- @id = {}
- @id.default = -1
- @thread = Thread.new {
- DNSThreadGroup.add Thread.current
- loop {
- reply, from = @sock.recvfrom(UDPSize)
- msg = begin
- Message.decode(reply)
- rescue DecodeError
- STDERR.print("DNS message decoding error: #{reply.inspect}\n")
- next
- end
- if s = @senders[[[from[3],from[1]],msg.id]]
- s.recv msg
+ @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
- #STDERR.print("non-handled DNS message: #{msg.inspect} from #{from.inspect}\n")
+ 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(readable_socks)
+ lazy_initialize
+ reply, from = readable_socks[0].recvfrom(UDPSize)
+ return reply, [from[3],from[1]]
end
- def sender(msg, data, queue, host, port=Port)
+ 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 = Thread.exclusive {
- @id[service] = (@id[service] + 1) & 0xffff
- }
+ 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, queue)
+ Sender.new(request, data, sock, host, port)
end
- class Sender < Requester::Sender
- def initialize(msg, data, sock, host, port, queue)
- super(msg, data, sock, queue)
+ def close
+ @mutex.synchronize {
+ if @initialized
+ super
+ @senders.each_key {|service, id|
+ DNS.free_request_id(service[0], service[1], id)
+ }
+ @initialized = false
+ end
+ }
+ end
+
+ class Sender < Requester::Sender # :nodoc:
+ def initialize(msg, data, sock, host, port)
+ super(msg, data, sock)
@host = host
@port = port
end
+ attr_reader :data
def send
+ raise "@sock is nil." if @sock.nil?
@sock.send(@msg, 0, @host, @port)
end
end
end
- class ConnectedUDP < Requester
+ class ConnectedUDP < Requester # :nodoc:
def initialize(host, port=Port)
super()
@host = host
@port = port
- @sock = UDPSocket.new
- @sock.connect(host, port)
- @sock.fcntl(Fcntl::F_SETFD, 1) if defined? Fcntl::F_SETFD
- @id = -1
- @thread = Thread.new {
- DNSThreadGroup.add Thread.current
- loop {
- reply = @sock.recv(UDPSize)
- msg = begin
- Message.decode(reply)
- rescue DecodeError
- STDERR.print("DNS message decoding error: #{reply.inspect}")
- next
- end
- if s = @senders[msg.id]
- s.recv msg
- else
- #STDERR.print("non-handled DNS message: #{msg.inspect}")
- end
- }
+ @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(readable_socks)
+ lazy_initialize
+ reply = readable_socks[0].recv(UDPSize)
+ return reply, nil
end
- def sender(msg, data, queue, host=@host, port=@port)
+ 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 = Thread.exclusive { @id = (@id + 1) & 0xffff }
+ id = DNS.allocate_request_id(@host, @port)
request = msg.encode
request[0,2] = [id].pack('n')
- return @senders[id] = Sender.new(request, data, @sock, queue)
+ return @senders[[nil,id]] = Sender.new(request, data, @socks[0])
end
- class Sender < Requester::Sender
+ def close
+ @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 TCP < Requester
+ 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
- @sock.connect(host, port)
- @sock.fcntl(Fcntl::F_SETFD, 1) if defined? Fcntl::F_SETFD
- @id = -1
+ sock = TCPSocket.new(@host, @port)
+ @socks = [sock]
@senders = {}
- @thread = Thread.new {
- DNSThreadGroup.add Thread.current
- loop {
- len = @sock.read(2).unpack('n')
- reply = @sock.read(len)
- msg = begin
- Message.decode(reply)
- rescue DecodeError
- STDERR.print("DNS message decoding error: #{reply.inspect}")
- next
- end
- if s = @senders[msg.id]
- s.push msg
- else
- #STDERR.print("non-handled DNS message: #{msg.inspect}")
- end
- }
- }
end
- def sender(msg, data, queue, host=@host, port=@port)
+ 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
+
+ def sender(msg, data, host=@host, port=@port)
unless host == @host && port == @port
raise RequestError.new("host/port don't match: #{host}:#{port}")
end
- id = Thread.exclusive { @id = (@id + 1) & 0xffff }
+ id = DNS.allocate_request_id(@host, @port)
request = msg.encode
request[0,2] = [request.length, id].pack('nn')
- return @senders[id] = Sender.new(request, data, @sock, queue)
+ return @senders[[nil,id]] = Sender.new(request, data, @socks[0])
end
- class Sender < Requester::Sender
+ class Sender < Requester::Sender # :nodoc:
def send
@sock.print(@msg)
@sock.flush
end
+ attr_reader :data
+ end
+
+ def close
+ super
+ @senders.each_key {|from,id|
+ DNS.free_request_id(@host, @port, id)
+ }
end
end
+ ##
+ # Indicates a problem with the DNS request.
+
class RequestError < StandardError
end
end
- class Config
- def initialize(filename="/etc/resolv.conf")
- @mutex = Mutex.new
- @filename = filename
+ class Config # :nodoc:
+ def initialize(config_info=nil)
+ @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
+ File.open(filename, 'rb') {|f|
+ f.each {|line|
+ line.sub!(/[#;].*/, '')
+ keyword, *args = line.split(/\s+/)
+ next unless keyword
+ case keyword
+ when 'nameserver'
+ nameserver.concat(args.each(&:freeze))
+ when 'domain'
+ next if args.empty?
+ search = [args[0].freeze]
+ when 'search'
+ next if args.empty?
+ search = args.each(&:freeze)
+ when 'options'
+ args.each {|arg|
+ case arg
+ when /\Andots:(\d+)\z/
+ ndots = $1.to_i
+ end
+ }
+ end
+ }
+ }
+ 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.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
+ {}
+ end
end
def lazy_initialize
@mutex.synchronize {
unless @initialized
- @nameserver = []
+ @nameserver_port = []
+ @use_ipv6 = nil
@search = nil
@ndots = 1
- begin
- open(@filename) {|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
- when 'domain'
- @search = [Label.split(args[0])]
- when 'search'
- @search = args.map {|arg| Label.split(arg)}
- end
- }
- }
- rescue Errno::ENOENT
- if /mswin32|cygwin|mingw|bccwin/ =~ RUBY_PLATFORM
- search, nameserver = Win32::Resolv.get_resolv_info
- @search = [search] if search
- @nameserver = nameserver if nameserver
+ case @config_info
+ when nil
+ config_hash = Config.default_config_hash
+ when String
+ config_hash = Config.parse_resolv_conf(@config_info)
+ when Hash
+ config_hash = @config_info.dup
+ if String === config_hash[:nameserver]
+ config_hash[:nameserver] = [config_hash[:nameserver]]
end
+ if String === config_hash[:search]
+ config_hash[:search] = [config_hash[:search]]
+ end
+ else
+ raise ArgumentError.new("invalid resolv configuration: #{@config_info.inspect}")
+ end
+ 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?
- unless @search
+ if @nameserver_port.empty?
+ @nameserver_port << ['0.0.0.0', Port]
+ end
+ if @search
+ @search = @search.map {|arg| Label.split(arg) }
+ else
hostname = Socket.gethostname
if /\./ =~ hostname
@search = [Label.split($')]
@@ -752,20 +1092,49 @@ class Resolv
@search = [[]]
end
end
+
+ 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) ||
+ !@search.all? {|ls| ls.all? {|l| Label::Str === l } }
+ raise ArgumentError.new("invalid search config: #{@search.inspect}")
+ end
+
+ if !@ndots.kind_of?(Integer)
+ raise ArgumentError.new("invalid ndots config: #{@ndots.inspect}")
+ end
+
@initialized = true
end
}
+ self
end
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)
@@ -778,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
@@ -786,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
@@ -794,36 +1167,43 @@ 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 OtherResolvError
- raise ResolvError.new("DNS error: #{$!.message}")
+ rescue ResolvError
+ raise if @raise_timeout_errors && timeout_error
end
- raise ResolvError.new("DNS resolv error: #{name}")
end
+ ##
+ # Indicates no such domain was found.
+
class NXDomain < ResolvError
end
+ ##
+ # Indicates some other unhandled resolver error was encountered.
+
class OtherResolvError < ResolvError
end
end
- module OpCode
+ module OpCode # :nodoc:
Query = 0
IQuery = 1
Status = 2
@@ -831,7 +1211,7 @@ class Resolv
Update = 5
end
- module RCode
+ module RCode # :nodoc:
NoError = 0
FormErr = 1
ServFail = 2
@@ -852,23 +1232,31 @@ class Resolv
BADALG = 21
end
+ ##
+ # Indicates that the DNS response was unable to be decoded.
+
class DecodeError < StandardError
end
+ ##
+ # Indicates that the DNS request was unable to be encoded.
+
class EncodeError < StandardError
end
- module Label
+ module Label # :nodoc:
def self.split(arg)
labels = []
arg.scan(/[^\.]+/) {labels << Str.new($&)}
return labels
end
- class Str
+ 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
@@ -877,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)
@@ -894,7 +1282,17 @@ class Resolv
end
end
+ ##
+ # A representation of a DNS name.
+
class Name
+
+ ##
+ # Creates a new DNS name from +arg+. +arg+ can be:
+ #
+ # Name:: returns +arg+.
+ # String:: Creates a new Name.
+
def self.create(arg)
case arg
when Name
@@ -906,42 +1304,93 @@ class Resolv
end
end
- def initialize(labels, absolute=true)
+ 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}#{@absolute ? '.' : ''}>"
+ end
+
+ ##
+ # True if this name is absolute.
+
def absolute?
return @absolute
end
- def ==(other)
- return @labels == other.to_a && @absolute == other.absolute?
+ def ==(other) # :nodoc:
+ return false unless Name === other
+ return false unless @absolute == other.absolute?
+ return @labels == other.to_a
end
- alias eql? ==
- def hash
+ alias eql? == # :nodoc:
+
+ ##
+ # Returns true if +other+ is a subdomain.
+ #
+ # Example:
+ #
+ # domain = Resolv::DNS::Name.create("y.z")
+ # p Resolv::DNS::Name.create("w.x.y.z").subdomain_of?(domain) #=> true
+ # p Resolv::DNS::Name.create("x.y.z").subdomain_of?(domain) #=> true
+ # p Resolv::DNS::Name.create("y.z").subdomain_of?(domain) #=> false
+ # p Resolv::DNS::Name.create("z").subdomain_of?(domain) #=> false
+ # p Resolv::DNS::Name.create("x.y.z.").subdomain_of?(domain) #=> false
+ # p Resolv::DNS::Name.create("w.z").subdomain_of?(domain) #=> false
+ #
+
+ def subdomain_of?(other)
+ raise ArgumentError, "not a domain name: #{other.inspect}" unless Name === other
+ return false if @absolute != other.absolute?
+ other_len = other.length
+ return false if @labels.length <= other_len
+ return @labels[-other_len, other_len] == other.to_a
+ end
+
+ def hash # :nodoc:
return @labels.hash ^ @absolute.hash
end
- def to_a
+ def to_a # :nodoc:
return @labels
end
- def length
+ def length # :nodoc:
return @labels.length
end
- def [](i)
+ def [](i) # :nodoc:
return @labels[i]
end
+ ##
+ # returns the domain name as a string.
+ #
+ # The domain name doesn't have a trailing dot even if the name object is
+ # absolute.
+ #
+ # Example:
+ #
+ # p Resolv::DNS::Name.create("x.y.z.").to_s #=> "x.y.z"
+ # p Resolv::DNS::Name.create("x.y.z").to_s #=> "x.y.z"
+
def to_s
return @labels.join('.')
end
end
- class Message
+ class Message # :nodoc:
@@identifier = -1
def initialize(id = (@@identifier += 1) & 0xffff)
@@ -1054,9 +1503,9 @@ class Resolv
}.to_s
end
- class MessageEncoder
+ class MessageEncoder # :nodoc:
def initialize
- @data = ''
+ @data = ''.dup
@names = {}
yield self
end
@@ -1087,18 +1536,26 @@ class Resolv
@data << d
end
- def put_name(d)
- put_labels(d.to_a)
+ def put_string_list(ds)
+ ds.each {|d|
+ self.put_string(d)
+ }
+ end
+
+ 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
}
@@ -1106,7 +1563,7 @@ class Resolv
end
def put_label(d)
- self.put_string(d.string)
+ self.put_string(d.to_s)
end
end
@@ -1116,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)
@@ -1143,30 +1602,35 @@ class Resolv
return o
end
- class MessageDecoder
+ class MessageDecoder # :nodoc:
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
@limit = @index + len
d = yield(len)
if @index < @limit
- raise DecodeError.new("junk exist")
+ raise DecodeError.new("junk exists")
elsif @limit < @index
- raise DecodeError.new("limit exceed")
+ raise DecodeError.new("limit exceeded")
end
@limit = save_limit
return d
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
@@ -1174,6 +1638,7 @@ class Resolv
def get_unpack(template)
len = 0
template.each_byte {|byte|
+ byte = "%c" % byte
case byte
when ?c, ?C
len += 1
@@ -1185,47 +1650,72 @@ class Resolv
raise StandardError.new("unsupported template: '#{byte.chr}' in '#{template}'")
end
}
- raise DecodeError.new("limit exceed") if @limit < @index + len
+ raise DecodeError.new("limit exceeded") if @limit < @index + len
arr = @data.unpack("@#{@index}#{template}")
@index += len
return arr
end
def get_string
- len = @data[@index]
- raise DecodeError.new("limit exceed") if @limit < @index + 1 + len
- d = @data[@index + 1, len]
+ raise DecodeError.new("limit exceeded") if @limit <= @index
+ len = @data.getbyte(@index)
+ raise DecodeError.new("limit exceeded") if @limit < @index + 1 + len
+ d = @data.byteslice(@index + 1, len)
@index += 1 + len
return d
end
+ def get_string_list
+ strings = []
+ while @index < @limit
+ strings << self.get_string
+ end
+ 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]
+ 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
@@ -1242,71 +1732,490 @@ class Resolv
name = self.get_name
type, klass, ttl = self.get_unpack('nnN')
typeclass = Resource.get_class(type, klass)
- return name, ttl, 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
+ end
+ 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
- def encode_rdata(msg)
- raise EncodeError.new("#{self.class} is query.")
+ def encode_rdata(msg) # :nodoc:
+ raise EncodeError.new("#{self.class} is query.")
end
- def self.decode_rdata(msg)
- raise DecodeError.new("#{self.class} is query.")
+ def self.decode_rdata(msg) # :nodoc:
+ raise DecodeError.new("#{self.class} is query.")
end
end
+ ##
+ # A DNS resource abstract class.
+
class Resource < Query
- ClassHash = {}
- def encode_rdata(msg)
+ ##
+ # Remaining Time To Live for this Resource.
+
+ attr_reader :ttl
+
+ 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
end
- def self.decode_rdata(msg)
+ def self.decode_rdata(msg) # :nodoc:
raise NotImplementedError.new
end
- def ==(other)
- return self.class == other.class &&
- self.instance_variables == other.instance_variables &&
- self.instance_variables.collect {|name| self.instance_eval name} ==
- other.instance_variables.collect {|name| other.instance_eval name}
+ def ==(other) # :nodoc:
+ return false unless self.class == other.class
+ s_ivars = self.instance_variables
+ s_ivars.sort!
+ s_ivars.delete :@ttl
+ o_ivars = other.instance_variables
+ o_ivars.sort!
+ 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}
end
- def eql?(other)
+ def eql?(other) # :nodoc:
return self == other
end
- def hash
+ def hash # :nodoc:
h = 0
- self.instance_variables.each {|name|
- h ^= self.instance_eval("#{name}.hash")
+ vars = self.instance_variables
+ vars.delete :@ttl
+ vars.each {|name|
+ h ^= self.instance_variable_get(name).hash
}
return h
end
- def self.get_class(type_value, class_value)
- return ClassHash[[type_value, class_value]] ||
+ def self.get_class(type_value, class_value) # :nodoc:
+ cache = :"Type#{type_value}_Class#{class_value}"
+
+ return (const_defined?(cache) && const_get(cache)) ||
Generic.create(type_value, class_value)
end
+ ##
+ # A generic resource abstract class.
+
class Generic < Resource
+
+ ##
+ # Creates a new generic resource.
+
def initialize(data)
@data = data
end
+
+ ##
+ # Data for this generic resource.
+
attr_reader :data
- def encode_rdata(msg)
+ def encode_rdata(msg) # :nodoc:
msg.put_bytes(data)
end
- def self.decode_rdata(msg)
+ def self.decode_rdata(msg) # :nodoc:
return self.new(msg.get_bytes)
end
- def self.create(type_value, class_value)
+ def self.create(type_value, class_value) # :nodoc:
c = Class.new(Generic)
c.const_set(:TypeValue, type_value)
c.const_set(:ClassValue, class_value)
@@ -1316,34 +2225,60 @@ class Resolv
end
end
+ ##
+ # Domain Name resource abstract class.
+
class DomainName < Resource
+
+ ##
+ # Creates a new DomainName from +name+.
+
def initialize(name)
@name = name
end
+
+ ##
+ # The name of this DomainName.
+
attr_reader :name
- def encode_rdata(msg)
+ def encode_rdata(msg) # :nodoc:
msg.put_name(@name)
end
- def self.decode_rdata(msg)
+ def self.decode_rdata(msg) # :nodoc:
return self.new(msg.get_name)
end
end
# Standard (class generic) RRs
- ClassValue = nil
+
+ ClassValue = nil # :nodoc:
+
+ ##
+ # An authoritative name server.
class NS < DomainName
- TypeValue = 2
+ TypeValue = 2 # :nodoc:
end
+ ##
+ # The canonical name for an alias.
+
class CNAME < DomainName
- TypeValue = 5
+ TypeValue = 5 # :nodoc:
end
+ ##
+ # Start Of Authority resource.
+
class SOA < Resource
- TypeValue = 6
+
+ TypeValue = 6 # :nodoc:
+
+ ##
+ # Creates a new SOA record. See the attr documentation for the
+ # details of each argument.
def initialize(mname, rname, serial, refresh, retry_, expire, minimum)
@mname = mname
@@ -1354,15 +2289,52 @@ class Resolv
@expire = expire
@minimum = minimum
end
- attr_reader :mname, :rname, :serial, :refresh, :retry, :expire, :minimum
- def encode_rdata(msg)
+ ##
+ # Name of the host where the master zone file for this zone resides.
+
+ attr_reader :mname
+
+ ##
+ # The person responsible for this domain name.
+
+ attr_reader :rname
+
+ ##
+ # The version number of the zone file.
+
+ attr_reader :serial
+
+ ##
+ # How often, in seconds, a secondary name server is to check for
+ # updates from the primary name server.
+
+ attr_reader :refresh
+
+ ##
+ # How often, in seconds, a secondary name server is to retry after a
+ # failure to check for a refresh.
+
+ attr_reader :retry
+
+ ##
+ # Time in seconds that a secondary name server is to use the data
+ # before refreshing from the primary name server.
+
+ attr_reader :expire
+
+ ##
+ # The minimum number of seconds to be used for TTL values in RRs.
+
+ attr_reader :minimum
+
+ def encode_rdata(msg) # :nodoc:
msg.put_name(@mname)
msg.put_name(@rname)
msg.put_pack('NNNNN', @serial, @refresh, @retry, @expire, @minimum)
end
- def self.decode_rdata(msg)
+ def self.decode_rdata(msg) # :nodoc:
mname = msg.get_name
rname = msg.get_name
serial, refresh, retry_, expire, minimum = msg.get_unpack('NNNNN')
@@ -1371,102 +2343,324 @@ class Resolv
end
end
+ ##
+ # A Pointer to another DNS name.
+
class PTR < DomainName
- TypeValue = 12
+ TypeValue = 12 # :nodoc:
end
+ ##
+ # Host Information resource.
+
class HINFO < Resource
- TypeValue = 13
+
+ TypeValue = 13 # :nodoc:
+
+ ##
+ # Creates a new HINFO running +os+ on +cpu+.
def initialize(cpu, os)
@cpu = cpu
@os = os
end
- attr_reader :cpu, :os
- def encode_rdata(msg)
+ ##
+ # CPU architecture for this resource.
+
+ attr_reader :cpu
+
+ ##
+ # Operating system for this resource.
+
+ attr_reader :os
+
+ def encode_rdata(msg) # :nodoc:
msg.put_string(@cpu)
msg.put_string(@os)
end
- def self.decode_rdata(msg)
+ def self.decode_rdata(msg) # :nodoc:
cpu = msg.get_string
os = msg.get_string
return self.new(cpu, os)
end
end
+ ##
+ # Mailing list or mailbox information.
+
class MINFO < Resource
- TypeValue = 14
+
+ TypeValue = 14 # :nodoc:
def initialize(rmailbx, emailbx)
@rmailbx = rmailbx
@emailbx = emailbx
end
- attr_reader :rmailbx, :emailbx
- def encode_rdata(msg)
+ ##
+ # Domain name responsible for this mail list or mailbox.
+
+ attr_reader :rmailbx
+
+ ##
+ # Mailbox to use for error messages related to the mail list or mailbox.
+
+ attr_reader :emailbx
+
+ def encode_rdata(msg) # :nodoc:
msg.put_name(@rmailbx)
msg.put_name(@emailbx)
end
- def self.decode_rdata(msg)
+ def self.decode_rdata(msg) # :nodoc:
rmailbx = msg.get_string
emailbx = msg.get_string
return self.new(rmailbx, emailbx)
end
end
+ ##
+ # Mail Exchanger resource.
+
class MX < Resource
- TypeValue= 15
+
+ TypeValue= 15 # :nodoc:
+
+ ##
+ # Creates a new MX record with +preference+, accepting mail at
+ # +exchange+.
def initialize(preference, exchange)
@preference = preference
@exchange = exchange
end
- attr_reader :preference, :exchange
- def encode_rdata(msg)
+ ##
+ # The preference for this MX.
+
+ attr_reader :preference
+
+ ##
+ # The host of this MX.
+
+ attr_reader :exchange
+
+ def encode_rdata(msg) # :nodoc:
msg.put_pack('n', @preference)
msg.put_name(@exchange)
end
- def self.decode_rdata(msg)
+ def self.decode_rdata(msg) # :nodoc:
preference, = msg.get_unpack('n')
exchange = msg.get_name
return self.new(preference, exchange)
end
end
+ ##
+ # Unstructured text resource.
+
class TXT < Resource
- TypeValue = 16
- def initialize(data)
- @data = data
+ TypeValue = 16 # :nodoc:
+
+ def initialize(first_string, *rest_strings)
+ @strings = [first_string, *rest_strings]
end
- attr_reader :data
- def encode_rdata(msg)
- msg.put_string(@data)
+ ##
+ # Returns an Array of Strings for this TXT record.
+
+ attr_reader :strings
+
+ ##
+ # Returns the concatenated string from +strings+.
+
+ def data
+ @strings.join("")
+ end
+
+ def encode_rdata(msg) # :nodoc:
+ msg.put_string_list(@strings)
+ end
+
+ def self.decode_rdata(msg) # :nodoc:
+ strings = msg.get_string_list
+ 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)
- data = msg.get_string
- return self.new(data)
+ 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
+ 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 = [
- NS, CNAME, SOA, PTR, HINFO, MINFO, MX, TXT, ANY
+ ClassInsensitiveTypes = [ # :nodoc:
+ NS, CNAME, SOA, PTR, HINFO, MINFO, MX, TXT, LOC, ANY, CAA
]
- # ARPA Internet specific RRs
+ ##
+ # module IN contains ARPA Internet specific RRs.
+
module IN
- ClassValue = 1
+
+ ClassValue = 1 # :nodoc:
ClassInsensitiveTypes.each {|s|
c = Class.new(s)
@@ -1476,40 +2670,76 @@ class Resolv
self.const_set(s.name.sub(/.*::/, ''), c)
}
+ ##
+ # IPv4 Address resource
+
class A < Resource
- ClassHash[[TypeValue = 1, ClassValue = ClassValue]] = self
+ TypeValue = 1
+ ClassValue = IN::ClassValue
+ ClassHash[[TypeValue, ClassValue]] = self # :nodoc:
+
+ ##
+ # Creates a new A for +address+.
def initialize(address)
@address = IPv4.create(address)
end
+
+ ##
+ # The Resolv::IPv4 address for this A.
+
attr_reader :address
- def encode_rdata(msg)
+ def encode_rdata(msg) # :nodoc:
msg.put_bytes(@address.address)
end
- def self.decode_rdata(msg)
+ def self.decode_rdata(msg) # :nodoc:
return self.new(IPv4.new(msg.get_bytes(4)))
end
end
+ ##
+ # Well Known Service resource.
+
class WKS < Resource
- ClassHash[[TypeValue = 11, ClassValue = ClassValue]] = self
+ TypeValue = 11
+ ClassValue = IN::ClassValue
+ ClassHash[[TypeValue, ClassValue]] = self # :nodoc:
def initialize(address, protocol, bitmap)
@address = IPv4.create(address)
@protocol = protocol
@bitmap = bitmap
end
- attr_reader :address, :protocol, :bitmap
- def encode_rdata(msg)
+ ##
+ # The host these services run on.
+
+ attr_reader :address
+
+ ##
+ # IP protocol number for these services.
+
+ attr_reader :protocol
+
+ ##
+ # A bit map of enabled services on this host.
+ #
+ # If protocol is 6 (TCP) then the 26th bit corresponds to the SMTP
+ # service (port 25). If this bit is set, then an SMTP server should
+ # be listening on TCP port 25; if zero, SMTP service is not
+ # supported.
+
+ attr_reader :bitmap
+
+ def encode_rdata(msg) # :nodoc:
msg.put_bytes(@address.address)
msg.put_pack("n", @protocol)
msg.put_bytes(@bitmap)
end
- def self.decode_rdata(msg)
+ def self.decode_rdata(msg) # :nodoc:
address = IPv4.new(msg.get_bytes(4))
protocol, = msg.get_unpack("n")
bitmap = msg.get_bytes
@@ -1517,28 +2747,209 @@ class Resolv
end
end
+ ##
+ # An IPv6 address record.
+
class AAAA < Resource
- ClassHash[[TypeValue = 28, ClassValue = ClassValue]] = self
+ TypeValue = 28
+ ClassValue = IN::ClassValue
+ ClassHash[[TypeValue, ClassValue]] = self # :nodoc:
+
+ ##
+ # Creates a new AAAA for +address+.
def initialize(address)
@address = IPv6.create(address)
end
+
+ ##
+ # The Resolv::IPv6 address for this AAAA.
+
attr_reader :address
- def encode_rdata(msg)
+ def encode_rdata(msg) # :nodoc:
msg.put_bytes(@address.address)
end
- def self.decode_rdata(msg)
+ def self.decode_rdata(msg) # :nodoc:
return self.new(IPv6.new(msg.get_bytes(16)))
end
end
+
+ ##
+ # SRV resource record defined in RFC 2782
+ #
+ # These records identify the hostname and port that a service is
+ # available at.
+
+ class SRV < Resource
+ TypeValue = 33
+ ClassValue = IN::ClassValue
+ ClassHash[[TypeValue, ClassValue]] = self # :nodoc:
+
+ # Create a SRV resource record.
+ #
+ # See the documentation for #priority, #weight, #port and #target
+ # for +priority+, +weight+, +port and +target+ respectively.
+
+ def initialize(priority, weight, port, target)
+ @priority = priority.to_int
+ @weight = weight.to_int
+ @port = port.to_int
+ @target = Name.create(target)
+ end
+
+ # The priority of this target host.
+ #
+ # A client MUST attempt to contact the target host with the
+ # lowest-numbered priority it can reach; target hosts with the same
+ # priority SHOULD be tried in an order defined by the weight field.
+ # The range is 0-65535. Note that it is not widely implemented and
+ # should be set to zero.
+
+ attr_reader :priority
+
+ # A server selection mechanism.
+ #
+ # The weight field specifies a relative weight for entries with the
+ # same priority. Larger weights SHOULD be given a proportionately
+ # higher probability of being selected. The range of this number is
+ # 0-65535. Domain administrators SHOULD use Weight 0 when there
+ # isn't any server selection to do, to make the RR easier to read
+ # for humans (less noisy). Note that it is not widely implemented
+ # and should be set to zero.
+
+ attr_reader :weight
+
+ # The port on this target host of this service.
+ #
+ # The range is 0-65535.
+
+ attr_reader :port
+
+ # The domain name of the target host.
+ #
+ # A target of "." means that the service is decidedly not available
+ # at this domain.
+
+ attr_reader :target
+
+ def encode_rdata(msg) # :nodoc:
+ msg.put_pack("n", @priority)
+ msg.put_pack("n", @weight)
+ msg.put_pack("n", @port)
+ msg.put_name(@target, compress: false)
+ end
+
+ def self.decode_rdata(msg) # :nodoc:
+ priority, = msg.get_unpack("n")
+ weight, = msg.get_unpack("n")
+ port, = msg.get_unpack("n")
+ target = msg.get_name
+ 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
+ ##
+ # A Resolv::DNS IPv4 address.
+
class IPv4
- Regex = /\A(\d+)\.(\d+)\.(\d+)\.(\d+)\z/
+
+ 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/
+
+ ##
+ # 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
@@ -1554,85 +2965,147 @@ class Resolv
raise ArgumentError.new("IPv4 address with invalid value: " + arg)
end
else
- raise ArgumentError.new("cannot interprete as IPv4 address: #{arg.inspect}")
+ raise ArgumentError.new("cannot interpret as IPv4 address: #{arg.inspect}")
end
end
- def initialize(address)
- unless address.kind_of?(String) && address.length == 4
- raise ArgumentError.new('IPv4 address must be 4 bytes')
+ def initialize(address) # :nodoc:
+ 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
+
+ ##
+ # A String representation of this IPv4 address.
+
+ ##
+ # The raw IPv4 address as a String.
+
attr_reader :address
- def to_s
+ def to_s # :nodoc:
return sprintf("%d.%d.%d.%d", *@address.unpack("CCCC"))
end
- def inspect
- return "#<#{self.class} #{self.to_s}>"
+ def inspect # :nodoc:
+ return "#<#{self.class} #{self}>"
end
+ ##
+ # Turns this IPv4 address into a Resolv::DNS::Name.
+
def to_name
return DNS::Name.create(
'%d.%d.%d.%d.in-addr.arpa.' % @address.unpack('CCCC').reverse)
end
- def ==(other)
+ def ==(other) # :nodoc:
return @address == other.address
end
- def eql?(other)
+ def eql?(other) # :nodoc:
return self == other
end
- def hash
+ def hash # :nodoc:
return @address.hash
end
end
+ ##
+ # A Resolv::DNS IPv6 address.
+
class IPv6
+
+ ##
+ # IPv6 address format a:b:c:d:e:f:g:h
Regex_8Hex = /\A
(?:[0-9A-Fa-f]{1,4}:){7}
[0-9A-Fa-f]{1,4}
\z/x
+ ##
+ # Compressed IPv6 address format a::b
+
Regex_CompressedHex = /\A
((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?) ::
((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)
\z/x
+ ##
+ # IPv4 mapped IPv6 address format a:b:c:d:e:f:w.x.y.z
+
Regex_6Hex4Dec = /\A
((?:[0-9A-Fa-f]{1,4}:){6,6})
(\d+)\.(\d+)\.(\d+)\.(\d+)
\z/x
+ ##
+ # Compressed IPv4 mapped IPv6 address format a::b:w.x.y.z
+
Regex_CompressedHex4Dec = /\A
((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?) ::
((?:[0-9A-Fa-f]{1,4}:)*)
(\d+)\.(\d+)\.(\d+)\.(\d+)
\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.source}) |
- (?:#{Regex_CompressedHex.source}) |
- (?:#{Regex_6Hex4Dec.source}) |
- (?:#{Regex_CompressedHex4Dec.source})/x
+ (?:#{Regex_8Hex}) |
+ (?:#{Regex_CompressedHex}) |
+ (?:#{Regex_6Hex4Dec}) |
+ (?:#{Regex_CompressedHex4Dec}) |
+ (?:#{Regex_8HexLinkLocal}) |
+ (?:#{Regex_CompressedHexLinkLocal})
+ /x
+
+ ##
+ # Creates a new IPv6 address from +arg+ which may be:
+ #
+ # IPv6:: returns +arg+.
+ # String:: +arg+ must match one of the IPv6::Regex* constants
def self.create(arg)
case arg
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
@@ -1648,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
@@ -1662,49 +3135,379 @@ class Resolv
end
return IPv6.new(address)
else
- raise ArgumentError.new("cannot interprete as IPv6 address: #{arg.inspect}")
+ raise ArgumentError.new("cannot interpret as IPv6 address: #{arg.inspect}")
end
end
- def initialize(address)
+ def initialize(address) # :nodoc:
unless address.kind_of?(String) && address.length == 16
raise ArgumentError.new('IPv6 address must be 16 bytes')
end
@address = address
end
+
+ ##
+ # The raw IPv6 address as a String.
+
attr_reader :address
- def to_s
- address = sprintf("%X:%X:%X:%X:%X:%X:%X:%X", *@address.unpack("nnnnnnnn"))
- unless address.sub!(/(^|:)0(:0)+(:|$)/, '::')
- address.sub!(/(^|:)0(:|$)/, '::')
- end
- return address
+ def to_s # :nodoc:
+ sprintf("%x:%x:%x:%x:%x:%x:%x:%x", *@address.unpack("nnnnnnnn")).sub(/(^|:)0(:0)+(:|$)/, '::')
end
- def inspect
- return "#<#{self.class} #{self.to_s}>"
+ def inspect # :nodoc:
+ return "#<#{self.class} #{self}>"
end
+ ##
+ # Turns this IPv6 address into a Resolv::DNS::Name.
+ #--
+ # ip6.arpa should be searched too. [RFC3152]
+
def to_name
- # ip6.arpa should be searched too. [RFC3152]
return DNS::Name.new(
- @address.unpack("H32")[0].split(//).reverse + ['ip6', 'int'])
+ @address.unpack("H32")[0].split(//).reverse + ['ip6', 'arpa'])
end
- def ==(other)
+ def ==(other) # :nodoc:
return @address == other.address
end
- def eql?(other)
+ def eql?(other) # :nodoc:
return self == other
end
- def hash
+ def hash # :nodoc:
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[-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
- AddressRegex = /(?:#{IPv4::Regex.source})|(?:#{IPv6::Regex.source})/
+
+ ##
+ # 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