httpx 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,251 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+ require "resolv"
5
+
6
+ module HTTPX
7
+ class Resolver::Native
8
+ extend Forwardable
9
+ include Resolver::ResolverMixin
10
+
11
+ RESOLVE_TIMEOUT = 5
12
+ RECORD_TYPES = {
13
+ "A" => Resolv::DNS::Resource::IN::A,
14
+ "AAAA" => Resolv::DNS::Resource::IN::AAAA,
15
+ }.freeze
16
+
17
+ DEFAULTS = if RUBY_VERSION < "2.2"
18
+ {
19
+ **Resolv::DNS::Config.default_config_hash,
20
+ packet_size: 512,
21
+ timeouts: RESOLVE_TIMEOUT,
22
+ record_types: RECORD_TYPES.keys,
23
+ }.freeze
24
+ else
25
+ {
26
+ nameserver: nil,
27
+ **Resolv::DNS::Config.default_config_hash,
28
+ packet_size: 512,
29
+ timeouts: RESOLVE_TIMEOUT,
30
+ record_types: RECORD_TYPES.keys,
31
+ }.freeze
32
+ end
33
+
34
+ DNS_PORT = 53
35
+
36
+ def_delegator :@channels, :empty?
37
+
38
+ def initialize(_, options)
39
+ @options = Options.new(options)
40
+ @ns_index = 0
41
+ @resolver_options = Resolver::Options.new(DEFAULTS.merge(@options.resolver_options || {}))
42
+ @nameserver = @resolver_options.nameserver
43
+ @_timeouts = Array(@resolver_options.timeouts)
44
+ @timeouts = Hash.new { |timeouts, host| timeouts[host] = @_timeouts.dup }
45
+ @_record_types = Hash.new { |types, host| types[host] = @resolver_options.record_types.dup }
46
+ @channels = []
47
+ @queries = {}
48
+ @read_buffer = Buffer.new(@resolver_options.packet_size)
49
+ @write_buffer = Buffer.new(@resolver_options.packet_size)
50
+ @state = :idle
51
+ end
52
+
53
+ def close
54
+ transition(:closed)
55
+ end
56
+
57
+ def closed?
58
+ @state == :closed
59
+ end
60
+
61
+ def to_io
62
+ case @state
63
+ when :idle
64
+ transition(:open)
65
+ when :closed
66
+ transition(:idle)
67
+ transition(:open)
68
+ end
69
+ resolve if @queries.empty?
70
+ @io.to_io
71
+ end
72
+
73
+ def call
74
+ case @state
75
+ when :open
76
+ consume
77
+ end
78
+ nil
79
+ rescue Errno::EHOSTUNREACH => e
80
+ @ns_index += 1
81
+ if @ns_index < @nameserver.size
82
+ transition(:idle)
83
+ else
84
+ ex = ResolvError.new(e.message)
85
+ ex.set_backtrace(e.backtrace)
86
+ raise ex
87
+ end
88
+ end
89
+
90
+ def interests
91
+ readable = !@read_buffer.full?
92
+ writable = !@write_buffer.empty?
93
+ if readable
94
+ writable ? :rw : :r
95
+ else
96
+ writable ? :w : :r
97
+ end
98
+ end
99
+
100
+ def <<(channel)
101
+ return if early_resolve(channel)
102
+ if @nameserver.nil?
103
+ ex = ResolveError.new("Can't resolve #{channel.uri.host}")
104
+ ex.set_backtrace(caller)
105
+ emit(:error, channel, ex)
106
+ else
107
+ @channels << channel
108
+ end
109
+ end
110
+
111
+ def timeout
112
+ @start_timeout = Process.clock_gettime(Process::CLOCK_MONOTONIC)
113
+ hosts = @queries.keys
114
+ @timeouts.values_at(*hosts).reject(&:empty?).map(&:first).min
115
+ end
116
+
117
+ private
118
+
119
+ def consume
120
+ dread
121
+ do_retry
122
+ dwrite
123
+ end
124
+
125
+ def do_retry
126
+ return if @queries.empty?
127
+ loop_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) - @start_timeout
128
+ channels = []
129
+ queries = {}
130
+ while (query = @queries.shift)
131
+ h, channel = query
132
+ host = channel.uri.host
133
+ timeout = (@timeouts[host][0] -= loop_time)
134
+ unless timeout.negative?
135
+ queries[h] = channel
136
+ next
137
+ end
138
+ @timeouts[host].shift
139
+ if @timeouts[host].empty?
140
+ @timeouts.delete(host)
141
+ emit_resolve_error(channel, host)
142
+ return
143
+ else
144
+ channels << channel
145
+ log(label: "resolver: ") do
146
+ "timeout after #{prev_timeout}s, retry(#{timeouts.first}) #{host}..."
147
+ end
148
+ end
149
+ end
150
+ @queries = queries
151
+ channels.each { |ch| resolve(ch) }
152
+ end
153
+
154
+ def dread(wsize = @read_buffer.limit)
155
+ loop do
156
+ siz = @io.read(wsize, @read_buffer)
157
+ unless siz
158
+ emit(:close)
159
+ return
160
+ end
161
+ return if siz.zero?
162
+ log(label: "resolver: ") { "READ: #{siz} bytes..." }
163
+ parse(@read_buffer.to_s)
164
+ end
165
+ end
166
+
167
+ def dwrite
168
+ loop do
169
+ return if @write_buffer.empty?
170
+ siz = @io.write(@write_buffer)
171
+ unless siz
172
+ emit(:close)
173
+ return
174
+ end
175
+ log(label: "resolver: ") { "WRITE: #{siz} bytes..." }
176
+ return if siz.zero?
177
+ end
178
+ end
179
+
180
+ def parse(buffer)
181
+ addresses = Resolver.decode_dns_answer(buffer)
182
+ if addresses.empty?
183
+ hostname, channel = @queries.first
184
+ if @_record_types[hostname].empty?
185
+ emit_resolve_error(channel, hostname)
186
+ return
187
+ end
188
+ else
189
+ address = addresses.first
190
+ channel = @queries.delete(address["name"])
191
+ return unless channel # probably a retried query for which there's an answer
192
+ if address.key?("alias") # CNAME
193
+ if early_resolve(channel, hostname: address["alias"])
194
+ @channels.delete(channel)
195
+ else
196
+ resolve(channel, address["alias"])
197
+ @queries.delete(address["name"])
198
+ return
199
+ end
200
+ else
201
+ @channels.delete(channel)
202
+ Resolver.cached_lookup_set(channel.uri.host, addresses)
203
+ emit_addresses(channel, addresses.map { |addr| addr["data"] })
204
+ end
205
+ end
206
+ return emit(:close) if @channels.empty?
207
+ resolve
208
+ end
209
+
210
+ def resolve(channel = @channels.first, hostname = nil)
211
+ raise Error, "no URI to resolve" unless channel
212
+ return unless @write_buffer.empty?
213
+ hostname = hostname || @queries.key(channel) || channel.uri.host
214
+ @queries[hostname] = channel
215
+ type = @_record_types[hostname].shift
216
+ log(label: "resolver: ") { "query #{type} for #{hostname}" }
217
+ @write_buffer << Resolver.encode_dns_query(hostname, type: RECORD_TYPES[type])
218
+ end
219
+
220
+ def build_socket
221
+ return if @io
222
+ ip, port = @nameserver[@ns_index]
223
+ port ||= DNS_PORT
224
+ uri = URI::Generic.build(scheme: "udp", port: port)
225
+ uri.hostname = ip
226
+ type = IO.registry(uri.scheme)
227
+ log(label: "resolver: ") { "server: #{uri}..." }
228
+ @io = type.new(uri, [IPAddr.new(ip)], @options)
229
+ end
230
+
231
+ def transition(nextstate)
232
+ case nextstate
233
+ when :idle
234
+ if @io
235
+ @io.close
236
+ @io = nil
237
+ end
238
+ @timeouts.clear
239
+ when :open
240
+ return unless @state == :idle
241
+ build_socket
242
+ @io.connect
243
+ return unless @io.connected?
244
+ when :closed
245
+ return unless @state == :open
246
+ @io.close if @io
247
+ end
248
+ @state = nextstate
249
+ end
250
+ end
251
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTPX
4
+ class Resolver::Options
5
+ def initialize(options = {})
6
+ @options = options
7
+ end
8
+
9
+ def method_missing(m, *args, &block)
10
+ if @options.key?(m)
11
+ @options[m]
12
+ else
13
+ super
14
+ end
15
+ end
16
+
17
+ def respond_to_missing?(m)
18
+ @options.key?(m) || super
19
+ end
20
+
21
+ def to_h
22
+ @options
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "resolv"
4
+ require "ipaddr"
5
+
6
+ module HTTPX
7
+ module Resolver
8
+ module ResolverMixin
9
+ include Callbacks
10
+ include Loggable
11
+
12
+ CHECK_IF_IP = proc do |name|
13
+ begin
14
+ IPAddr.new(name)
15
+ true
16
+ rescue ArgumentError
17
+ false
18
+ end
19
+ end
20
+
21
+ def uncache(channel)
22
+ hostname = hostname || @queries.key(channel) || channel.uri.host
23
+ Resolver.uncache(hostname)
24
+ end
25
+
26
+ private
27
+
28
+ def emit_addresses(channel, addresses)
29
+ addresses.map! do |address|
30
+ address.is_a?(IPAddr) ? address : IPAddr.new(address.to_s)
31
+ end
32
+ log(label: "resolver: ") { "answer #{channel.uri.host}: #{addresses.inspect}" }
33
+ channel.addresses = addresses
34
+ emit(:resolve, channel, addresses)
35
+ end
36
+
37
+ def early_resolve(channel, hostname: channel.uri.host)
38
+ addresses = ip_resolve(hostname) || Resolver.cached_lookup(hostname) || system_resolve(hostname)
39
+ return unless addresses
40
+ emit_addresses(channel, addresses)
41
+ end
42
+
43
+ def ip_resolve(hostname)
44
+ [hostname] if CHECK_IF_IP[hostname]
45
+ end
46
+
47
+ def system_resolve(hostname)
48
+ @system_resolver ||= Resolv::Hosts.new
49
+ ips = @system_resolver.getaddresses(hostname)
50
+ return if ips.empty?
51
+ ips.map { |ip| IPAddr.new(ip) }
52
+ end
53
+
54
+ def emit_resolve_error(channel, hostname)
55
+ error = ResolveError.new("Can't resolve #{hostname}")
56
+ error.set_backtrace(caller)
57
+ emit(:error, channel, error)
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+ require "resolv"
5
+
6
+ module HTTPX
7
+ class Resolver::System
8
+ include Resolver::ResolverMixin
9
+
10
+ def initialize(_, options)
11
+ @options = Options.new(options)
12
+ roptions = @options.resolver_options
13
+ @state = :idle
14
+ @resolver = Resolv::DNS.new(roptions.nil? ? nil : roptions)
15
+ @resolver.timeouts = roptions[:timeouts] if roptions
16
+ end
17
+
18
+ def closed?
19
+ true
20
+ end
21
+
22
+ def empty?
23
+ true
24
+ end
25
+
26
+ def <<(channel)
27
+ hostname = channel.uri.host
28
+ addresses = ip_resolve(hostname) || system_resolve(hostname) || @resolver.getaddresses(hostname)
29
+ addresses.empty? ? emit_resolve_error(channel, hostname) : emit_addresses(channel, addresses)
30
+ end
31
+
32
+ def uncache(*); end
33
+ end
34
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTPX
4
+ module Resolver
5
+ autoload :ResolverMixin, "httpx/resolver/resolver_mixin"
6
+ autoload :System, "httpx/resolver/system"
7
+ autoload :Native, "httpx/resolver/native"
8
+ autoload :HTTPS, "httpx/resolver/https"
9
+
10
+ extend Registry
11
+
12
+ register :system, :System
13
+ register :native, :Native
14
+ register :https, :HTTPS
15
+
16
+ @lookup_mutex = Mutex.new
17
+ @lookups = Hash.new { |h, k| h[k] = [] }
18
+
19
+ @identifier_mutex = Mutex.new
20
+ @identifier = 1
21
+
22
+ module_function
23
+
24
+ def cached_lookup(hostname)
25
+ now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
26
+ @lookup_mutex.synchronize do
27
+ lookup(hostname, now)
28
+ end
29
+ end
30
+
31
+ def cached_lookup_set(hostname, entries)
32
+ now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
33
+ entries.each do |entry|
34
+ entry["TTL"] += now
35
+ end
36
+ @lookup_mutex.synchronize do
37
+ @lookups[hostname] += entries
38
+ entries.each do |entry|
39
+ @lookups[entry["name"]] << entry if entry["name"] != hostname
40
+ end
41
+ end
42
+ end
43
+
44
+ def uncache(hostname)
45
+ @lookup_mutex.synchronize do
46
+ @lookups.delete(hostname)
47
+ end
48
+ end
49
+
50
+ # do not use directly!
51
+ def lookup(hostname, ttl)
52
+ return unless @lookups.key?(hostname)
53
+ @lookups[hostname] = @lookups[hostname].select do |address|
54
+ address["TTL"] > ttl
55
+ end
56
+ ips = @lookups[hostname].flat_map do |address|
57
+ if address.key?("alias")
58
+ lookup(address["alias"], ttl)
59
+ else
60
+ address["data"]
61
+ end
62
+ end
63
+ ips unless ips.empty?
64
+ end
65
+
66
+ def generate_id
67
+ @identifier_mutex.synchronize { @identifier = (@identifier + 1) & 0xFFFF }
68
+ end
69
+
70
+ def encode_dns_query(hostname, type: Resolv::DNS::Resource::IN::A)
71
+ Resolv::DNS::Message.new.tap do |query|
72
+ query.id = generate_id
73
+ query.rd = 1
74
+ query.add_question(hostname, type)
75
+ end.encode
76
+ end
77
+
78
+ def decode_dns_answer(payload)
79
+ message = Resolv::DNS::Message.decode(payload)
80
+ addresses = []
81
+ message.each_answer do |question, _, value|
82
+ case value
83
+ when Resolv::DNS::Resource::IN::CNAME
84
+ addresses << {
85
+ "name" => question.to_s,
86
+ "TTL" => value.ttl,
87
+ "alias" => value.name.to_s,
88
+ }
89
+ when Resolv::DNS::Resource::IN::A,
90
+ Resolv::DNS::Resource::IN::AAAA
91
+ addresses << {
92
+ "name" => question.to_s,
93
+ "TTL" => value.ttl,
94
+ "data" => value.address.to_s,
95
+ }
96
+ end
97
+ end
98
+ addresses
99
+ end
100
+ end
101
+ end
102
+
103
+ require "httpx/resolver/options"