initial reimplementation of the system resolver, now using getaddrinfo

the ruby `resolver` library does everthing in ruby, and sequentially
(first ipv4 then ipv6 resolution). we already have native for that, and
getaddrinfo should be considered the ideal way to use DNS (potentially
in the future, it becomes the default resolver).
This commit is contained in:
HoneyryderChuck 2021-12-17 17:36:39 +00:00
parent 2940323412
commit 554957f6ca
7 changed files with 164 additions and 25 deletions

View File

@ -18,11 +18,6 @@ module HTTPX
use_get: false, use_get: false,
}.freeze }.freeze
FAMILY_TYPES = {
Resolv::DNS::Resource::IN::AAAA => "AAAA",
Resolv::DNS::Resource::IN::A => "A",
}.freeze
def_delegators :@resolver_connection, :state, :connecting?, :to_io, :call, :close def_delegators :@resolver_connection, :state, :connecting?, :to_io, :call, :close
def initialize(_, options) def initialize(_, options)
@ -64,7 +59,7 @@ module HTTPX
@building_connection = true @building_connection = true
connection = @options.connection_class.new("ssl", @uri, @options.merge(ssl: { alpn_protocols: %w[h2] })) connection = @options.connection_class.new("ssl", @uri, @options.merge(ssl: { alpn_protocols: %w[h2] }))
@pool.init_connection(connection, @options) @pool.init_connection(connection, @options)
emit_addresses(connection, @uri_addresses) emit_addresses(connection, @family, @uri_addresses)
@building_connection = false @building_connection = false
connection connection
end end
@ -154,7 +149,7 @@ module HTTPX
@connections.delete(connection) @connections.delete(connection)
Resolver.cached_lookup_set(hostname, @family, addresses) if @resolver_options[:cache] Resolver.cached_lookup_set(hostname, @family, addresses) if @resolver_options[:cache]
emit_addresses(connection, addresses.map { |addr| addr["data"] }) emit_addresses(connection, @family, addresses.map { |addr| addr["data"] })
end end
end end
return if @connections.empty? return if @connections.empty?

View File

@ -6,6 +6,7 @@ require "resolv"
module HTTPX module HTTPX
class Resolver::Multi class Resolver::Multi
include Callbacks include Callbacks
using ArrayExtensions
attr_reader :resolvers attr_reader :resolvers

View File

@ -237,7 +237,7 @@ module HTTPX
@timeouts.delete(connection.origin.host) @timeouts.delete(connection.origin.host)
@connections.delete(connection) @connections.delete(connection)
Resolver.cached_lookup_set(connection.origin.host, @family, addresses) if @resolver_options[:cache] Resolver.cached_lookup_set(connection.origin.host, @family, addresses) if @resolver_options[:cache]
emit_addresses(connection, addresses.map { |addr| addr["data"] }) emit_addresses(connection, @family, addresses.map { |addr| addr["data"] })
end end
end end
return emit(:close) if @connections.empty? return emit(:close) if @connections.empty?

View File

@ -13,6 +13,11 @@ module HTTPX
Socket::AF_INET => Resolv::DNS::Resource::IN::A, Socket::AF_INET => Resolv::DNS::Resource::IN::A,
}.freeze }.freeze
FAMILY_TYPES = {
Resolv::DNS::Resource::IN::AAAA => "AAAA",
Resolv::DNS::Resource::IN::A => "A",
}.freeze
CHECK_IF_IP = ->(name) do CHECK_IF_IP = ->(name) do
begin begin
IPAddr.new(name) IPAddr.new(name)
@ -50,14 +55,14 @@ module HTTPX
private private
def emit_addresses(connection, addresses) def emit_addresses(connection, family, addresses)
addresses.map! do |address| addresses.map! do |address|
address.is_a?(IPAddr) ? address : IPAddr.new(address.to_s) address.is_a?(IPAddr) ? address : IPAddr.new(address.to_s)
end end
log { "resolver: answer #{connection.origin.host}: #{addresses.inspect}" } log { "resolver: answer #{connection.origin.host}: #{addresses.inspect}" }
if !connection.io && if !connection.io &&
connection.options.ip_families.size > 1 && connection.options.ip_families.size > 1 &&
addresses.first.ipv4? && family == Socket::AF_INET &&
addresses.first.to_s != connection.origin.host.to_s addresses.first.to_s != connection.origin.host.to_s
log { "resolver: A response, applying resolution delay..." } log { "resolver: A response, applying resolution delay..." }
@pool.after(0.05) do @pool.after(0.05) do
@ -77,11 +82,12 @@ module HTTPX
system_resolve(hostname) system_resolve(hostname)
return unless addresses return unless addresses
emit_addresses(connection, addresses) emit_addresses(connection, nil, addresses)
end end
def ip_resolve(hostname) def ip_resolve(hostname)
[hostname] if CHECK_IF_IP[hostname] [IPAddr.new(hostname)]
rescue ArgumentError
end end
def system_resolve(hostname) def system_resolve(hostname)

View File

