lib/httpx/resolver/native.rb in httpx-0.22.5 vs lib/httpx/resolver/native.rb in httpx-0.23.0

- old
+ new

@@ -31,10 +31,11 @@ def initialize(_, options) super @ns_index = 0 @resolver_options = DEFAULTS.merge(@options.resolver_options) + @socket_type = @resolver_options.fetch(:socket_type, :udp) @nameserver = Array(@resolver_options[:nameserver]) if @resolver_options[:nameserver] @ndots = @resolver_options[:ndots] @search = Array(@resolver_options[:search]).map { |srch| srch.scan(/[^.]+/) } @_timeouts = Array(@resolver_options[:timeouts]) @timeouts = Hash.new { |timeouts, host| timeouts[host] = @_timeouts.dup } @@ -154,11 +155,11 @@ transition(:idle) resolve(connection) else @timeouts.delete(host) - @queries.delete(h) + reset_hostname(h, reset_candidates: false) return unless @queries.empty? @connections.delete(connection) # This loop_time passed to the exception is bogus. Ideally we would pass the total @@ -167,81 +168,143 @@ end end def dread(wsize = @resolver_options[:packet_size]) loop do + wsize = @large_packet.capacity if @large_packet + siz = @io.read(wsize, @read_buffer) - return unless siz && siz.positive? - parse(@read_buffer) + unless siz + ex = EOFError.new("descriptor closed") + ex.set_backtrace(caller) + raise ex + end + + return unless siz.positive? + + if @socket_type == :tcp + # packet may be incomplete, need to keep draining from the socket + if @large_packet + # large packet buffer already exists, continue pumping + @large_packet << @read_buffer + + next unless @large_packet.full? + + parse(@large_packet.to_s) + + @socket_type = @resolver_options.fetch(:socket_type, :udp) + @large_packet = nil + transition(:closed) + return + else + size = @read_buffer[0, 2].unpack1("n") + + if size > @read_buffer.bytesize + # only do buffer logic if it's worth it, and the whole packet isn't here already + @large_packet = Buffer.new(size) + @large_packet << @read_buffer.byteslice(2..-1) + + next + else + parse(@read_buffer) + end + end + else # udp + parse(@read_buffer) + end + return if @state == :closed end end def dwrite loop do return if @write_buffer.empty? siz = @io.write(@write_buffer) - return unless siz && siz.positive? + unless siz + ex = EOFError.new("descriptor closed") + ex.set_backtrace(caller) + raise ex + end + + return unless siz.positive? + return if @state == :closed end end def parse(buffer) - begin - addresses = Resolver.decode_dns_answer(buffer) - rescue Resolv::DNS::DecodeError => e + code, result = Resolver.decode_dns_answer(buffer) + + case code + when :ok + parse_addresses(result) + when :no_domain_found + # Indicates no such domain was found. hostname, connection = @queries.first - @queries.delete(hostname) - @timeouts.delete(hostname) + reset_hostname(hostname) + @connections.delete(connection) - ex = NativeResolveError.new(connection, connection.origin.host, e.message) + raise NativeResolveError.new(connection, connection.origin.host, "name or service not known (#{hostname})") + when :message_truncated + # TODO: what to do if it's already tcp?? + return if @socket_type == :tcp + + @socket_type = :tcp + + hostname, _ = @queries.first + reset_hostname(hostname) + transition(:closed) + when :dns_error + hostname, connection = @queries.first + reset_hostname(hostname) + @connections.delete(connection) + ex = NativeResolveError.new(connection, connection.origin.host, "unknown DNS error (error code #{result})") ex.set_backtrace(e.backtrace) raise ex + when :decode_error + hostname, connection = @queries.first + reset_hostname(hostname) + @connections.delete(connection) + ex = NativeResolveError.new(connection, connection.origin.host, result.message) + ex.set_backtrace(result.backtrace) + raise ex end + end - if addresses.nil? - # Indicates no such domain was found. - hostname, connection = @queries.first - @queries.delete(hostname) - @timeouts.delete(hostname) - - unless @queries.value?(connection) - @connections.delete(connection) - raise NativeResolveError.new(connection, connection.origin.host) - end - elsif addresses.empty? + def parse_addresses(addresses) + if addresses.empty? # no address found, eliminate candidates _, connection = @queries.first - candidates = @queries.select { |_, conn| connection == conn }.keys - @queries.delete_if { |hs, _| candidates.include?(hs) } - @timeouts.delete_if { |hs, _| candidates.include?(hs) } + reset_hostname(hostname) @connections.delete(connection) raise NativeResolveError.new(connection, connection.origin.host) else address = addresses.first name = address["name"] connection = @queries.delete(name) unless connection + orig_name = name # absolute name name_labels = Resolv::DNS::Name.create(name).to_a - name = @queries.keys.first { |hname| name_labels == Resolv::DNS::Name.create(hname).to_a } + name = @queries.each_key.first { |hname| name_labels == Resolv::DNS::Name.create(hname).to_a } # probably a retried query for which there's an answer - return unless name + unless name + @timeouts.delete(orig_name) + return + end address["name"] = name connection = @queries.delete(name) end - # eliminate other candidates - @queries.delete_if { |_, conn| connection == conn } - if address.key?("alias") # CNAME # clean up intermediate queries @timeouts.delete(name) unless connection.origin.host == name if catch(:coalesced) { early_resolve(connection, hostname: address["alias"]) } @@ -263,10 +326,11 @@ resolve end def resolve(connection = @connections.first, hostname = nil) raise Error, "no URI to resolve" unless connection + return unless @write_buffer.empty? hostname ||= @queries.key(connection) if hostname.nil? @@ -279,35 +343,49 @@ else @queries[hostname] = connection end log { "resolver: query #{@record_type.name.split("::").last} for #{hostname}" } begin - @write_buffer << Resolver.encode_dns_query(hostname, type: @record_type) + @write_buffer << encode_dns_query(hostname) rescue Resolv::DNS::EncodeError => e emit_resolve_error(connection, hostname, e) end end + def encode_dns_query(hostname) + message_id = Resolver.generate_id + msg = Resolver.encode_dns_query(hostname, type: @record_type, message_id: message_id) + msg[0, 2] = [msg.size, message_id].pack("nn") if @socket_type == :tcp + msg + end + def generate_candidates(name) return [name] if name.end_with?(".") candidates = [] name_parts = name.scan(/[^.]+/) candidates = [name] if @ndots <= name_parts.size - 1 candidates.concat(@search.map { |domain| [*name_parts, *domain].join(".") }) - candidates << name unless candidates.include?(name) + fname = "#{name}." + candidates << fname unless candidates.include?(fname) candidates end def build_socket - return if @io - ip, port = @nameserver[@ns_index] port ||= DNS_PORT - log { "resolver: server: #{ip}:#{port}..." } - @io = UDP.new(ip, port, @options) + + case @socket_type + when :udp + log { "resolver: server: udp://#{ip}:#{port}..." } + UDP.new(ip, port, @options) + when :tcp + log { "resolver: server: tcp://#{ip}:#{port}..." } + origin = URI("tcp://#{ip}:#{port}") + TCP.new(origin, [ip], @options) + end end def transition(nextstate) case nextstate when :idle @@ -317,11 +395,11 @@ end @timeouts.clear when :open return unless @state == :idle - build_socket + @io ||= build_socket @io.connect return unless @io.connected? resolve if @queries.empty? && !@connections.empty? @@ -343,8 +421,22 @@ else @queries.each do |host, connection| emit_resolve_error(connection, host, error) end end + end + + def reset_hostname(hostname, reset_candidates: true) + @timeouts.delete(hostname) + connection = @queries.delete(hostname) + @timeouts.delete(hostname) + + return unless connection && reset_candidates + + # eliminate other candidates + candidates = @queries.select { |_, conn| connection == conn }.keys + @queries.delete_if { |h, _| candidates.include?(h) } + # reset timeouts + @timeouts.delete_if { |h, _| candidates.include?(h) } end end end