mirror of
https://github.com/HoneyryderChuck/httpx.git
synced 2025-10-04 00:00:37 -04:00
applying a resolver manager to hold the different family type resolvers for the pool. This allows to have multiple resolvers per type, i.e. IPv6 and IPv4
This commit is contained in:
parent
71920157f4
commit
06b162b6ea
@ -1,5 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "socket"
|
||||
|
||||
module HTTPX
|
||||
class Options
|
||||
WINDOW_SIZE = 1 << 14 # 16K
|
||||
@ -9,6 +11,17 @@ module HTTPX
|
||||
KEEP_ALIVE_TIMEOUT = 20
|
||||
SETTINGS_TIMEOUT = 10
|
||||
|
||||
# https://github.com/ruby/resolv/blob/095f1c003f6073730500f02acbdbc55f83d70987/lib/resolv.rb#L408
|
||||
ip_address_families = begin
|
||||
list = Socket.ip_address_list
|
||||
if list.any? { |a| a.ipv6? && !a.ipv6_loopback? && !a.ipv6_linklocal? }
|
||||
# [Socket::AF_INET6, Socket::AF_INET]
|
||||
end
|
||||
[Socket::AF_INET]
|
||||
rescue NotImplementedError
|
||||
[Socket::AF_INET]
|
||||
end
|
||||
|
||||
DEFAULT_OPTIONS = {
|
||||
:debug => ENV.key?("HTTPX_DEBUG") ? $stderr : nil,
|
||||
:debug_level => (ENV["HTTPX_DEBUG"] || 1).to_i,
|
||||
@ -37,6 +50,7 @@ module HTTPX
|
||||
:persistent => false,
|
||||
:resolver_class => (ENV["HTTPX_RESOLVER"] || :native).to_sym,
|
||||
:resolver_options => { cache: true },
|
||||
:ip_families => ip_address_families,
|
||||
}.freeze
|
||||
|
||||
begin
|
||||
@ -172,6 +186,10 @@ module HTTPX
|
||||
Array(value)
|
||||
end
|
||||
|
||||
def option_ip_families(value)
|
||||
Array(value)
|
||||
end
|
||||
|
||||
%i[
|
||||
params form json body ssl http2_settings
|
||||
request_class response_class headers_class request_body_class
|
||||
|
@ -1,9 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "resolv"
|
||||
require "ipaddr"
|
||||
require "forwardable"
|
||||
|
||||
module HTTPX
|
||||
class HTTPProxyError < Error; end
|
||||
|
||||
@ -252,7 +248,7 @@ module HTTPX
|
||||
case nextstate
|
||||
when :closing
|
||||
# this is a hack so that we can use the super method
|
||||
# and it'll thing that the current state is open
|
||||
# and it'll think that the current state is open
|
||||
@state = :open if @state == :connecting
|
||||
end
|
||||
super
|
||||
|
@ -106,11 +106,12 @@ module HTTPX
|
||||
return
|
||||
end
|
||||
|
||||
resolver = find_resolver_for(connection)
|
||||
resolver << connection
|
||||
return if resolver.empty?
|
||||
find_resolver_for(connection) do |resolver|
|
||||
resolver << connection
|
||||
return if resolver.empty?
|
||||
|
||||
select_connection(resolver)
|
||||
select_connection(resolver)
|
||||
end
|
||||
end
|
||||
|
||||
def on_resolver_connection(connection)
|
||||
@ -137,7 +138,7 @@ module HTTPX
|
||||
|
||||
def on_resolver_close(resolver)
|
||||
resolver_type = resolver.class
|
||||
return unless @resolvers[resolver_type] == resolver
|
||||
return if resolver.closed?
|
||||
|
||||
@resolvers.delete(resolver_type)
|
||||
|
||||
@ -172,12 +173,10 @@ module HTTPX
|
||||
end
|
||||
|
||||
def coalesce_connections(conn1, conn2)
|
||||
if conn1.coalescable?(conn2)
|
||||
conn1.merge(conn2)
|
||||
@connections.delete(conn2)
|
||||
else
|
||||
register_connection(conn2)
|
||||
end
|
||||
return register_connection(conn2) unless conn1.coalescable?(conn2)
|
||||
|
||||
conn1.merge(conn2)
|
||||
@connections.delete(conn2)
|
||||
end
|
||||
|
||||
def next_timeout
|
||||
@ -194,13 +193,19 @@ module HTTPX
|
||||
resolver_type = Resolver.registry(resolver_type) if resolver_type.is_a?(Symbol)
|
||||
|
||||
@resolvers[resolver_type] ||= begin
|
||||
resolver = resolver_type.new(connection_options)
|
||||
resolver.pool = self if resolver.respond_to?(:pool=)
|
||||
resolver.on(:resolve, &method(:on_resolver_connection))
|
||||
resolver.on(:error, &method(:on_resolver_error))
|
||||
resolver.on(:close) { on_resolver_close(resolver) }
|
||||
resolver
|
||||
resolver_manager = Resolver::Multi.new(resolver_type, connection_options)
|
||||
resolver_manager.on(:resolve, &method(:on_resolver_connection))
|
||||
resolver_manager.on(:error, &method(:on_resolver_error))
|
||||
resolver_manager.on(:close, &method(:on_resolver_close))
|
||||
resolver_manager
|
||||
end
|
||||
|
||||
manager = @resolvers[resolver_type]
|
||||
manager.resolvers.each do |resolver|
|
||||
resolver.pool = self if resolver.respond_to?(:pool=)
|
||||
yield resolver
|
||||
end
|
||||
manager
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -12,6 +12,7 @@ module HTTPX
|
||||
require "httpx/resolver/system"
|
||||
require "httpx/resolver/native"
|
||||
require "httpx/resolver/https"
|
||||
require "httpx/resolver/multi"
|
||||
|
||||
register :system, System
|
||||
register :native, Native
|
||||
|
@ -16,17 +16,20 @@ module HTTPX
|
||||
DEFAULTS = {
|
||||
uri: NAMESERVER,
|
||||
use_get: false,
|
||||
record_types: RECORD_TYPES.keys,
|
||||
}.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
|
||||
|
||||
attr_writer :pool
|
||||
|
||||
def initialize(options)
|
||||
def initialize(_, options)
|
||||
super
|
||||
@resolver_options = DEFAULTS.merge(@options.resolver_options)
|
||||
@_record_types = Hash.new { |types, host| types[host] = @resolver_options[:record_types].dup }
|
||||
@queries = {}
|
||||
@requests = {}
|
||||
@connections = []
|
||||
@ -81,17 +84,16 @@ module HTTPX
|
||||
hostname = connection.origin.host
|
||||
log { "resolver: resolve IDN #{connection.origin.non_ascii_hostname} as #{hostname}" } if connection.origin.non_ascii_hostname
|
||||
end
|
||||
type = @_record_types[hostname].first || "A"
|
||||
log { "resolver: query #{type} for #{hostname}" }
|
||||
log { "resolver: query #{FAMILY_TYPES[@family]} for #{hostname}" }
|
||||
begin
|
||||
request = build_request(hostname, type)
|
||||
request = build_request(hostname)
|
||||
request.on(:response, &method(:on_response).curry(2)[request])
|
||||
request.on(:promise, &method(:on_promise))
|
||||
@requests[request] = connection
|
||||
resolver_connection.send(request)
|
||||
@queries[hostname] = connection
|
||||
@connections << connection
|
||||
rescue Resolv::DNS::EncodeError, JSON::JSONError => e
|
||||
rescue ResolveError, Resolv::DNS::EncodeError, JSON::JSONError => e
|
||||
emit_resolve_error(connection, hostname, e)
|
||||
end
|
||||
end
|
||||
@ -119,21 +121,15 @@ module HTTPX
|
||||
answers = decode_response_body(response)
|
||||
rescue Resolv::DNS::DecodeError, JSON::JSONError => e
|
||||
host, connection = @queries.first
|
||||
if @_record_types[host].empty?
|
||||
@queries.delete(host)
|
||||
emit_resolve_error(connection, host, e)
|
||||
return
|
||||
end
|
||||
@queries.delete(host)
|
||||
emit_resolve_error(connection, host, e)
|
||||
return
|
||||
end
|
||||
if answers.nil? || answers.empty?
|
||||
host, connection = @queries.first
|
||||
@_record_types[host].shift
|
||||
if @_record_types[host].empty?
|
||||
@queries.delete(host)
|
||||
@_record_types.delete(host)
|
||||
emit_resolve_error(connection, host)
|
||||
return
|
||||
end
|
||||
@queries.delete(host)
|
||||
emit_resolve_error(connection, host)
|
||||
return
|
||||
else
|
||||
answers = answers.group_by { |answer| answer["name"] }
|
||||
answers.each do |hostname, addresses|
|
||||
@ -168,14 +164,14 @@ module HTTPX
|
||||
resolve
|
||||
end
|
||||
|
||||
def build_request(hostname, type)
|
||||
def build_request(hostname)
|
||||
uri = @uri.dup
|
||||
rklass = @options.request_class
|
||||
payload = Resolver.encode_dns_query(hostname, type: RECORD_TYPES[type])
|
||||
payload = Resolver.encode_dns_query(hostname, type: @record_type)
|
||||
|
||||
if @resolver_options[:use_get]
|
||||
params = URI.decode_www_form(uri.query.to_s)
|
||||
params << ["type", type]
|
||||
params << ["type", FAMILY_TYPES[@record_type]]
|
||||
params << ["dns", Base64.urlsafe_encode64(payload, padding: false)]
|
||||
uri.query = URI.encode_www_form(params)
|
||||
request = rklass.new("GET", uri, @options)
|
||||
|
57
lib/httpx/resolver/multi.rb
Normal file
57
lib/httpx/resolver/multi.rb
Normal file
@ -0,0 +1,57 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "forwardable"
|
||||
require "resolv"
|
||||
|
||||
module HTTPX
|
||||
class Resolver::Multi
|
||||
include Callbacks
|
||||
|
||||
attr_reader :resolvers
|
||||
|
||||
def initialize(resolver_type, options)
|
||||
@options = options
|
||||
|
||||
@resolvers = options.ip_families.map do |ip_family|
|
||||
resolver = resolver_type.new(ip_family, options)
|
||||
resolver.on(:resolve, &method(:on_resolver_connection))
|
||||
resolver.on(:error, &method(:on_resolver_error))
|
||||
resolver.on(:close) { on_resolver_close(resolver) }
|
||||
resolver
|
||||
end
|
||||
|
||||
@errors = Hash.new { |hs, k| hs[k] = [] }
|
||||
end
|
||||
|
||||
def closed?
|
||||
@resolvers.all?(&:closed?)
|
||||
end
|
||||
|
||||
def timeout
|
||||
@resolvers.map(&:timeout).min
|
||||
end
|
||||
|
||||
def close
|
||||
@resolvers.each(&:close)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def on_resolver_connection(connection)
|
||||
emit(:resolve, connection)
|
||||
end
|
||||
|
||||
def on_resolver_error(connection, error)
|
||||
@errors[connection] << error
|
||||
|
||||
return unless @errors[connection].size >= @resolvers.size
|
||||
|
||||
errors = @errors.delete(connection)
|
||||
emit(:error, connection, errors.first)
|
||||
end
|
||||
|
||||
def on_resolver_close(resolver)
|
||||
emit(:close, resolver)
|
||||
end
|
||||
end
|
||||
end
|
@ -13,7 +13,6 @@ module HTTPX
|
||||
**Resolv::DNS::Config.default_config_hash,
|
||||
packet_size: 512,
|
||||
timeouts: Resolver::RESOLVE_TIMEOUT,
|
||||
record_types: RECORD_TYPES.keys,
|
||||
}.freeze
|
||||
else
|
||||
{
|
||||
@ -21,7 +20,6 @@ module HTTPX
|
||||
**Resolv::DNS::Config.default_config_hash,
|
||||
packet_size: 512,
|
||||
timeouts: Resolver::RESOLVE_TIMEOUT,
|
||||
record_types: RECORD_TYPES.keys,
|
||||
}.freeze
|
||||
end
|
||||
|
||||
@ -43,14 +41,13 @@ module HTTPX
|
||||
|
||||
attr_reader :state
|
||||
|
||||
def initialize(options)
|
||||
def initialize(_, options)
|
||||
super
|
||||
@ns_index = 0
|
||||
@resolver_options = DEFAULTS.merge(@options.resolver_options)
|
||||
@nameserver = @resolver_options[:nameserver]
|
||||
@_timeouts = Array(@resolver_options[:timeouts])
|
||||
@timeouts = Hash.new { |timeouts, host| timeouts[host] = @_timeouts.dup }
|
||||
@_record_types = Hash.new { |types, host| types[host] = @resolver_options[:record_types].dup }
|
||||
@connections = []
|
||||
@queries = {}
|
||||
@read_buffer = "".b
|
||||
@ -192,27 +189,21 @@ module HTTPX
|
||||
addresses = Resolver.decode_dns_answer(buffer)
|
||||
rescue Resolv::DNS::DecodeError => e
|
||||
hostname, connection = @queries.first
|
||||
if @_record_types[hostname].empty?
|
||||
@queries.delete(hostname)
|
||||
@timeouts.delete(hostname)
|
||||
@connections.delete(connection)
|
||||
ex = NativeResolveError.new(connection, hostname, e.message)
|
||||
ex.set_backtrace(e.backtrace)
|
||||
raise ex
|
||||
end
|
||||
@queries.delete(hostname)
|
||||
@timeouts.delete(hostname)
|
||||
@connections.delete(connection)
|
||||
ex = NativeResolveError.new(connection, hostname, e.message)
|
||||
ex.set_backtrace(e.backtrace)
|
||||
raise ex
|
||||
end
|
||||
|
||||
if addresses.nil? || addresses.empty?
|
||||
hostname, connection = @queries.first
|
||||
@_record_types[hostname].shift
|
||||
if @_record_types[hostname].empty?
|
||||
@queries.delete(hostname)
|
||||
@_record_types.delete(hostname)
|
||||
@timeouts.delete(hostname)
|
||||
@connections.delete(connection)
|
||||
@queries.delete(hostname)
|
||||
@timeouts.delete(hostname)
|
||||
@connections.delete(connection)
|
||||
|
||||
raise NativeResolveError.new(connection, hostname)
|
||||
end
|
||||
raise NativeResolveError.new(connection, hostname)
|
||||
else
|
||||
address = addresses.first
|
||||
name = address["name"]
|
||||
@ -265,10 +256,9 @@ module HTTPX
|
||||
log { "resolver: resolve IDN #{connection.origin.non_ascii_hostname} as #{hostname}" } if connection.origin.non_ascii_hostname
|
||||
end
|
||||
@queries[hostname] = connection
|
||||
type = @_record_types[hostname].first || "A"
|
||||
log { "resolver: query #{type} for #{hostname}" }
|
||||
log { "resolver: query #{@record_type.name.split("::").last} for #{hostname}" }
|
||||
begin
|
||||
@write_buffer << Resolver.encode_dns_query(hostname, type: RECORD_TYPES[type])
|
||||
@write_buffer << Resolver.encode_dns_query(hostname, type: @record_type)
|
||||
rescue Resolv::DNS::EncodeError => e
|
||||
emit_resolve_error(connection, hostname, e)
|
||||
end
|
||||
|
@ -9,8 +9,8 @@ module HTTPX
|
||||
include Loggable
|
||||
|
||||
RECORD_TYPES = {
|
||||
"A" => Resolv::DNS::Resource::IN::A,
|
||||
"AAAA" => Resolv::DNS::Resource::IN::AAAA,
|
||||
Socket::AF_INET6 => Resolv::DNS::Resource::IN::AAAA,
|
||||
Socket::AF_INET => Resolv::DNS::Resource::IN::A,
|
||||
}.freeze
|
||||
|
||||
CHECK_IF_IP = ->(name) do
|
||||
@ -22,7 +22,11 @@ module HTTPX
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(options)
|
||||
attr_reader :family
|
||||
|
||||
def initialize(family, options)
|
||||
@family = family
|
||||
@record_type = RECORD_TYPES[family]
|
||||
@options = Options.new(options)
|
||||
end
|
||||
|
||||
|
@ -12,7 +12,7 @@ module HTTPX
|
||||
|
||||
attr_reader :state
|
||||
|
||||
def initialize(options)
|
||||
def initialize(_, options)
|
||||
super
|
||||
@resolver_options = @options.resolver_options
|
||||
resolv_options = @resolver_options.dup
|
||||
@ -28,6 +28,7 @@ module HTTPX
|
||||
ip_resolve(hostname) ||
|
||||
system_resolve(hostname) ||
|
||||
@resolver.getaddresses(hostname)
|
||||
|
||||
throw(:resolve_error, resolve_error(hostname)) if addresses.empty?
|
||||
|
||||
emit_addresses(connection, addresses)
|
||||
|
@ -87,10 +87,6 @@ module HTTPX
|
||||
|
||||
attr_reader ssl: Hash[Symbol, untyped]
|
||||
|
||||
# request_class response_class headers_class request_body_class
|
||||
# response_body_class connection_class
|
||||
# resolver_class resolver_options
|
||||
|
||||
# io
|
||||
type io_option = _ToIO | Hash[String, _ToIO]
|
||||
attr_reader io: io_option?
|
||||
@ -110,6 +106,9 @@ module HTTPX
|
||||
# resolver_options
|
||||
attr_reader resolver_options: Hash[Symbol, untyped]
|
||||
|
||||
# ip_families
|
||||
attr_reader ip_families: Array[Integer]
|
||||
|
||||
def ==: (untyped other) -> bool
|
||||
def merge: (_ToHash[Symbol, untyped] other) -> instance
|
||||
def to_hash: () -> Hash[Symbol, untyped]
|
||||
|
@ -1,6 +1,6 @@
|
||||
module HTTPX
|
||||
class Pool
|
||||
@resolvers: Hash[Class, Resolver::Resolver]
|
||||
@resolvers: Hash[Class, Resolver::Multi]
|
||||
@timers: Timers
|
||||
@selector: Selector
|
||||
@connections: Array[Connection]
|
||||
@ -42,6 +42,6 @@ module HTTPX
|
||||
|
||||
def next_timeout: () -> (Integer | Float | nil)
|
||||
|
||||
def find_resolver_for: (Connection) -> Resolver::Resolver
|
||||
def find_resolver_for: (Connection) { (Resolver::Resolver) -> void } -> Resolver::Multi
|
||||
end
|
||||
end
|
||||
|
@ -4,6 +4,7 @@ module HTTPX
|
||||
NAMESERVER: String
|
||||
|
||||
DEFAULTS: Hash[Symbol, untyped]
|
||||
FAMILY_TYPES: Hash[singleton(Resolv::DNS::Resource), String]
|
||||
|
||||
@options: Options
|
||||
@requests: Hash[Request, Connection]
|
||||
@ -19,8 +20,6 @@ module HTTPX
|
||||
|
||||
private
|
||||
|
||||
def initialize: (options) -> untyped
|
||||
|
||||
def resolver_connection: () -> Connection
|
||||
|
||||
def resolve: (?Connection connection, ?String? hostname) -> void
|
||||
@ -29,7 +28,7 @@ module HTTPX
|
||||
|
||||
def parse: (Response response) -> void
|
||||
|
||||
def build_request: (String hostname, "A" | "AAAA") -> Request
|
||||
def build_request: (String hostname) -> Request
|
||||
|
||||
def decode_response_body: (Response) -> Array[dns_result]
|
||||
end
|
||||
|
6
sig/resolver/multi.rbs
Normal file
6
sig/resolver/multi.rbs
Normal file
@ -0,0 +1,6 @@
|
||||
module HTTPX
|
||||
module Resolver
|
||||
class Multi
|
||||
end
|
||||
end
|
||||
end
|
@ -28,8 +28,6 @@ module HTTPX
|
||||
|
||||
private
|
||||
|
||||
def initialize: (options) -> untyped
|
||||
|
||||
def calculate_interests: () -> (:r | :w)
|
||||
|
||||
def consume: () -> void
|
||||
|
@ -4,16 +4,17 @@ module HTTPX
|
||||
include Callbacks
|
||||
include Loggable
|
||||
|
||||
RECORD_TYPES: Hash[String, singleton(Resolv::DNS::Resource)]
|
||||
RECORD_TYPES: Hash[Integer, singleton(Resolv::DNS::Resource)]
|
||||
CHECK_IF_IP: ^(String name) -> bool
|
||||
|
||||
attr_reader family: Integer
|
||||
|
||||
@record_type: singleton(Resolv::DNS::Resource)
|
||||
@options: Options
|
||||
@resolver_options: Hash[Symbol, untyped]
|
||||
@_record_types: Hash[String, ["A" | "AAAA", dns_resource]]
|
||||
@queries: Hash[String, Connection]
|
||||
@system_resolver: Resolv::Hosts
|
||||
|
||||
CHECK_IF_IP: ^(String name) -> bool
|
||||
|
||||
def close: () -> void
|
||||
|
||||
def closed?: () -> bool
|
||||
@ -22,7 +23,7 @@ module HTTPX
|
||||
|
||||
private
|
||||
|
||||
def initialize: (options) -> void
|
||||
def initialize: (Integer family, options options) -> void
|
||||
|
||||
def emit_addresses: (Connection, Array[ipaddr | Resolv::DNS::ip_address]) -> void
|
||||
|
||||
@ -34,7 +35,7 @@ module HTTPX
|
||||
|
||||
def emit_resolve_error: (Connection, ?String hostname, ?StandardError) -> void
|
||||
|
||||
def resolve_error: (String hostname, ?StandardError?) -> void
|
||||
def resolve_error: (String hostname, ?StandardError?) -> ResolveError
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -6,10 +6,6 @@ module HTTPX
|
||||
@resolver: Resolv::DNS
|
||||
|
||||
def <<: (Connection) -> void
|
||||
|
||||
private
|
||||
|
||||
def initialize: (options) -> untyped
|
||||
end
|
||||
end
|
||||
end
|
@ -127,6 +127,7 @@ class OptionsTest < Minitest::Test
|
||||
:persistent => false,
|
||||
:resolver_class => bar.resolver_class,
|
||||
:resolver_options => bar.resolver_options,
|
||||
:ip_families => bar.ip_families,
|
||||
}.compact
|
||||
|
||||
assert foo.merge(bar).to_hash == expected, "options haven't merged correctly"
|
||||
|
@ -57,7 +57,7 @@ module Requests
|
||||
session = HTTPX.plugin(SessionWithPool)
|
||||
uri = URI(build_uri("/get"))
|
||||
resolver_class = Class.new(HTTPX::Resolver::HTTPS) do
|
||||
def build_request(_hostname, _type)
|
||||
def build_request(_hostname)
|
||||
@options.request_class.new("POST", @uri)
|
||||
end
|
||||
end
|
||||
|
@ -150,8 +150,15 @@ module WSTestPlugin
|
||||
attr_reader :websocket
|
||||
|
||||
def init_websocket(connection)
|
||||
socket = connection.to_io
|
||||
@websocket = WSCLient.new(socket, @headers)
|
||||
if connection.state == :open
|
||||
socket = connection.to_io
|
||||
@websocket = WSCLient.new(socket, @headers)
|
||||
else
|
||||
connection.once(:open) do
|
||||
socket = connection.to_io
|
||||
@websocket = WSCLient.new(socket, @headers)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user