@ -5,11 +5,17 @@ require "resolv"
module HTTPX module HTTPX
class Resolver::System < Resolver::Resolver class Resolver::System < Resolver::Resolver
using URIExtensions
extend Forwardable
RESOLV_ERRORS = [Resolv::ResolvError, RESOLV_ERRORS = [Resolv::ResolvError,
Resolv::DNS::Requester::RequestError, Resolv::DNS::Requester::RequestError,
Resolv::DNS::EncodeError, Resolv::DNS::EncodeError,
Resolv::DNS::DecodeError].freeze Resolv::DNS::DecodeError].freeze
DONE = 1
ERROR = 2
class << self class << self
def multi? def multi?
false false
@ -18,14 +24,21 @@ module HTTPX
attr_reader :state attr_reader :state
def_delegator :@connections, :empty?
def initialize(options) def initialize(options)
super(nil, options) super(nil, options)
@resolver_options = @options.resolver_options @resolver_options = @options.resolver_options
resolv_options = @resolver_options.dup resolv_options = @resolver_options.dup
timeouts = resolv_options.delete(:timeouts) timeouts = resolv_options.delete(:timeouts) || Resolver::RESOLVE_TIMEOUT
@_timeouts = Array(timeouts)
@timeouts = Hash.new { |tims, host| tims[host] = @_timeouts.dup }
resolv_options.delete(:cache) resolv_options.delete(:cache)
@resolver = resolv_options.empty? ? Resolv::DNS.new : Resolv::DNS.new(resolv_options) @connections = []
@resolver.timeouts = timeouts || Resolver::RESOLVE_TIMEOUT @queries = []
@ips = []
@pipe_mutex = Thread::Mutex.new
@state = :idle
end end
def resolvers def resolvers
@ -38,18 +51,142 @@ module HTTPX
EMPTY EMPTY
end end
def close
transition(:closed)
end
def closed?
@state == :closed
end
def to_io
@pipe_read.to_io
end
def call
case @state
when :open
consume
end
nil
end
def interests
return if @queries.empty?
:r
end
def timeout
return unless @queries.empty?
_, connection = @queries.first
@timeouts[connection.origin.host].first
end
def <<(connection) def <<(connection)
return if early_resolve(connection)
@connections << connection
resolve
end
private
def transition(nextstate)
case nextstate
when :idle
@timeouts.clear
when :open
return unless @state == :idle
@pipe_read, @pipe_write = ::IO.pipe
when :closed
return unless @state == :open
@pipe_write.close
@pipe_read.close
end
@state = nextstate
end
def consume
return if @connections.empty?
while @pipe_read.ready? && (event = @pipe_read.getbyte)
case event
when DONE
*pair, addrs = @pipe_mutex.synchronize { @ips.pop }
@queries.delete(pair)
family, connection = pair
emit_addresses(connection, family, addrs)
when ERROR
*pair, error = @pipe_mutex.synchronize { @ips.pop }
@queries.delete(pair)
family, connection = pair
emit_resolve_error(connection, connection.origin.host, error)
end
@connections.delete(connection) if @queries.empty?
end
return emit(:close, self) if @connections.empty?
resolve
end
def resolve(connection = @connections.first)
raise Error, "no URI to resolve" unless connection
return unless @queries.empty?
hostname = connection.origin.host hostname = connection.origin.host
addresses = connection.addresses || scheme = connection.origin.scheme
ip_resolve(hostname) || log { "resolver: resolve IDN #{connection.origin.non_ascii_hostname} as #{hostname}" } if connection.origin.non_ascii_hostname
system_resolve(hostname) ||
@resolver.getaddresses(hostname)
throw(:resolve_error, resolve_error(hostname)) if addresses.empty? transition(:open)
emit_addresses(connection, addresses) connection.options.ip_families.each do |family|
rescue Errno::EHOSTUNREACH, *RESOLV_ERRORS => e @queries << [family, connection]
emit_resolve_error(connection, hostname, e) end
async_resolve(connection, hostname, scheme)
consume
end
def async_resolve(connection, hostname, scheme)
families = connection.options.ip_families
log { "resolver: query for #{hostname}" }
Thread.start do
Thread.current.report_on_exception = false
begin
addrs = __addrinfo_resolve(hostname, scheme)
addrs = addrs.sort_by(&:afamily).group_by(&:afamily)
families.each do |family|
addresses = addrs[family]
next unless addresses
addresses.map!(&:ip_address)
addresses.uniq!
@pipe_mutex.synchronize do
@ips.unshift([family, connection, addresses])
@pipe_write.putc(DONE) unless @pipe_write.closed?
end
end
rescue StandardError => e
@pipe_mutex.synchronize do
families.each do |family|
@ips.unshift([family, connection, e])
@pipe_write.putc(ERROR) unless @pipe_write.closed?
end
end
end
end
end
def __addrinfo_resolve(host, scheme)
Addrinfo.getaddrinfo(host, scheme, Socket::AF_UNSPEC, Socket::SOCK_STREAM)
end end
end end
end end

View File

@ -25,7 +25,7 @@ module HTTPX
def initialize: (ip_family? family, options options) -> void def initialize: (ip_family? family, options options) -> void
def emit_addresses: (Connection, Array[ipaddr | Resolv::DNS::ip_address]) -> void def emit_addresses: (Connection, ip_family? family, Array[ipaddr | Resolv::DNS::ip_address]) -> void
def early_resolve: (Connection, ?hostname: String) -> void def early_resolve: (Connection, ?hostname: String) -> void

View File

@ -1,6 +1,6 @@
module HTTPX module HTTPX
class Selector class Selector
type selectable = Connection | Resolver::Native type selectable = Connection | Resolver::Native | Resolver::System
READABLE: Array[Symbol] READABLE: Array[Symbol]
WRITABLE: Array[Symbol] WRITABLE: Array[Symbol]