httpx 1.5.1 → 1.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/doc/release_notes/1_6_0.md +50 -0
- data/doc/release_notes/1_6_1.md +17 -0
- data/lib/httpx/adapters/datadog.rb +23 -13
- data/lib/httpx/adapters/faraday.rb +14 -9
- data/lib/httpx/adapters/webmock.rb +1 -1
- data/lib/httpx/callbacks.rb +1 -1
- data/lib/httpx/connection/http1.rb +5 -6
- data/lib/httpx/connection/http2.rb +34 -18
- data/lib/httpx/connection.rb +19 -26
- data/lib/httpx/errors.rb +3 -1
- data/lib/httpx/io/ssl.rb +1 -4
- data/lib/httpx/io/tcp.rb +52 -21
- data/lib/httpx/io/unix.rb +4 -3
- data/lib/httpx/loggable.rb +4 -1
- data/lib/httpx/options.rb +248 -160
- data/lib/httpx/plugins/aws_sdk_authentication.rb +2 -0
- data/lib/httpx/plugins/aws_sigv4.rb +2 -0
- data/lib/httpx/plugins/callbacks.rb +13 -1
- data/lib/httpx/plugins/circuit_breaker.rb +2 -0
- data/lib/httpx/plugins/content_digest.rb +2 -0
- data/lib/httpx/plugins/cookies.rb +2 -2
- data/lib/httpx/plugins/digest_auth.rb +2 -0
- data/lib/httpx/plugins/expect.rb +2 -0
- data/lib/httpx/plugins/fiber_concurrency.rb +195 -0
- data/lib/httpx/plugins/follow_redirects.rb +2 -0
- data/lib/httpx/plugins/grpc.rb +2 -0
- data/lib/httpx/plugins/h2c.rb +26 -16
- data/lib/httpx/plugins/internal_telemetry.rb +0 -49
- data/lib/httpx/plugins/ntlm_auth.rb +2 -0
- data/lib/httpx/plugins/oauth.rb +10 -2
- data/lib/httpx/plugins/persistent.rb +27 -18
- data/lib/httpx/plugins/proxy/socks4.rb +1 -1
- data/lib/httpx/plugins/proxy/socks5.rb +1 -1
- data/lib/httpx/plugins/proxy/ssh.rb +2 -0
- data/lib/httpx/plugins/proxy.rb +61 -20
- data/lib/httpx/plugins/response_cache/file_store.rb +2 -2
- data/lib/httpx/plugins/response_cache.rb +2 -0
- data/lib/httpx/plugins/retries.rb +2 -0
- data/lib/httpx/plugins/ssrf_filter.rb +2 -2
- data/lib/httpx/plugins/stream_bidi.rb +3 -3
- data/lib/httpx/plugins/upgrade/h2.rb +11 -1
- data/lib/httpx/plugins/upgrade.rb +8 -0
- data/lib/httpx/pool.rb +15 -10
- data/lib/httpx/request/body.rb +8 -3
- data/lib/httpx/request.rb +22 -11
- data/lib/httpx/resolver/entry.rb +30 -0
- data/lib/httpx/resolver/https.rb +3 -1
- data/lib/httpx/resolver/multi.rb +16 -3
- data/lib/httpx/resolver/native.rb +15 -6
- data/lib/httpx/resolver/resolver.rb +15 -11
- data/lib/httpx/resolver/system.rb +5 -3
- data/lib/httpx/resolver.rb +49 -21
- data/lib/httpx/response/body.rb +1 -1
- data/lib/httpx/response/buffer.rb +13 -18
- data/lib/httpx/selector.rb +92 -34
- data/lib/httpx/session.rb +89 -30
- data/lib/httpx/session_extensions.rb +3 -2
- data/lib/httpx/transcoder/form.rb +1 -13
- data/lib/httpx/transcoder/multipart/mime_type_detector.rb +1 -1
- data/lib/httpx/transcoder/multipart.rb +14 -0
- data/lib/httpx/transcoder/utils/deflater.rb +1 -1
- data/lib/httpx/version.rb +1 -1
- data/sig/callbacks.rbs +1 -1
- data/sig/chainable.rbs +1 -0
- data/sig/connection/http1.rbs +2 -0
- data/sig/connection/http2.rbs +5 -1
- data/sig/connection.rbs +6 -6
- data/sig/errors.rbs +3 -1
- data/sig/io/ssl.rbs +1 -1
- data/sig/io/tcp.rbs +13 -7
- data/sig/io/udp.rbs +7 -2
- data/sig/io/unix.rbs +0 -1
- data/sig/io.rbs +0 -3
- data/sig/options.rbs +64 -11
- data/sig/plugins/fiber_concurrency.rbs +51 -0
- data/sig/plugins/h2c.rbs +5 -1
- data/sig/plugins/oauth.rbs +15 -1
- data/sig/plugins/persistent.rbs +1 -1
- data/sig/plugins/proxy/socks4.rbs +1 -1
- data/sig/plugins/proxy/socks5.rbs +1 -1
- data/sig/plugins/proxy.rbs +5 -2
- data/sig/plugins/ssrf_filter.rbs +1 -1
- data/sig/plugins/stream_bidi.rbs +2 -2
- data/sig/request.rbs +4 -1
- data/sig/resolver/entry.rbs +13 -0
- data/sig/resolver/native.rbs +1 -0
- data/sig/resolver/resolver.rbs +2 -3
- data/sig/resolver/system.rbs +2 -2
- data/sig/resolver.rbs +12 -11
- data/sig/response.rbs +2 -2
- data/sig/selector.rbs +18 -10
- data/sig/session.rbs +4 -0
- data/sig/transcoder/form.rbs +3 -3
- data/sig/transcoder/multipart.rbs +9 -3
- metadata +11 -3
@@ -57,6 +57,8 @@ module HTTPX
|
|
57
57
|
# :retry_on :: callable which alternatively defines a different rule for when a response is to be retried
|
58
58
|
# (i.e. <tt>->(res) { ... }</tt>).
|
59
59
|
module OptionsMethods
|
60
|
+
private
|
61
|
+
|
60
62
|
def option_retry_after(value)
|
61
63
|
# return early if callable
|
62
64
|
unless value.respond_to?(:call)
|
@@ -91,6 +91,8 @@ module HTTPX
|
|
91
91
|
#
|
92
92
|
# :allowed_schemes :: list of URI schemes allowed (defaults to <tt>["https", "http"]</tt>)
|
93
93
|
module OptionsMethods
|
94
|
+
private
|
95
|
+
|
94
96
|
def option_allowed_schemes(value)
|
95
97
|
Array(value)
|
96
98
|
end
|
@@ -129,8 +131,6 @@ module HTTPX
|
|
129
131
|
end
|
130
132
|
|
131
133
|
def addresses=(addrs)
|
132
|
-
addrs = addrs.map { |addr| addr.is_a?(IPAddr) ? addr : IPAddr.new(addr) }
|
133
|
-
|
134
134
|
addrs.reject!(&SsrfFilter.method(:unsafe_ip_address?))
|
135
135
|
|
136
136
|
raise ServerSideRequestForgeryError, "#{@origin.host} has no public IP addresses" if addrs.empty?
|
@@ -42,7 +42,7 @@ module HTTPX
|
|
42
42
|
%i[join_headers join_trailers join_body].each do |lock_meth|
|
43
43
|
class_eval(<<-METH, __FILE__, __LINE__ + 1)
|
44
44
|
# lock.aware version of +#{lock_meth}+
|
45
|
-
def #{lock_meth}(*) # def join_headers(*)
|
45
|
+
private def #{lock_meth}(*) # private def join_headers(*)
|
46
46
|
return super if @lock.owned?
|
47
47
|
|
48
48
|
# small race condition between
|
@@ -119,7 +119,7 @@ module HTTPX
|
|
119
119
|
class Signal
|
120
120
|
def initialize
|
121
121
|
@closed = false
|
122
|
-
@pipe_read, @pipe_write =
|
122
|
+
@pipe_read, @pipe_write = IO.pipe
|
123
123
|
end
|
124
124
|
|
125
125
|
def state
|
@@ -127,7 +127,7 @@ module HTTPX
|
|
127
127
|
end
|
128
128
|
|
129
129
|
# noop
|
130
|
-
def log(
|
130
|
+
def log(**, &_); end
|
131
131
|
|
132
132
|
def to_io
|
133
133
|
@pipe_read.to_io
|
@@ -22,6 +22,16 @@ module HTTPX
|
|
22
22
|
module ConnectionMethods
|
23
23
|
using URIExtensions
|
24
24
|
|
25
|
+
def interests
|
26
|
+
return super unless connecting? && @parser
|
27
|
+
|
28
|
+
connect
|
29
|
+
|
30
|
+
return @io.interests if connecting?
|
31
|
+
|
32
|
+
super
|
33
|
+
end
|
34
|
+
|
25
35
|
def upgrade_to_h2
|
26
36
|
prev_parser = @parser
|
27
37
|
|
@@ -30,7 +40,7 @@ module HTTPX
|
|
30
40
|
@inflight -= prev_parser.requests.size
|
31
41
|
end
|
32
42
|
|
33
|
-
@parser =
|
43
|
+
@parser = @options.http2_class.new(@write_buffer, @options)
|
34
44
|
set_parser_callbacks(@parser)
|
35
45
|
@upgrade_protocol = "h2"
|
36
46
|
|
@@ -20,6 +20,8 @@ module HTTPX
|
|
20
20
|
end
|
21
21
|
|
22
22
|
module OptionsMethods
|
23
|
+
private
|
24
|
+
|
23
25
|
def option_upgrade_handlers(value)
|
24
26
|
raise TypeError, ":upgrade_handlers must be a Hash" unless value.is_a?(Hash)
|
25
27
|
|
@@ -65,6 +67,12 @@ module HTTPX
|
|
65
67
|
module ConnectionMethods
|
66
68
|
attr_reader :upgrade_protocol, :hijacked
|
67
69
|
|
70
|
+
def initialize(*)
|
71
|
+
super
|
72
|
+
|
73
|
+
@upgrade_protocol = nil
|
74
|
+
end
|
75
|
+
|
68
76
|
def hijack_io
|
69
77
|
@hijacked = true
|
70
78
|
|
data/lib/httpx/pool.rb
CHANGED
@@ -2,6 +2,8 @@
|
|
2
2
|
|
3
3
|
require "httpx/selector"
|
4
4
|
require "httpx/connection"
|
5
|
+
require "httpx/connection/http2"
|
6
|
+
require "httpx/connection/http1"
|
5
7
|
require "httpx/resolver"
|
6
8
|
|
7
9
|
module HTTPX
|
@@ -51,18 +53,21 @@ module HTTPX
|
|
51
53
|
# this takes precedence over per-origin
|
52
54
|
@max_connections_cond.wait(@connection_mtx, @pool_timeout)
|
53
55
|
|
54
|
-
acquire_connection(uri, options)
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
56
|
+
if (conn = acquire_connection(uri, options))
|
57
|
+
return conn
|
58
|
+
end
|
59
|
+
|
60
|
+
if @connections_counter == @max_connections
|
61
|
+
# if no matching usable connection was found, the pool will make room and drop a closed connection. if none is found,
|
62
|
+
# this means that all of them are persistent or being used, so raise a timeout error.
|
63
|
+
conn = @connections.find { |c| c.state == :closed }
|
59
64
|
|
60
|
-
|
61
|
-
|
65
|
+
raise PoolTimeoutError.new(@pool_timeout,
|
66
|
+
"Timed out after #{@pool_timeout} seconds while waiting for a connection") unless conn
|
62
67
|
|
63
|
-
|
64
|
-
end
|
68
|
+
drop_connection(conn)
|
65
69
|
end
|
70
|
+
|
66
71
|
end
|
67
72
|
|
68
73
|
if @origin_counters[uri.origin] == @max_connections_per_origin
|
@@ -110,7 +115,7 @@ module HTTPX
|
|
110
115
|
|
111
116
|
def checkout_resolver(options)
|
112
117
|
resolver_type = options.resolver_class
|
113
|
-
resolver_type = Resolver.resolver_for(resolver_type)
|
118
|
+
resolver_type = Resolver.resolver_for(resolver_type, options)
|
114
119
|
|
115
120
|
@resolver_mtx.synchronize do
|
116
121
|
resolvers = @resolvers[resolver_type]
|
data/lib/httpx/request/body.rb
CHANGED
@@ -56,7 +56,7 @@ module HTTPX
|
|
56
56
|
block.call(chunk)
|
57
57
|
end
|
58
58
|
# TODO: use copy_stream once bug is resolved: https://bugs.ruby-lang.org/issues/21131
|
59
|
-
#
|
59
|
+
# IO.copy_stream(body, ProcIO.new(block))
|
60
60
|
elsif body.respond_to?(:each)
|
61
61
|
body.each(&block)
|
62
62
|
else
|
@@ -127,8 +127,13 @@ module HTTPX
|
|
127
127
|
# @type var body: bodyIO
|
128
128
|
Transcoder::Body.encode(body)
|
129
129
|
elsif (form = params.delete(:form))
|
130
|
-
|
131
|
-
|
130
|
+
if Transcoder::Multipart.multipart?(form)
|
131
|
+
# @type var form: Transcoder::multipart_input
|
132
|
+
Transcoder::Multipart.encode(form)
|
133
|
+
else
|
134
|
+
# @type var form: Transcoder::urlencoded_input
|
135
|
+
Transcoder::Form.encode(form)
|
136
|
+
end
|
132
137
|
elsif (json = params.delete(:json))
|
133
138
|
# @type var body: _ToJson
|
134
139
|
Transcoder::JSON.encode(json)
|
data/lib/httpx/request.rb
CHANGED
@@ -8,6 +8,7 @@ module HTTPX
|
|
8
8
|
# as well as maintaining the state machine which manages streaming the request onto the wire.
|
9
9
|
class Request
|
10
10
|
extend Forwardable
|
11
|
+
include Loggable
|
11
12
|
include Callbacks
|
12
13
|
using URIExtensions
|
13
14
|
|
@@ -102,13 +103,16 @@ module HTTPX
|
|
102
103
|
raise UnsupportedSchemeError, "#{@uri}: #{@uri.scheme}: unsupported URI scheme" unless ALLOWED_URI_SCHEMES.include?(@uri.scheme)
|
103
104
|
|
104
105
|
@state = :idle
|
105
|
-
@response = nil
|
106
|
-
@peer_address = nil
|
106
|
+
@response = @peer_address = @context = @informational_status = nil
|
107
107
|
@ping = false
|
108
108
|
@persistent = @options.persistent
|
109
109
|
@active_timeouts = []
|
110
110
|
end
|
111
111
|
|
112
|
+
def complete!(response = @response)
|
113
|
+
emit(:complete, response)
|
114
|
+
end
|
115
|
+
|
112
116
|
# whether request has been buffered with a ping
|
113
117
|
def ping?
|
114
118
|
@ping
|
@@ -173,17 +177,23 @@ module HTTPX
|
|
173
177
|
def response=(response)
|
174
178
|
return unless response
|
175
179
|
|
176
|
-
|
177
|
-
|
180
|
+
case response
|
181
|
+
when Response
|
182
|
+
if response.status < 200
|
183
|
+
# deal with informational responses
|
178
184
|
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
185
|
+
if response.status == 100 && @headers.key?("expect")
|
186
|
+
@informational_status = response.status
|
187
|
+
return
|
188
|
+
end
|
189
|
+
|
190
|
+
# 103 Early Hints advertises resources in document to browsers.
|
191
|
+
# not very relevant for an HTTP client, discard.
|
192
|
+
return if response.status >= 103
|
183
193
|
|
184
|
-
|
185
|
-
|
186
|
-
|
194
|
+
end
|
195
|
+
when ErrorResponse
|
196
|
+
response.error.connection = nil if response.error.respond_to?(:connection=)
|
187
197
|
end
|
188
198
|
|
189
199
|
@response = response
|
@@ -293,6 +303,7 @@ module HTTPX
|
|
293
303
|
return if @state == :expect
|
294
304
|
|
295
305
|
end
|
306
|
+
log(level: 3) { "#{@state}] -> #{nextstate}" }
|
296
307
|
@state = nextstate
|
297
308
|
emit(@state, self)
|
298
309
|
nil
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "ipaddr"
|
4
|
+
|
5
|
+
module HTTPX
|
6
|
+
module Resolver
|
7
|
+
class Entry < SimpleDelegator
|
8
|
+
attr_reader :address
|
9
|
+
|
10
|
+
def self.convert(address)
|
11
|
+
new(address, rescue_on_convert: true)
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(address, expires_in = Float::INFINITY, rescue_on_convert: false)
|
15
|
+
@expires_in = expires_in
|
16
|
+
@address = address.is_a?(IPAddr) ? address : IPAddr.new(address.to_s)
|
17
|
+
super(@address)
|
18
|
+
rescue IPAddr::InvalidAddressError
|
19
|
+
raise unless rescue_on_convert
|
20
|
+
|
21
|
+
@address = address.to_s
|
22
|
+
super(@address)
|
23
|
+
end
|
24
|
+
|
25
|
+
def expired?
|
26
|
+
@expires_in < Utils.now
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/lib/httpx/resolver/https.rb
CHANGED
@@ -59,6 +59,8 @@ module HTTPX
|
|
59
59
|
resolve(connection)
|
60
60
|
end
|
61
61
|
|
62
|
+
# This is already indirectly monitored bt the HTTP connection. In order to skip
|
63
|
+
# monitoring, this method returns <tt>true</tt>.
|
62
64
|
def closed?
|
63
65
|
true
|
64
66
|
end
|
@@ -202,7 +204,7 @@ module HTTPX
|
|
202
204
|
@queries.delete_if { |_, conn| connection == conn }
|
203
205
|
|
204
206
|
Resolver.cached_lookup_set(hostname, @family, addresses) if @resolver_options[:cache]
|
205
|
-
catch(:coalesced) { emit_addresses(connection, @family, addresses.map { |
|
207
|
+
catch(:coalesced) { emit_addresses(connection, @family, addresses.map { |a| Resolver::Entry.new(a["data"], a["TTL"]) }) }
|
206
208
|
end
|
207
209
|
end
|
208
210
|
return if @connections.empty?
|
data/lib/httpx/resolver/multi.rb
CHANGED
@@ -11,12 +11,13 @@ module HTTPX
|
|
11
11
|
attr_reader :resolvers, :options
|
12
12
|
|
13
13
|
def initialize(resolver_type, options)
|
14
|
-
@current_selector = nil
|
15
|
-
@current_session = nil
|
14
|
+
@current_selector = @current_session = nil
|
16
15
|
@options = options
|
17
16
|
@resolver_options = @options.resolver_options
|
18
17
|
|
19
|
-
|
18
|
+
ip_families = options.ip_families || Resolver.supported_ip_families
|
19
|
+
|
20
|
+
@resolvers = ip_families.map do |ip_family|
|
20
21
|
resolver = resolver_type.new(ip_family, options)
|
21
22
|
resolver.multi = self
|
22
23
|
resolver
|
@@ -35,6 +36,10 @@ module HTTPX
|
|
35
36
|
@resolvers.each { |r| r.__send__(__method__, s) }
|
36
37
|
end
|
37
38
|
|
39
|
+
def log(*args, **kwargs, &blk)
|
40
|
+
@resolvers.each { |r| r.__send__(__method__, *args, **kwargs, &blk) }
|
41
|
+
end
|
42
|
+
|
38
43
|
def closed?
|
39
44
|
@resolvers.all?(&:closed?)
|
40
45
|
end
|
@@ -64,8 +69,12 @@ module HTTPX
|
|
64
69
|
addresses = @resolver_options[:cache] && (connection.addresses || HTTPX::Resolver.nolookup_resolve(hostname))
|
65
70
|
return false unless addresses
|
66
71
|
|
72
|
+
ip_families = connection.options.ip_families || Resolver.supported_ip_families
|
73
|
+
|
67
74
|
resolved = false
|
68
75
|
addresses.group_by(&:family).sort { |(f1, _), (f2, _)| f2 <=> f1 }.each do |family, addrs|
|
76
|
+
next unless ip_families.include?(family)
|
77
|
+
|
69
78
|
# try to match the resolver by family. However, there are cases where that's not possible, as when
|
70
79
|
# the system does not have IPv6 connectivity, but it does support IPv6 via loopback/link-local.
|
71
80
|
resolver = @resolvers.find { |r| r.family == family } || @resolvers.first
|
@@ -82,7 +91,11 @@ module HTTPX
|
|
82
91
|
end
|
83
92
|
|
84
93
|
def lazy_resolve(connection)
|
94
|
+
ip_families = connection.options.ip_families || Resolver.supported_ip_families
|
95
|
+
|
85
96
|
@resolvers.each do |resolver|
|
97
|
+
next unless ip_families.include?(resolver.family)
|
98
|
+
|
86
99
|
resolver << @current_session.try_clone_connection(connection, @current_selector, resolver.family)
|
87
100
|
next if resolver.empty?
|
88
101
|
|
@@ -42,6 +42,7 @@ module HTTPX
|
|
42
42
|
@read_buffer = "".b
|
43
43
|
@write_buffer = Buffer.new(@resolver_options[:packet_size])
|
44
44
|
@state = :idle
|
45
|
+
@timer = nil
|
45
46
|
end
|
46
47
|
|
47
48
|
def close
|
@@ -104,11 +105,11 @@ module HTTPX
|
|
104
105
|
private
|
105
106
|
|
106
107
|
def calculate_interests
|
107
|
-
return
|
108
|
+
return if @queries.empty?
|
108
109
|
|
109
|
-
return :r
|
110
|
+
return :r if @write_buffer.empty?
|
110
111
|
|
111
|
-
|
112
|
+
:w
|
112
113
|
end
|
113
114
|
|
114
115
|
def consume
|
@@ -153,6 +154,8 @@ module HTTPX
|
|
153
154
|
@timer = @current_selector.after(timeout) do
|
154
155
|
next unless @connections.include?(connection)
|
155
156
|
|
157
|
+
@timer = nil
|
158
|
+
|
156
159
|
do_retry(h, connection, timeout)
|
157
160
|
end
|
158
161
|
end
|
@@ -270,6 +273,8 @@ module HTTPX
|
|
270
273
|
def parse(buffer)
|
271
274
|
@timer.cancel
|
272
275
|
|
276
|
+
@timer = nil
|
277
|
+
|
273
278
|
code, result = Resolver.decode_dns_answer(buffer)
|
274
279
|
|
275
280
|
case code
|
@@ -370,7 +375,9 @@ module HTTPX
|
|
370
375
|
@timeouts.delete(connection.peer.host)
|
371
376
|
@connections.delete(connection)
|
372
377
|
Resolver.cached_lookup_set(connection.peer.host, @family, addresses) if @resolver_options[:cache]
|
373
|
-
catch(:coalesced)
|
378
|
+
catch(:coalesced) do
|
379
|
+
emit_addresses(connection, @family, addresses.map { |a| Resolver::Entry.new(a["data"], a["TTL"]) })
|
380
|
+
end
|
374
381
|
end
|
375
382
|
end
|
376
383
|
close_or_resolve
|
@@ -383,7 +390,8 @@ module HTTPX
|
|
383
390
|
|
384
391
|
raise Error, "no URI to resolve" unless connection
|
385
392
|
|
386
|
-
|
393
|
+
# do not buffer query if previous is still in the buffer or awaiting reply/retry
|
394
|
+
return unless @write_buffer.empty? && @timer.nil?
|
387
395
|
|
388
396
|
hostname ||= @queries.key(connection)
|
389
397
|
|
@@ -444,7 +452,7 @@ module HTTPX
|
|
444
452
|
when :tcp
|
445
453
|
log { "resolver #{FAMILY_TYPES[@record_type]}: server: tcp://#{ip}:#{port}..." }
|
446
454
|
origin = URI("tcp://#{ip}:#{port}")
|
447
|
-
TCP.new(origin, [ip], @options)
|
455
|
+
TCP.new(origin, [Resolver::Entry.new(ip)], @options)
|
448
456
|
end
|
449
457
|
end
|
450
458
|
|
@@ -480,6 +488,7 @@ module HTTPX
|
|
480
488
|
@write_buffer.clear
|
481
489
|
@read_buffer.clear
|
482
490
|
end
|
491
|
+
log(level: 3) { "#{@state} -> #{nextstate}" }
|
483
492
|
@state = nextstate
|
484
493
|
rescue Errno::ECONNREFUSED,
|
485
494
|
Errno::EADDRNOTAVAIL,
|
@@ -1,7 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "resolv"
|
4
|
-
require "ipaddr"
|
5
4
|
|
6
5
|
module HTTPX
|
7
6
|
# Base class for all internal internet name resolvers. It handles basic blocks
|
@@ -69,24 +68,29 @@ module HTTPX
|
|
69
68
|
end
|
70
69
|
|
71
70
|
def emit_addresses(connection, family, addresses, early_resolve = false)
|
72
|
-
addresses.map!
|
73
|
-
address.is_a?(IPAddr) ? address : IPAddr.new(address.to_s)
|
74
|
-
end
|
71
|
+
addresses.map! { |address| address.is_a?(Resolver::Entry) ? address : Resolver::Entry.new(address) }
|
75
72
|
|
76
73
|
# double emission check, but allow early resolution to work
|
77
|
-
|
74
|
+
conn_addrs = connection.addresses
|
75
|
+
return if !early_resolve && conn_addrs && (!conn_addrs.empty? && !addresses.intersect?(!conn_addrs))
|
78
76
|
|
79
77
|
log do
|
80
78
|
"resolver #{FAMILY_TYPES[RECORD_TYPES[family]]}: " \
|
81
79
|
"answer #{connection.peer.host}: #{addresses.inspect} (early resolve: #{early_resolve})"
|
82
80
|
end
|
83
81
|
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
82
|
+
# do not apply resolution delay for non-dns name resolution
|
83
|
+
if !early_resolve &&
|
84
|
+
# just in case...
|
85
|
+
@current_selector &&
|
86
|
+
# resolution delay only applies to IPv4
|
87
|
+
family == Socket::AF_INET &&
|
88
|
+
# connection already has addresses and initiated/ended handshake
|
89
|
+
!connection.io &&
|
90
|
+
# no need to delay if not supporting dual stack / multi-homed IP
|
91
|
+
(connection.options.ip_families || Resolver.supported_ip_families).size > 1 &&
|
92
|
+
# connection URL host is already the IP (early resolve included perhaps?)
|
93
|
+
addresses.first.to_s != connection.peer.host.to_s
|
90
94
|
log { "resolver #{FAMILY_TYPES[RECORD_TYPES[family]]}: applying resolution delay..." }
|
91
95
|
|
92
96
|
@current_selector.after(0.05) do
|
@@ -127,7 +127,7 @@ module HTTPX
|
|
127
127
|
when :open
|
128
128
|
return unless @state == :idle
|
129
129
|
|
130
|
-
@pipe_read, @pipe_write =
|
130
|
+
@pipe_read, @pipe_write = IO.pipe
|
131
131
|
when :closed
|
132
132
|
return unless @state == :open
|
133
133
|
|
@@ -187,7 +187,9 @@ module HTTPX
|
|
187
187
|
|
188
188
|
transition(:open)
|
189
189
|
|
190
|
-
connection.options.ip_families
|
190
|
+
ip_families = connection.options.ip_families || Resolver.supported_ip_families
|
191
|
+
|
192
|
+
ip_families.each do |family|
|
191
193
|
@queries << [family, connection]
|
192
194
|
end
|
193
195
|
async_resolve(connection, hostname, scheme)
|
@@ -195,7 +197,7 @@ module HTTPX
|
|
195
197
|
end
|
196
198
|
|
197
199
|
def async_resolve(connection, hostname, scheme)
|
198
|
-
families = connection.options.ip_families
|
200
|
+
families = connection.options.ip_families || Resolver.supported_ip_families
|
199
201
|
log { "resolver: query for #{hostname}" }
|
200
202
|
timeouts = @timeouts[connection.peer.host]
|
201
203
|
resolve_timeout = timeouts.first
|
data/lib/httpx/resolver.rb
CHANGED
@@ -1,12 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "socket"
|
3
4
|
require "resolv"
|
4
|
-
require "ipaddr"
|
5
5
|
|
6
6
|
module HTTPX
|
7
7
|
module Resolver
|
8
8
|
RESOLVE_TIMEOUT = [2, 3].freeze
|
9
9
|
|
10
|
+
require "httpx/resolver/entry"
|
10
11
|
require "httpx/resolver/resolver"
|
11
12
|
require "httpx/resolver/system"
|
12
13
|
require "httpx/resolver/native"
|
@@ -18,36 +19,54 @@ module HTTPX
|
|
18
19
|
|
19
20
|
@identifier_mutex = Thread::Mutex.new
|
20
21
|
@identifier = 1
|
21
|
-
@
|
22
|
+
@hosts_resolver = Resolv::Hosts.new
|
22
23
|
|
23
24
|
module_function
|
24
25
|
|
25
|
-
def
|
26
|
+
def supported_ip_families
|
27
|
+
@supported_ip_families ||= begin
|
28
|
+
# https://github.com/ruby/resolv/blob/095f1c003f6073730500f02acbdbc55f83d70987/lib/resolv.rb#L408
|
29
|
+
list = Socket.ip_address_list
|
30
|
+
if list.any? { |a| a.ipv6? && !a.ipv6_loopback? && !a.ipv6_linklocal? }
|
31
|
+
[Socket::AF_INET6, Socket::AF_INET]
|
32
|
+
else
|
33
|
+
[Socket::AF_INET]
|
34
|
+
end
|
35
|
+
rescue NotImplementedError
|
36
|
+
[Socket::AF_INET]
|
37
|
+
end.freeze
|
38
|
+
end
|
39
|
+
|
40
|
+
def resolver_for(resolver_type, options)
|
26
41
|
case resolver_type
|
27
|
-
when
|
28
|
-
|
29
|
-
when :https then HTTPS
|
30
|
-
else
|
31
|
-
return resolver_type if resolver_type.is_a?(Class) && resolver_type < Resolver
|
42
|
+
when Symbol
|
43
|
+
meth = :"resolver_#{resolver_type}_class"
|
32
44
|
|
33
|
-
|
45
|
+
return options.__send__(meth) if options.respond_to?(meth)
|
46
|
+
when Class
|
47
|
+
return resolver_type if resolver_type < Resolver
|
34
48
|
end
|
49
|
+
|
50
|
+
raise Error, "unsupported resolver type (#{resolver_type})"
|
35
51
|
end
|
36
52
|
|
37
53
|
def nolookup_resolve(hostname)
|
38
|
-
ip_resolve(hostname) || cached_lookup(hostname) ||
|
54
|
+
ip_resolve(hostname) || cached_lookup(hostname) || hosts_resolve(hostname)
|
39
55
|
end
|
40
56
|
|
57
|
+
# tries to convert +hostname+ into an IPAddr, returns <tt>nil</tt> otherwise.
|
41
58
|
def ip_resolve(hostname)
|
42
|
-
[
|
59
|
+
[Entry.new(hostname)]
|
43
60
|
rescue ArgumentError
|
44
61
|
end
|
45
62
|
|
46
|
-
|
47
|
-
|
63
|
+
# matches +hostname+ to entries in the hosts file, returns <tt>nil</nil> if none is
|
64
|
+
# found, or there is no hosts file.
|
65
|
+
def hosts_resolve(hostname)
|
66
|
+
ips = @hosts_resolver.getaddresses(hostname)
|
48
67
|
return if ips.empty?
|
49
68
|
|
50
|
-
ips.map { |ip|
|
69
|
+
ips.map { |ip| Entry.new(ip) }
|
51
70
|
rescue IOError
|
52
71
|
end
|
53
72
|
|
@@ -59,10 +78,6 @@ module HTTPX
|
|
59
78
|
end
|
60
79
|
|
61
80
|
def cached_lookup_set(hostname, family, entries)
|
62
|
-
now = Utils.now
|
63
|
-
entries.each do |entry|
|
64
|
-
entry["TTL"] += now
|
65
|
-
end
|
66
81
|
lookup_synchronize do |lookups|
|
67
82
|
case family
|
68
83
|
when Socket::AF_INET6
|
@@ -83,6 +98,18 @@ module HTTPX
|
|
83
98
|
end
|
84
99
|
end
|
85
100
|
|
101
|
+
def cached_lookup_evict(hostname, ip)
|
102
|
+
ip = ip.to_s
|
103
|
+
|
104
|
+
lookup_synchronize do |lookups|
|
105
|
+
entries = lookups[hostname]
|
106
|
+
|
107
|
+
return unless entries
|
108
|
+
|
109
|
+
lookups.delete_if { |entry| entry["data"] == ip }
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
86
113
|
# do not use directly!
|
87
114
|
def lookup(hostname, lookups, ttl)
|
88
115
|
return unless lookups.key?(hostname)
|
@@ -95,7 +122,7 @@ module HTTPX
|
|
95
122
|
if (als = address["alias"])
|
96
123
|
lookup(als, lookups, ttl)
|
97
124
|
else
|
98
|
-
|
125
|
+
Entry.new(address["data"], address["TTL"])
|
99
126
|
end
|
100
127
|
end.compact
|
101
128
|
|
@@ -129,19 +156,20 @@ module HTTPX
|
|
129
156
|
|
130
157
|
addresses = []
|
131
158
|
|
159
|
+
now = Utils.now
|
132
160
|
message.each_answer do |question, _, value|
|
133
161
|
case value
|
134
162
|
when Resolv::DNS::Resource::IN::CNAME
|
135
163
|
addresses << {
|
136
164
|
"name" => question.to_s,
|
137
|
-
"TTL" => value.ttl,
|
165
|
+
"TTL" => (now + value.ttl),
|
138
166
|
"alias" => value.name.to_s,
|
139
167
|
}
|
140
168
|
when Resolv::DNS::Resource::IN::A,
|
141
169
|
Resolv::DNS::Resource::IN::AAAA
|
142
170
|
addresses << {
|
143
171
|
"name" => question.to_s,
|
144
|
-
"TTL" => value.ttl,
|
172
|
+
"TTL" => (now + value.ttl),
|
145
173
|
"data" => value.address.to_s,
|
146
174
|
}
|
147
175
|
end
|
data/lib/httpx/response/body.rb
CHANGED