summaryrefslogtreecommitdiff
path: root/lib/resolv.rb
diff options
context:
space:
mode:
Diffstat (limited to 'lib/resolv.rb')
-rw-r--r--lib/resolv.rb1031
1 files changed, 844 insertions, 187 deletions
diff --git a/lib/resolv.rb b/lib/resolv.rb
index 1044b95e68..6b58f92813 100644
--- a/lib/resolv.rb
+++ b/lib/resolv.rb
@@ -3,11 +3,8 @@
require 'socket'
require 'timeout'
require 'io/wait'
-
-begin
- require 'securerandom'
-rescue LoadError
-end
+require 'securerandom'
+require 'rbconfig'
# Resolv is a thread-aware DNS resolver library written in Ruby. Resolv can
# handle multiple DNS requests concurrently without blocking the entire Ruby
@@ -37,6 +34,9 @@ end
class Resolv
+ # The version string
+ VERSION = "0.7.1"
+
##
# Looks up the first IP address for +name+.
@@ -81,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
##
@@ -166,13 +179,15 @@ class Resolv
# Resolv::Hosts is a hostname resolver that uses the system hosts file.
class Hosts
- begin
- raise LoadError unless /mswin|mingw|cygwin/ =~ RUBY_PLATFORM
- require 'win32/resolv'
- DefaultFileName = Win32::Resolv.get_hosts_path
- rescue LoadError
- 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.
@@ -188,23 +203,13 @@ class Resolv
unless @initialized
@name2addr = {}
@addr2name = {}
- open(@filename, 'rb') {|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!}
@@ -236,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
##
@@ -314,6 +317,8 @@ class Resolv
# 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.
@@ -403,13 +408,20 @@ class Resolv
# be a Resolv::IPv4 or Resolv::IPv6
def each_address(name)
- each_resource(name, Resource::IN::A) {|resource| yield resource.address}
if use_ipv6?
each_resource(name, Resource::IN::AAAA) {|resource| yield resource.address}
end
+ each_resource(name, Resource::IN::A) {|resource| yield resource.address}
end
def use_ipv6? # :nodoc:
+ @config.lazy_initialize unless @config.instance_variable_get(:@initialized)
+
+ use_ipv6 = @config.use_ipv6?
+ unless use_ipv6.nil?
+ return use_ipv6
+ end
+
begin
list = Socket.ip_address_list
rescue NotImplementedError
@@ -453,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
@@ -473,13 +487,18 @@ class Resolv
# * Resolv::DNS::Resource::IN::A
# * Resolv::DNS::Resource::IN::AAAA
# * Resolv::DNS::Resource::IN::ANY
+ # * Resolv::DNS::Resource::IN::CAA
# * Resolv::DNS::Resource::IN::CNAME
# * Resolv::DNS::Resource::IN::HINFO
+ # * Resolv::DNS::Resource::IN::HTTPS
+ # * Resolv::DNS::Resource::IN::LOC
# * Resolv::DNS::Resource::IN::MINFO
# * Resolv::DNS::Resource::IN::MX
# * Resolv::DNS::Resource::IN::NS
# * Resolv::DNS::Resource::IN::PTR
# * Resolv::DNS::Resource::IN::SOA
+ # * Resolv::DNS::Resource::IN::SRV
+ # * Resolv::DNS::Resource::IN::SVCB
# * Resolv::DNS::Resource::IN::TXT
# * Resolv::DNS::Resource::IN::WKS
#
@@ -511,32 +530,44 @@ class Resolv
}
end
+ # :stopdoc:
+
def fetch_resource(name, typeclass)
lazy_initialize
- requester = make_udp_requester
+ truncated = {}
+ requesters = {}
+ udp_requester = begin
+ make_udp_requester
+ rescue Errno::EACCES
+ # fall back to TCP
+ end
senders = {}
+
begin
- @config.resolv(name) {|candidate, tout, nameserver, port|
+ @config.resolv(name) do |candidate, tout, nameserver, port|
msg = Message.new
msg.rd = 1
msg.add_question(candidate, typeclass)
- unless sender = senders[[candidate, nameserver, port]]
+
+ requester = requesters.fetch([nameserver, port]) do
+ if !truncated[candidate] && udp_requester
+ udp_requester
+ else
+ requesters[[nameserver, port]] = make_tcp_requester(nameserver, port)
+ end
+ end
+
+ unless sender = senders[[candidate, requester, nameserver, port]]
sender = requester.sender(msg, candidate, nameserver, port)
next if !sender
- senders[[candidate, nameserver, port]] = sender
+ senders[[candidate, requester, nameserver, port]] = sender
end
reply, reply_name = requester.request(sender, tout)
case reply.rcode
when RCode::NoError
if reply.tc == 1 and not Requester::TCP === requester
- requester.close
# Retry via TCP:
- requester = make_tcp_requester(nameserver, port)
- senders = {}
- # This will use TCP for all remaining candidates (assuming the
- # current candidate does not already respond successfully via
- # TCP). This makes sense because we already know the full
- # response will not fit in an untruncated UDP packet.
+ truncated[candidate] = true
redo
else
yield(reply, reply_name)
@@ -547,9 +578,10 @@ class Resolv
else
raise Config::OtherResolvError.new(reply_name.to_s)
end
- }
+ end
ensure
- requester.close
+ udp_requester&.close
+ requesters.each_value { |requester| requester&.close }
end
end
@@ -564,6 +596,11 @@ class Resolv
def make_tcp_requester(host, port) # :nodoc:
return Requester::TCP.new(host, port)
+ rescue Errno::ECONNREFUSED
+ # Treat a refused TCP connection attempt to a nameserver like a timeout,
+ # as Resolv::DNS::Config#resolv considers ResolvTimeout exceptions as a
+ # hint to try the next nameserver:
+ raise ResolvTimeout
end
def extract_resources(msg, name, typeclass) # :nodoc:
@@ -597,30 +634,14 @@ 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 = {} # :nodoc:
RequestIDMutex = Thread::Mutex.new # :nodoc:
@@ -629,7 +650,7 @@ class Resolv
RequestIDMutex.synchronize {
h = (RequestID[[host, port]] ||= {})
begin
- id = rangerand(0x0000..0xffff)
+ id = random(0x0000..0xffff)
end while h[id]
h[id] = true
}
@@ -648,9 +669,21 @@ class Resolv
}
end
- def self.bind_random_port(udpsock, bind_host="0.0.0.0") # :nodoc:
- begin
- port = rangerand(1024..65535)
+ 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)
@@ -693,7 +726,8 @@ class Resolv
begin
reply, from = recv_reply(select_result[0])
rescue Errno::ECONNREFUSED, # GNU/Linux, FreeBSD
- Errno::ECONNRESET # Windows
+ Errno::ECONNRESET, # Windows
+ EOFError
# No name server running on the server?
# Don't wait anymore.
raise ResolvTimeout
@@ -703,13 +737,13 @@ class Resolv
rescue DecodeError
next # broken DNS message ignored
end
- if s = sender_for(from, msg)
+ 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)
@@ -734,35 +768,48 @@ class Resolv
def initialize(*nameserver_port)
super()
@nameserver_port = nameserver_port
- @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
- next # The kernel doesn't support the address family.
- end
- sock.do_not_reverse_lookup = true
- DNS.bind_random_port(sock, bind_host)
- @socks << sock
- @socks_hash[bind_host] = sock
+ @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(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]
@@ -774,9 +821,14 @@ class Resolv
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
@@ -800,20 +852,32 @@ class Resolv
super()
@host = host
@port = port
- is_ipv6 = host.index(':')
- sock = UDPSocket.new(is_ipv6 ? Socket::AF_INET6 : Socket::AF_INET)
- @socks = [sock]
- sock.do_not_reverse_lookup = true
- DNS.bind_random_port(sock, is_ipv6 ? "::" : "0.0.0.0")
- sock.connect(host, port)
+ @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, host=@host, port=@port)
+ lazy_initialize
unless host == @host && port == @port
raise RequestError.new("host/port don't match: #{host}:#{port}")
end
@@ -824,10 +888,15 @@ class Resolv
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:
@@ -841,6 +910,7 @@ class Resolv
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')
@@ -850,6 +920,7 @@ class Resolv
end
def sender_for(addr, msg)
+ lazy_initialize
@senders[msg.id]
end
end
@@ -865,8 +936,11 @@ class Resolv
end
def recv_reply(readable_socks)
- len = readable_socks[0].read(2).unpack('n')[0]
+ len_data = readable_socks[0].read(2)
+ raise EOFError if len_data.nil? || len_data.bytesize != 2
+ len = len_data.unpack('n')[0]
reply = @socks[0].read(len)
+ raise EOFError if reply.nil? || reply.bytesize != len
return reply, nil
end
@@ -928,21 +1002,20 @@ class Resolv
nameserver = []
search = nil
ndots = 1
- open(filename, 'rb') {|f|
+ File.open(filename, 'rb') {|f|
f.each {|line|
line.sub!(/[#;].*/, '')
keyword, *args = line.split(/\s+/)
- args.each(&: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
@@ -953,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_port = []
+ @use_ipv6 = nil
@search = nil
@ndots = 1
case @config_info
@@ -999,8 +1072,12 @@ class Resolv
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]
if @nameserver_port.empty?
@nameserver_port << ['0.0.0.0', Port]
@@ -1054,6 +1131,10 @@ class Resolv
@nameserver_port
end
+ def use_ipv6?
+ @use_ipv6
+ end
+
def generate_candidates(name)
candidates = nil
name = Name.create(name)
@@ -1087,6 +1168,7 @@ class Resolv
def resolv(name)
candidates = generate_candidates(name)
timeouts = @timeouts || generate_timeouts
+ timeout_error = false
begin
candidates.each {|candidate|
begin
@@ -1098,11 +1180,13 @@ class Resolv
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
@@ -1458,14 +1542,14 @@ 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
@@ -1489,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)
@@ -1587,6 +1673,14 @@ 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
@@ -1595,6 +1689,7 @@ class Resolv
prev_index = @index
save_index = nil
d = []
+ size = -1
while true
raise DecodeError.new("limit exceeded") if @limit <= @index
case @data.getbyte(@index)
@@ -1615,7 +1710,10 @@ class Resolv
end
@index = idx
else
- d << self.get_label
+ l = self.get_label
+ d << l
+ size += 1 + l.string.bytesize
+ raise DecodeError.new("name label data exceed 255 octets") if size > 255
end
end
end
@@ -1648,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
@@ -1670,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
@@ -1708,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
@@ -2074,7 +2552,6 @@ class Resolv
attr_reader :altitude
-
def encode_rdata(msg) # :nodoc:
msg.put_bytes(@version)
msg.put_bytes(@ssize.scalar)
@@ -2112,8 +2589,70 @@ class Resolv
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, LOC, ANY
+ NS, CNAME, SOA, PTR, HINFO, MINFO, MX, TXT, LOC, ANY, CAA
]
##
@@ -2299,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:
@@ -2310,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
@@ -2319,15 +2936,21 @@ class Resolv
class IPv4
- ##
- # Regular expression IPv4 addresses must match.
-
Regex256 = /0
|1(?:[0-9][0-9]?)?
|2(?:[0-4][0-9]?|5[0-5]?|[6-9])?
- |[3-9][0-9]?/x
+ |[3-9][0-9]?/x # :nodoc:
+
+ ##
+ # Regular expression IPv4 addresses must match.
Regex = /\A(#{Regex256})\.(#{Regex256})\.(#{Regex256})\.(#{Regex256})\z/
+ ##
+ # Creates a new IPv4 address from +arg+ which may be:
+ #
+ # IPv4:: returns +arg+.
+ # String:: +arg+ must match the IPv4::Regex constant
+
def self.create(arg)
case arg
when IPv4
@@ -2431,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:
@@ -2504,11 +3152,7 @@ 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:
@@ -2603,7 +3247,7 @@ class Resolv
def each_address(name)
name = Resolv::DNS::Name.create(name)
- return unless name.to_a.last.to_s == 'local'
+ return unless name[-1].to_s == 'local'
super(name)
end
@@ -2615,14 +3259,16 @@ class Resolv
end
- module LOC
+ module LOC # :nodoc:
##
# A Resolv::LOC::Size
class Size
- Regex = /^(\d+\.*\d*)[m]$/
+ # Regular expression LOC size must match.
+
+ Regex = /\A0*(\d{1,8}(?:\.\d+)?)m\z/
##
# Creates a new LOC::Size from +arg+ which may be:
@@ -2635,18 +3281,20 @@ class Resolv
when Size
return arg
when String
- scalar = ''
- if Regex =~ arg
- scalar = [(($1.to_f*(1e2)).to_i.to_s[0].to_i*(2**4)+(($1.to_f*(1e2)).to_i.to_s.length-1))].pack("C")
- else
+ unless Regex =~ arg
raise ArgumentError.new("not a properly formed Size string: " + arg)
end
- return Size.new(scalar)
+ unless (0.0...1e8) === (scalar = $1.to_f)
+ raise ArgumentError.new("out of range as Size: #{arg}")
+ end
+ str = (scalar * 100).to_i.to_s
+ return new([(str[0].to_i << 4) + (str.bytesize-1)].pack("C"))
else
raise ArgumentError.new("cannot interpret as Size: #{arg.inspect}")
end
end
+ # Internal use; use self.create.
def initialize(scalar)
@scalar = scalar
end
@@ -2657,8 +3305,8 @@ class Resolv
attr_reader :scalar
def to_s # :nodoc:
- s = @scalar.unpack("H2").join.to_s
- return ((s[0].to_i)*(10**(s[1].to_i-2))).to_s << "m"
+ s, = @scalar.unpack("C")
+ return "#{(s >> 4) * (10.0 ** ((s & 0xf) - 2))}m"
end
def inspect # :nodoc:
@@ -2684,7 +3332,12 @@ class Resolv
class Coord
- Regex = /^(\d+)\s(\d+)\s(\d+\.\d+)\s([NESW])$/
+ # Regular expression LOC Coord must match.
+
+ Regex = /\A0*(\d{1,3})\s([0-5]?\d)\s([0-5]?\d(?:\.\d+)?)\s([NESW])\z/
+
+ # Bias for the equator/prime meridian, in thousandths of a second of arc.
+ Bias = 1 << 31
##
# Creates a new LOC::Coord from +arg+ which may be:
@@ -2697,27 +3350,30 @@ class Resolv
when Coord
return arg
when String
- coordinates = ''
- if Regex =~ arg && $1.to_f < 180
- m = $~
- hemi = (m[4][/[NE]/]) || (m[4][/[SW]/]) ? 1 : -1
- coordinates = [ ((m[1].to_i*(36e5)) + (m[2].to_i*(6e4)) +
- (m[3].to_f*(1e3))) * hemi+(2**31) ].pack("N")
- orientation = m[4][/[NS]/] ? 'lat' : 'lon'
- else
+ unless m = Regex.match(arg)
raise ArgumentError.new("not a properly formed Coord string: " + arg)
end
- return Coord.new(coordinates,orientation)
+
+ arc = (m[1].to_i * 3_600_000) + (m[2].to_i * 60_000) + (m[3].to_f * 1_000).to_i
+ dir = m[4]
+ lat = dir[/[NS]/]
+ unless arc <= (lat ? 324_000_000 : 648_000_000) # (lat ? 90 : 180) * 3_600_000
+ raise ArgumentError.new("out of range as Coord: #{arg}")
+ end
+
+ hemi = dir[/[NE]/] ? 1 : -1
+ return new([arc * hemi + Bias].pack("N"), lat ? "lat" : "lon")
else
raise ArgumentError.new("cannot interpret as Coord: #{arg.inspect}")
end
end
+ # Internal use; use self.create.
def initialize(coordinates,orientation)
- unless coordinates.kind_of?(String)
+ unless coordinates.kind_of?(String) and coordinates.bytesize == 4
raise ArgumentError.new("Coord must be a 32bit unsigned integer in hex format: #{coordinates.inspect}")
end
- unless orientation.kind_of?(String) && orientation[/^lon$|^lat$/]
+ unless orientation == "lon" || orientation == "lat"
raise ArgumentError.new('Coord expects orientation to be a String argument of "lat" or "lon"')
end
@coordinates = coordinates
@@ -2734,22 +3390,17 @@ class Resolv
attr_reader :orientation
def to_s # :nodoc:
- c = @coordinates.unpack("N").join.to_i
- val = (c - (2**31)).abs
- fracsecs = (val % 1e3).to_i.to_s
- val = val / 1e3
- secs = (val % 60).to_i.to_s
- val = val / 60
- mins = (val % 60).to_i.to_s
- degs = (val / 60).to_i.to_s
- posi = (c >= 2**31)
- case posi
- when true
- hemi = @orientation[/^lat$/] ? "N" : "E"
+ c, = @coordinates.unpack("N")
+ val = (c -= Bias).abs
+ val, fracsecs = val.divmod(1000)
+ val, secs = val.divmod(60)
+ degs, mins = val.divmod(60)
+ hemi = if c.negative?
+ @orientation == "lon" ? "W" : "S"
else
- hemi = @orientation[/^lon$/] ? "W" : "S"
+ @orientation == "lat" ? "N" : "E"
end
- return degs << " " << mins << " " << secs << "." << fracsecs << " " << hemi
+ format("%d %02d %02d.%03d %s", degs, mins, secs, fracsecs, hemi)
end
def inspect # :nodoc:
@@ -2775,7 +3426,12 @@ class Resolv
class Alt
- Regex = /^([+-]*\d+\.*\d*)[m]$/
+ # Regular expression LOC Alt must match.
+
+ Regex = /\A([+-]?0*\d{1,8}(?:\.\d+)?)m\z/
+
+ # Bias to a base of 100,000m below the WGS 84 reference spheroid.
+ Bias = 100_000_00
##
# Creates a new LOC::Alt from +arg+ which may be:
@@ -2788,18 +3444,20 @@ class Resolv
when Alt
return arg
when String
- altitude = ''
- if Regex =~ arg
- altitude = [($1.to_f*(1e2))+(1e7)].pack("N")
- else
+ unless Regex =~ arg
raise ArgumentError.new("not a properly formed Alt string: " + arg)
end
- return Alt.new(altitude)
+ altitude = ($1.to_f * 100).to_i + Bias
+ unless (0...0x1_0000_0000) === altitude
+ raise ArgumentError.new("out of raise as Alt: #{arg}")
+ end
+ return new([altitude].pack("N"))
else
raise ArgumentError.new("cannot interpret as Alt: #{arg.inspect}")
end
end
+ # Internal use; use self.create.
def initialize(altitude)
@altitude = altitude
end
@@ -2810,8 +3468,8 @@ class Resolv
attr_reader :altitude
def to_s # :nodoc:
- a = @altitude.unpack("N").join.to_i
- return ((a.to_f/1e2)-1e5).to_s + "m"
+ a, = @altitude.unpack("N")
+ return "#{(a - Bias).fdiv(100)}m"
end
def inspect # :nodoc:
@@ -2853,4 +3511,3 @@ class Resolv
AddressRegex = /(?:#{IPv4::Regex})|(?:#{IPv6::Regex})/
end
-