File: resolver.rb

package info (click to toggle)
ruby-httpx 1.7.2-3
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 1,816 kB
  • sloc: ruby: 12,209; makefile: 4
file content (190 lines) | stat: -rw-r--r-- 5,245 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
# frozen_string_literal: true

require "resolv"

module HTTPX
  # Base class for all internal internet name resolvers. It handles basic blocks
  # from the Selectable API.
  #
  class Resolver::Resolver
    include Loggable

    using ArrayExtensions::Intersect

    RECORD_TYPES = {
      Socket::AF_INET6 => Resolv::DNS::Resource::IN::AAAA,
      Socket::AF_INET => Resolv::DNS::Resource::IN::A,
    }.freeze

    FAMILY_TYPES = {
      Resolv::DNS::Resource::IN::AAAA => "AAAA",
      Resolv::DNS::Resource::IN::A => "A",
    }.freeze

    class << self
      def multi?
        true
      end
    end

    attr_reader :family, :options

    attr_writer :current_selector, :current_session

    attr_accessor :multi

    def initialize(family, options)
      @family = family
      @record_type = RECORD_TYPES[family]
      @options = options
      @connections = []
    end

    def each_connection(&block)
      enum_for(__method__) unless block

      return unless @connections

      @connections.each(&block)
    end

    def close; end

    alias_method :terminate, :close

    def force_close(*args)
      while (connection = @connections.shift)
        connection.force_close(*args)
      end
    end

    def closed?
      true
    end

    def empty?
      true
    end

    def inflight?
      false
    end

    def emit_addresses(connection, family, addresses, early_resolve = false)
      addresses.map! { |address| address.is_a?(Resolver::Entry) ? address : Resolver::Entry.new(address) }

      # double emission check, but allow early resolution to work
      conn_addrs = connection.addresses
      return if !early_resolve && conn_addrs && !conn_addrs.empty? && !addresses.intersect?(conn_addrs)

      log do
        "resolver #{FAMILY_TYPES[RECORD_TYPES[family]]}: " \
          "answer #{connection.peer.host}: #{addresses.inspect} (early resolve: #{early_resolve})"
      end

      # do not apply resolution delay for non-dns name resolution
      if !early_resolve &&
         # just in case...
         @current_selector &&
         # resolution delay only applies to IPv4
         family == Socket::AF_INET &&
         # connection already has addresses and initiated/ended handshake
         !connection.io &&
         # no need to delay if not supporting dual stack / multi-homed IP
         (connection.options.ip_families || Resolver.supported_ip_families).size > 1 &&
         # connection URL host is already the IP (early resolve included perhaps?)
         addresses.first.to_s != connection.peer.host.to_s
        log { "resolver #{FAMILY_TYPES[RECORD_TYPES[family]]}: applying resolution delay..." }

        @current_selector.after(0.05) do
          # double emission check
          unless connection.addresses && addresses.intersect?(connection.addresses)
            emit_resolved_connection(connection, addresses, early_resolve)
          end
        end
      else
        emit_resolved_connection(connection, addresses, early_resolve)
      end
    end

    def handle_error(error)
      if error.respond_to?(:connection) &&
         error.respond_to?(:host)
        @connections.delete(error.connection)
        emit_resolve_error(error.connection, error.host, error)
      else
        while (connection = @connections.shift)
          emit_resolve_error(connection, connection.peer.host, error)
        end
      end
    end

    def on_error(error)
      handle_error(error)
      disconnect
    end

    def early_resolve(connection, hostname: connection.peer.host) # rubocop:disable Naming/PredicateMethod
      addresses = @resolver_options[:cache] && (connection.addresses || @options.resolver_cache.resolve(hostname))

      return false unless addresses

      addresses = addresses.select { |addr| addr.family == @family }

      return false if addresses.empty?

      emit_addresses(connection, @family, addresses, true)

      true
    end

    private

    def emit_resolved_connection(connection, addresses, early_resolve)
      begin
        connection.addresses = addresses

        return if connection.state == :closed

        resolve_connection(connection)
      rescue StandardError => e
        if early_resolve
          connection.force_close
          throw(:resolve_error, e)
        else
          emit_connection_error(connection, e)
        end
      end
    end

    def emit_resolve_error(connection, hostname = connection.peer.host, ex = nil)
      emit_connection_error(connection, resolve_error(hostname, ex))
    end

    def resolve_error(hostname, ex = nil)
      return ex if ex.is_a?(ResolveError) || ex.is_a?(ResolveTimeoutError)

      message = ex ? ex.message : "Can't resolve #{hostname}"
      error = ResolveError.new(message)
      error.set_backtrace(ex ? ex.backtrace : caller)
      error
    end

    def resolve_connection(connection)
      @current_session.__send__(:on_resolver_connection, connection, @current_selector)
    end

    def emit_connection_error(connection, error)
      return connection.handle_connect_error(error) if connection.connecting?

      connection.on_error(error)
    end

    def disconnect
      return if closed?

      close
      @current_session.deselect_resolver(self, @current_selector)
    end
  end
end