httpx 0.20.0 → 1.3.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/LICENSE.txt +0 -48
- data/README.md +54 -45
- data/doc/release_notes/0_10_0.md +2 -2
- data/doc/release_notes/0_11_0.md +3 -5
- data/doc/release_notes/0_12_0.md +5 -5
- data/doc/release_notes/0_13_0.md +5 -5
- data/doc/release_notes/0_14_0.md +2 -2
- data/doc/release_notes/0_16_0.md +3 -3
- data/doc/release_notes/0_17_0.md +1 -1
- data/doc/release_notes/0_18_0.md +4 -4
- data/doc/release_notes/0_18_2.md +1 -1
- data/doc/release_notes/0_19_0.md +1 -1
- data/doc/release_notes/0_19_8.md +1 -1
- data/doc/release_notes/0_20_0.md +2 -2
- data/doc/release_notes/0_20_1.md +5 -0
- data/doc/release_notes/0_20_2.md +7 -0
- data/doc/release_notes/0_20_3.md +6 -0
- data/doc/release_notes/0_20_4.md +17 -0
- data/doc/release_notes/0_20_5.md +3 -0
- data/doc/release_notes/0_21_0.md +96 -0
- data/doc/release_notes/0_21_1.md +12 -0
- data/doc/release_notes/0_22_0.md +13 -0
- data/doc/release_notes/0_22_1.md +11 -0
- data/doc/release_notes/0_22_2.md +5 -0
- data/doc/release_notes/0_22_3.md +55 -0
- data/doc/release_notes/0_22_4.md +6 -0
- data/doc/release_notes/0_22_5.md +6 -0
- data/doc/release_notes/0_23_0.md +42 -0
- data/doc/release_notes/0_23_1.md +5 -0
- data/doc/release_notes/0_23_2.md +5 -0
- data/doc/release_notes/0_23_3.md +6 -0
- data/doc/release_notes/0_23_4.md +5 -0
- data/doc/release_notes/0_24_0.md +48 -0
- data/doc/release_notes/0_24_1.md +12 -0
- data/doc/release_notes/0_24_2.md +12 -0
- data/doc/release_notes/0_24_3.md +12 -0
- data/doc/release_notes/0_24_4.md +18 -0
- data/doc/release_notes/0_24_5.md +6 -0
- data/doc/release_notes/0_24_6.md +5 -0
- data/doc/release_notes/0_24_7.md +10 -0
- data/doc/release_notes/1_0_0.md +60 -0
- data/doc/release_notes/1_0_1.md +5 -0
- data/doc/release_notes/1_0_2.md +7 -0
- data/doc/release_notes/1_1_0.md +32 -0
- data/doc/release_notes/1_1_1.md +17 -0
- data/doc/release_notes/1_1_2.md +12 -0
- data/doc/release_notes/1_1_3.md +18 -0
- data/doc/release_notes/1_1_4.md +6 -0
- data/doc/release_notes/1_1_5.md +12 -0
- data/doc/release_notes/1_2_0.md +49 -0
- data/doc/release_notes/1_2_1.md +6 -0
- data/doc/release_notes/1_2_2.md +10 -0
- data/doc/release_notes/1_2_3.md +16 -0
- data/doc/release_notes/1_2_4.md +8 -0
- data/doc/release_notes/1_2_5.md +7 -0
- data/doc/release_notes/1_2_6.md +13 -0
- data/doc/release_notes/1_3_0.md +18 -0
- data/doc/release_notes/1_3_1.md +17 -0
- data/lib/httpx/adapters/datadog.rb +215 -122
- data/lib/httpx/adapters/faraday.rb +145 -107
- data/lib/httpx/adapters/sentry.rb +26 -7
- data/lib/httpx/adapters/webmock.rb +34 -18
- data/lib/httpx/altsvc.rb +63 -26
- data/lib/httpx/base64.rb +27 -0
- data/lib/httpx/buffer.rb +12 -0
- data/lib/httpx/callbacks.rb +5 -3
- data/lib/httpx/chainable.rb +54 -39
- data/lib/httpx/connection/http1.rb +75 -44
- data/lib/httpx/connection/http2.rb +31 -38
- data/lib/httpx/connection.rb +287 -117
- data/lib/httpx/domain_name.rb +10 -13
- data/lib/httpx/errors.rb +52 -2
- data/lib/httpx/extensions.rb +24 -131
- data/lib/httpx/io/ssl.rb +83 -77
- data/lib/httpx/io/tcp.rb +48 -71
- data/lib/httpx/io/udp.rb +18 -52
- data/lib/httpx/io/unix.rb +10 -15
- data/lib/httpx/io.rb +3 -9
- data/lib/httpx/loggable.rb +4 -19
- data/lib/httpx/options.rb +176 -118
- data/lib/httpx/parser/http1.rb +4 -0
- data/lib/httpx/plugins/{authentication → auth}/basic.rb +1 -5
- data/lib/httpx/plugins/{authentication → auth}/digest.rb +14 -14
- data/lib/httpx/plugins/{authentication → auth}/ntlm.rb +1 -3
- data/lib/httpx/plugins/{authentication → auth}/socks5.rb +0 -2
- data/lib/httpx/plugins/auth.rb +25 -0
- data/lib/httpx/plugins/aws_sdk_authentication.rb +4 -3
- data/lib/httpx/plugins/aws_sigv4.rb +12 -9
- data/lib/httpx/plugins/basic_auth.rb +29 -0
- data/lib/httpx/plugins/brotli.rb +50 -0
- data/lib/httpx/plugins/callbacks.rb +91 -0
- data/lib/httpx/plugins/circuit_breaker/circuit.rb +100 -0
- data/lib/httpx/plugins/circuit_breaker/circuit_store.rb +53 -0
- data/lib/httpx/plugins/circuit_breaker.rb +148 -0
- data/lib/httpx/plugins/cookies/set_cookie_parser.rb +0 -2
- data/lib/httpx/plugins/cookies.rb +30 -17
- data/lib/httpx/plugins/{digest_authentication.rb → digest_auth.rb} +14 -12
- data/lib/httpx/plugins/expect.rb +21 -14
- data/lib/httpx/plugins/follow_redirects.rb +140 -41
- data/lib/httpx/plugins/grpc/call.rb +2 -3
- data/lib/httpx/plugins/grpc/grpc_encoding.rb +88 -0
- data/lib/httpx/plugins/grpc/message.rb +7 -37
- data/lib/httpx/plugins/grpc.rb +36 -29
- data/lib/httpx/plugins/h2c.rb +26 -19
- data/lib/httpx/plugins/internal_telemetry.rb +16 -0
- data/lib/httpx/plugins/{ntlm_authentication.rb → ntlm_auth.rb} +7 -5
- data/lib/httpx/plugins/oauth.rb +175 -0
- data/lib/httpx/plugins/persistent.rb +1 -1
- data/lib/httpx/plugins/proxy/http.rb +23 -13
- data/lib/httpx/plugins/proxy/socks4.rb +9 -7
- data/lib/httpx/plugins/proxy/socks5.rb +11 -9
- data/lib/httpx/plugins/proxy.rb +80 -61
- data/lib/httpx/plugins/push_promise.rb +1 -1
- data/lib/httpx/plugins/rate_limiter.rb +5 -1
- data/lib/httpx/plugins/response_cache/file_store.rb +40 -0
- data/lib/httpx/plugins/response_cache/store.rb +62 -25
- data/lib/httpx/plugins/response_cache.rb +105 -12
- data/lib/httpx/plugins/retries.rb +87 -17
- data/lib/httpx/plugins/ssrf_filter.rb +145 -0
- data/lib/httpx/plugins/stream.rb +27 -23
- data/lib/httpx/plugins/upgrade/h2.rb +4 -4
- data/lib/httpx/plugins/upgrade.rb +8 -10
- data/lib/httpx/plugins/webdav.rb +80 -0
- data/lib/httpx/pool/synch_pool.rb +93 -0
- data/lib/httpx/pool.rb +102 -27
- data/lib/httpx/punycode.rb +9 -291
- data/lib/httpx/request/body.rb +154 -0
- data/lib/httpx/request.rb +130 -146
- data/lib/httpx/resolver/https.rb +62 -27
- data/lib/httpx/resolver/multi.rb +9 -13
- data/lib/httpx/resolver/native.rb +192 -76
- data/lib/httpx/resolver/resolver.rb +34 -9
- data/lib/httpx/resolver/system.rb +16 -11
- data/lib/httpx/resolver.rb +38 -16
- data/lib/httpx/response/body.rb +242 -0
- data/lib/httpx/response/buffer.rb +96 -0
- data/lib/httpx/response.rb +159 -217
- data/lib/httpx/selector.rb +9 -4
- data/lib/httpx/session.rb +137 -89
- data/lib/httpx/session_extensions.rb +4 -1
- data/lib/httpx/timers.rb +34 -8
- data/lib/httpx/transcoder/body.rb +0 -2
- data/lib/httpx/transcoder/chunker.rb +0 -1
- data/lib/httpx/transcoder/deflate.rb +37 -0
- data/lib/httpx/transcoder/form.rb +52 -33
- data/lib/httpx/transcoder/gzip.rb +74 -0
- data/lib/httpx/transcoder/json.rb +21 -8
- data/lib/httpx/transcoder/multipart/decoder.rb +139 -0
- data/lib/httpx/{plugins → transcoder}/multipart/encoder.rb +4 -4
- data/lib/httpx/{plugins → transcoder}/multipart/mime_type_detector.rb +1 -1
- data/lib/httpx/{plugins → transcoder}/multipart/part.rb +3 -2
- data/lib/httpx/transcoder/multipart.rb +17 -0
- data/lib/httpx/transcoder/utils/body_reader.rb +46 -0
- data/lib/httpx/transcoder/utils/deflater.rb +72 -0
- data/lib/httpx/transcoder/utils/inflater.rb +19 -0
- data/lib/httpx/transcoder/xml.rb +52 -0
- data/lib/httpx/transcoder.rb +5 -6
- data/lib/httpx/utils.rb +36 -16
- data/lib/httpx/version.rb +1 -1
- data/lib/httpx.rb +12 -14
- data/sig/altsvc.rbs +33 -0
- data/sig/buffer.rbs +2 -1
- data/sig/callbacks.rbs +3 -3
- data/sig/chainable.rbs +11 -9
- data/sig/connection/http1.rbs +8 -7
- data/sig/connection/http2.rbs +19 -19
- data/sig/connection.rbs +64 -24
- data/sig/errors.rbs +22 -3
- data/sig/httpx.rbs +5 -4
- data/sig/io/ssl.rbs +27 -0
- data/sig/io/tcp.rbs +60 -0
- data/sig/io/udp.rbs +20 -0
- data/sig/io/unix.rbs +27 -0
- data/sig/io.rbs +6 -0
- data/sig/options.rbs +32 -22
- data/sig/parser/http1.rbs +1 -1
- data/sig/plugins/{authentication → auth}/basic.rbs +0 -2
- data/sig/plugins/{authentication → auth}/digest.rbs +2 -1
- data/sig/plugins/auth.rbs +13 -0
- data/sig/plugins/{basic_authentication.rbs → basic_auth.rbs} +2 -2
- data/sig/plugins/brotli.rbs +22 -0
- data/sig/plugins/callbacks.rbs +38 -0
- data/sig/plugins/circuit_breaker.rbs +71 -0
- data/sig/plugins/compression.rbs +7 -5
- data/sig/plugins/cookies/jar.rbs +2 -2
- data/sig/plugins/cookies.rbs +2 -0
- data/sig/plugins/{digest_authentication.rbs → digest_auth.rbs} +2 -2
- data/sig/plugins/follow_redirects.rbs +18 -4
- data/sig/plugins/grpc/call.rbs +19 -0
- data/sig/plugins/grpc/grpc_encoding.rbs +37 -0
- data/sig/plugins/grpc/message.rbs +17 -0
- data/sig/plugins/grpc.rbs +7 -32
- data/sig/plugins/h2c.rbs +1 -1
- data/sig/plugins/{ntlm_authentication.rbs → ntlm_auth.rbs} +2 -2
- data/sig/plugins/oauth.rbs +54 -0
- data/sig/plugins/proxy/http.rbs +3 -0
- data/sig/plugins/proxy/socks4.rbs +9 -6
- data/sig/plugins/proxy/socks5.rbs +10 -6
- data/sig/plugins/proxy/ssh.rbs +1 -1
- data/sig/plugins/proxy.rbs +13 -5
- data/sig/plugins/push_promise.rbs +3 -3
- data/sig/plugins/rate_limiter.rbs +1 -1
- data/sig/plugins/response_cache.rbs +36 -7
- data/sig/plugins/retries.rbs +30 -8
- data/sig/plugins/stream.rbs +24 -17
- data/sig/plugins/upgrade.rbs +5 -3
- data/sig/pool.rbs +10 -7
- data/sig/request/body.rbs +38 -0
- data/sig/request.rbs +15 -24
- data/sig/resolver/https.rbs +8 -3
- data/sig/resolver/native.rbs +17 -4
- data/sig/resolver/resolver.rbs +8 -6
- data/sig/resolver/system.rbs +2 -0
- data/sig/resolver.rbs +9 -5
- data/sig/response/body.rbs +53 -0
- data/sig/response/buffer.rbs +24 -0
- data/sig/response.rbs +24 -39
- data/sig/selector.rbs +1 -1
- data/sig/session.rbs +29 -18
- data/sig/timers.rbs +18 -8
- data/sig/transcoder/body.rbs +4 -3
- data/sig/transcoder/deflate.rbs +11 -0
- data/sig/transcoder/form.rbs +5 -3
- data/sig/transcoder/gzip.rbs +24 -0
- data/sig/transcoder/json.rbs +8 -3
- data/sig/{plugins → transcoder}/multipart.rbs +15 -19
- data/sig/transcoder/utils/body_reader.rbs +15 -0
- data/sig/transcoder/utils/deflater.rbs +29 -0
- data/sig/transcoder/utils/inflater.rbs +12 -0
- data/sig/transcoder/xml.rbs +22 -0
- data/sig/transcoder.rbs +24 -9
- data/sig/utils.rbs +8 -2
- metadata +163 -41
- data/lib/httpx/plugins/authentication.rb +0 -20
- data/lib/httpx/plugins/basic_authentication.rb +0 -30
- data/lib/httpx/plugins/compression/brotli.rb +0 -54
- data/lib/httpx/plugins/compression/deflate.rb +0 -49
- data/lib/httpx/plugins/compression/gzip.rb +0 -88
- data/lib/httpx/plugins/compression.rb +0 -164
- data/lib/httpx/plugins/multipart/decoder.rb +0 -187
- data/lib/httpx/plugins/multipart.rb +0 -84
- data/lib/httpx/registry.rb +0 -85
- data/sig/plugins/authentication.rbs +0 -11
- data/sig/plugins/compression/brotli.rbs +0 -21
- data/sig/plugins/compression/deflate.rbs +0 -17
- data/sig/plugins/compression/gzip.rbs +0 -29
- data/sig/registry.rbs +0 -12
- /data/sig/plugins/{authentication → auth}/ntlm.rbs +0 -0
- /data/sig/plugins/{authentication → auth}/socks5.rbs +0 -0
data/lib/httpx/resolver/https.rb
CHANGED
@@ -4,12 +4,12 @@ require "resolv"
|
|
4
4
|
require "uri"
|
5
5
|
require "cgi"
|
6
6
|
require "forwardable"
|
7
|
+
require "httpx/base64"
|
7
8
|
|
8
9
|
module HTTPX
|
9
10
|
class Resolver::HTTPS < Resolver::Resolver
|
10
11
|
extend Forwardable
|
11
12
|
using URIExtensions
|
12
|
-
using StringExtensions
|
13
13
|
|
14
14
|
module DNSExtensions
|
15
15
|
refine Resolv::DNS do
|
@@ -27,7 +27,7 @@ module HTTPX
|
|
27
27
|
use_get: false,
|
28
28
|
}.freeze
|
29
29
|
|
30
|
-
def_delegators :@resolver_connection, :state, :connecting?, :to_io, :call, :close
|
30
|
+
def_delegators :@resolver_connection, :state, :connecting?, :to_io, :call, :close, :terminate
|
31
31
|
|
32
32
|
def initialize(_, options)
|
33
33
|
super
|
@@ -39,6 +39,7 @@ module HTTPX
|
|
39
39
|
@uri_addresses = nil
|
40
40
|
@resolver = Resolv::DNS.new
|
41
41
|
@resolver.timeouts = @resolver_options.fetch(:timeouts, Resolver::RESOLVE_TIMEOUT)
|
42
|
+
@resolver.lazy_initialize
|
42
43
|
end
|
43
44
|
|
44
45
|
def <<(connection)
|
@@ -49,6 +50,7 @@ module HTTPX
|
|
49
50
|
if @uri_addresses.empty?
|
50
51
|
ex = ResolveError.new("Can't resolve DNS server #{@uri.host}")
|
51
52
|
ex.set_backtrace(caller)
|
53
|
+
connection.force_reset
|
52
54
|
throw(:resolve_error, ex)
|
53
55
|
end
|
54
56
|
|
@@ -66,11 +68,14 @@ module HTTPX
|
|
66
68
|
def resolver_connection
|
67
69
|
@resolver_connection ||= @pool.find_connection(@uri, @options) || begin
|
68
70
|
@building_connection = true
|
69
|
-
connection = @options.connection_class.new(
|
71
|
+
connection = @options.connection_class.new(@uri, @options.merge(ssl: { alpn_protocols: %w[h2] }))
|
70
72
|
@pool.init_connection(connection, @options)
|
71
|
-
|
72
|
-
|
73
|
-
|
73
|
+
# only explicity emit addresses if connection didn't pre-resolve, i.e. it's not an IP.
|
74
|
+
catch(:coalesced) do
|
75
|
+
@building_connection = false
|
76
|
+
emit_addresses(connection, @family, @uri_addresses) unless connection.addresses
|
77
|
+
connection
|
78
|
+
end
|
74
79
|
end
|
75
80
|
end
|
76
81
|
|
@@ -101,8 +106,8 @@ module HTTPX
|
|
101
106
|
@requests[request] = hostname
|
102
107
|
resolver_connection.send(request)
|
103
108
|
@connections << connection
|
104
|
-
rescue ResolveError, Resolv::DNS::EncodeError
|
105
|
-
|
109
|
+
rescue ResolveError, Resolv::DNS::EncodeError => e
|
110
|
+
reset_hostname(hostname)
|
106
111
|
emit_resolve_error(connection, connection.origin.host, e)
|
107
112
|
end
|
108
113
|
end
|
@@ -111,7 +116,7 @@ module HTTPX
|
|
111
116
|
response.raise_for_status
|
112
117
|
rescue StandardError => e
|
113
118
|
hostname = @requests.delete(request)
|
114
|
-
connection =
|
119
|
+
connection = reset_hostname(hostname)
|
115
120
|
emit_resolve_error(connection, connection.origin.host, e)
|
116
121
|
else
|
117
122
|
# @type var response: HTTPX::Response
|
@@ -126,17 +131,40 @@ module HTTPX
|
|
126
131
|
end
|
127
132
|
|
128
133
|
def parse(request, response)
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
134
|
+
code, result = decode_response_body(response)
|
135
|
+
|
136
|
+
case code
|
137
|
+
when :ok
|
138
|
+
parse_addresses(result, request)
|
139
|
+
when :no_domain_found
|
140
|
+
# Indicates no such domain was found.
|
141
|
+
|
142
|
+
host = @requests.delete(request)
|
143
|
+
connection = reset_hostname(host, reset_candidates: false)
|
144
|
+
|
145
|
+
unless @queries.value?(connection)
|
146
|
+
emit_resolve_error(connection)
|
147
|
+
return
|
148
|
+
end
|
149
|
+
|
150
|
+
resolve
|
151
|
+
when :dns_error
|
152
|
+
host = @requests.delete(request)
|
153
|
+
connection = reset_hostname(host)
|
154
|
+
|
155
|
+
emit_resolve_error(connection)
|
156
|
+
when :decode_error
|
157
|
+
host = @requests.delete(request)
|
158
|
+
connection = reset_hostname(host)
|
159
|
+
emit_resolve_error(connection, connection.origin.host, result)
|
136
160
|
end
|
137
|
-
|
161
|
+
end
|
162
|
+
|
163
|
+
def parse_addresses(answers, request)
|
164
|
+
if answers.empty?
|
165
|
+
# no address found, eliminate candidates
|
138
166
|
host = @requests.delete(request)
|
139
|
-
connection =
|
167
|
+
connection = reset_hostname(host)
|
140
168
|
emit_resolve_error(connection)
|
141
169
|
return
|
142
170
|
|
@@ -147,7 +175,7 @@ module HTTPX
|
|
147
175
|
if address.key?("alias")
|
148
176
|
alias_address = answers[address["alias"]]
|
149
177
|
if alias_address.nil?
|
150
|
-
|
178
|
+
reset_hostname(address["name"])
|
151
179
|
if catch(:coalesced) { early_resolve(connection, hostname: address["alias"]) }
|
152
180
|
@connections.delete(connection)
|
153
181
|
else
|
@@ -164,7 +192,7 @@ module HTTPX
|
|
164
192
|
next if addresses.empty?
|
165
193
|
|
166
194
|
hostname.delete_suffix!(".") if hostname.end_with?(".")
|
167
|
-
connection =
|
195
|
+
connection = reset_hostname(hostname, reset_candidates: false)
|
168
196
|
next unless connection # probably a retried query for which there's an answer
|
169
197
|
|
170
198
|
@connections.delete(connection)
|
@@ -173,7 +201,7 @@ module HTTPX
|
|
173
201
|
@queries.delete_if { |_, conn| connection == conn }
|
174
202
|
|
175
203
|
Resolver.cached_lookup_set(hostname, @family, addresses) if @resolver_options[:cache]
|
176
|
-
emit_addresses(connection, @family, addresses.map { |addr| addr["data"] })
|
204
|
+
catch(:coalesced) { emit_addresses(connection, @family, addresses.map { |addr| addr["data"] }) }
|
177
205
|
end
|
178
206
|
end
|
179
207
|
return if @connections.empty?
|
@@ -193,7 +221,7 @@ module HTTPX
|
|
193
221
|
uri.query = URI.encode_www_form(params)
|
194
222
|
request = rklass.new("GET", uri, @options)
|
195
223
|
else
|
196
|
-
request = rklass.new("POST", uri, @options
|
224
|
+
request = rklass.new("POST", uri, @options, body: [payload])
|
197
225
|
request.headers["content-type"] = "application/dns-message"
|
198
226
|
end
|
199
227
|
request.headers["accept"] = "application/dns-message"
|
@@ -202,11 +230,6 @@ module HTTPX
|
|
202
230
|
|
203
231
|
def decode_response_body(response)
|
204
232
|
case response.headers["content-type"]
|
205
|
-
when "application/dns-json",
|
206
|
-
"application/json",
|
207
|
-
%r{^application/x-javascript} # because google...
|
208
|
-
payload = JSON.parse(response.to_s)
|
209
|
-
payload["Answer"]
|
210
233
|
when "application/dns-udpwireformat",
|
211
234
|
"application/dns-message"
|
212
235
|
Resolver.decode_dns_answer(response.to_s)
|
@@ -214,5 +237,17 @@ module HTTPX
|
|
214
237
|
raise Error, "unsupported DNS mime-type (#{response.headers["content-type"]})"
|
215
238
|
end
|
216
239
|
end
|
240
|
+
|
241
|
+
def reset_hostname(hostname, reset_candidates: true)
|
242
|
+
connection = @queries.delete(hostname)
|
243
|
+
|
244
|
+
return connection unless connection && reset_candidates
|
245
|
+
|
246
|
+
# eliminate other candidates
|
247
|
+
candidates = @queries.select { |_, conn| connection == conn }.keys
|
248
|
+
@queries.delete_if { |h, _| candidates.include?(h) }
|
249
|
+
|
250
|
+
connection
|
251
|
+
end
|
217
252
|
end
|
218
253
|
end
|
data/lib/httpx/resolver/multi.rb
CHANGED
@@ -6,7 +6,7 @@ require "resolv"
|
|
6
6
|
module HTTPX
|
7
7
|
class Resolver::Multi
|
8
8
|
include Callbacks
|
9
|
-
using ArrayExtensions
|
9
|
+
using ArrayExtensions::FilterMap
|
10
10
|
|
11
11
|
attr_reader :resolvers
|
12
12
|
|
@@ -46,14 +46,15 @@ module HTTPX
|
|
46
46
|
addresses = @resolver_options[:cache] && (connection.addresses || HTTPX::Resolver.nolookup_resolve(hostname))
|
47
47
|
return unless addresses
|
48
48
|
|
49
|
-
addresses
|
49
|
+
addresses.group_by(&:family).sort { |(f1, _), (f2, _)| f2 <=> f1 }.each do |family, addrs|
|
50
|
+
# try to match the resolver by family. However, there are cases where that's not possible, as when
|
51
|
+
# the system does not have IPv6 connectivity, but it does support IPv6 via loopback/link-local.
|
52
|
+
resolver = @resolvers.find { |r| r.family == family } || @resolvers.first
|
50
53
|
|
51
|
-
|
52
|
-
addrs = addresses[resolver.family]
|
54
|
+
next unless resolver # this should ever happen
|
53
55
|
|
54
|
-
|
55
|
-
|
56
|
-
resolver.emit_addresses(connection, resolver.family, addrs)
|
56
|
+
# it does not matter which resolver it is, as early-resolve code is shared.
|
57
|
+
resolver.emit_addresses(connection, family, addrs, true)
|
57
58
|
end
|
58
59
|
end
|
59
60
|
|
@@ -64,12 +65,7 @@ module HTTPX
|
|
64
65
|
end
|
65
66
|
|
66
67
|
def on_resolver_error(connection, error)
|
67
|
-
|
68
|
-
|
69
|
-
return unless @errors[connection].size >= @resolvers.size
|
70
|
-
|
71
|
-
errors = @errors.delete(connection)
|
72
|
-
emit(:error, connection, errors.first)
|
68
|
+
emit(:error, connection, error)
|
73
69
|
end
|
74
70
|
|
75
71
|
def on_resolver_close(resolver)
|
@@ -8,32 +8,12 @@ module HTTPX
|
|
8
8
|
extend Forwardable
|
9
9
|
using URIExtensions
|
10
10
|
|
11
|
-
DEFAULTS =
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
else
|
18
|
-
{
|
19
|
-
nameserver: nil,
|
20
|
-
**Resolv::DNS::Config.default_config_hash,
|
21
|
-
packet_size: 512,
|
22
|
-
timeouts: Resolver::RESOLVE_TIMEOUT,
|
23
|
-
}.freeze
|
24
|
-
end
|
25
|
-
|
26
|
-
# nameservers for ipv6 are misconfigured in certain systems;
|
27
|
-
# this can use an unexpected endless loop
|
28
|
-
# https://gitlab.com/honeyryderchuck/httpx/issues/56
|
29
|
-
DEFAULTS[:nameserver].select! do |nameserver|
|
30
|
-
begin
|
31
|
-
IPAddr.new(nameserver)
|
32
|
-
true
|
33
|
-
rescue IPAddr::InvalidAddressError
|
34
|
-
false
|
35
|
-
end
|
36
|
-
end if DEFAULTS[:nameserver]
|
11
|
+
DEFAULTS = {
|
12
|
+
nameserver: nil,
|
13
|
+
**Resolv::DNS::Config.default_config_hash,
|
14
|
+
packet_size: 512,
|
15
|
+
timeouts: Resolver::RESOLVE_TIMEOUT,
|
16
|
+
}.freeze
|
37
17
|
|
38
18
|
DNS_PORT = 53
|
39
19
|
|
@@ -41,12 +21,16 @@ module HTTPX
|
|
41
21
|
|
42
22
|
attr_reader :state
|
43
23
|
|
44
|
-
def initialize(
|
24
|
+
def initialize(family, options)
|
45
25
|
super
|
46
26
|
@ns_index = 0
|
47
27
|
@resolver_options = DEFAULTS.merge(@options.resolver_options)
|
48
|
-
@
|
49
|
-
@
|
28
|
+
@socket_type = @resolver_options.fetch(:socket_type, :udp)
|
29
|
+
@nameserver = if (nameserver = @resolver_options[:nameserver])
|
30
|
+
nameserver = nameserver[family] if nameserver.is_a?(Hash)
|
31
|
+
Array(nameserver)
|
32
|
+
end
|
33
|
+
@ndots = @resolver_options.fetch(:ndots, 1)
|
50
34
|
@search = Array(@resolver_options[:search]).map { |srch| srch.scan(/[^.]+/) }
|
51
35
|
@_timeouts = Array(@resolver_options[:timeouts])
|
52
36
|
@timeouts = Hash.new { |timeouts, host| timeouts[host] = @_timeouts.dup }
|
@@ -77,9 +61,11 @@ module HTTPX
|
|
77
61
|
nil
|
78
62
|
rescue Errno::EHOSTUNREACH => e
|
79
63
|
@ns_index += 1
|
80
|
-
|
64
|
+
nameserver = @nameserver
|
65
|
+
if nameserver && @ns_index < nameserver.size
|
81
66
|
log { "resolver: failed resolving on nameserver #{@nameserver[@ns_index - 1]} (#{e.message})" }
|
82
67
|
transition(:idle)
|
68
|
+
@timeouts.clear
|
83
69
|
else
|
84
70
|
handle_error(e)
|
85
71
|
end
|
@@ -103,6 +89,7 @@ module HTTPX
|
|
103
89
|
if @nameserver.nil?
|
104
90
|
ex = ResolveError.new("No available nameserver")
|
105
91
|
ex.set_backtrace(caller)
|
92
|
+
connection.force_reset
|
106
93
|
throw(:resolve_error, ex)
|
107
94
|
else
|
108
95
|
@connections << connection
|
@@ -118,6 +105,10 @@ module HTTPX
|
|
118
105
|
@timeouts.values_at(*hosts).reject(&:empty?).map(&:first).min
|
119
106
|
end
|
120
107
|
|
108
|
+
def handle_socket_timeout(interval)
|
109
|
+
do_retry(interval)
|
110
|
+
end
|
111
|
+
|
121
112
|
private
|
122
113
|
|
123
114
|
def calculate_interests
|
@@ -134,10 +125,10 @@ module HTTPX
|
|
134
125
|
dwrite if calculate_interests == :w
|
135
126
|
end
|
136
127
|
|
137
|
-
def do_retry
|
128
|
+
def do_retry(loop_time = nil)
|
138
129
|
return if @queries.empty? || !@start_timeout
|
139
130
|
|
140
|
-
loop_time
|
131
|
+
loop_time ||= Utils.elapsed_time(@start_timeout)
|
141
132
|
|
142
133
|
query = @queries.first
|
143
134
|
|
@@ -147,12 +138,26 @@ module HTTPX
|
|
147
138
|
host = connection.origin.host
|
148
139
|
timeout = (@timeouts[host][0] -= loop_time)
|
149
140
|
|
150
|
-
return unless timeout
|
141
|
+
return unless timeout <= 0
|
151
142
|
|
152
143
|
@timeouts[host].shift
|
153
|
-
|
144
|
+
|
145
|
+
if !@timeouts[host].empty?
|
146
|
+
log { "resolver: timeout after #{timeout}s, retry(#{@timeouts[host].first}) #{host}..." }
|
147
|
+
# must downgrade to tcp AND retry on same host as last
|
148
|
+
downgrade_socket
|
149
|
+
resolve(connection, h)
|
150
|
+
elsif @ns_index + 1 < @nameserver.size
|
151
|
+
# try on the next nameserver
|
152
|
+
@ns_index += 1
|
153
|
+
log { "resolver: failed resolving #{host} on nameserver #{@nameserver[@ns_index - 1]} (timeout error)" }
|
154
|
+
transition(:idle)
|
155
|
+
@timeouts.clear
|
156
|
+
resolve(connection, h)
|
157
|
+
else
|
158
|
+
|
154
159
|
@timeouts.delete(host)
|
155
|
-
|
160
|
+
reset_hostname(h, reset_candidates: false)
|
156
161
|
|
157
162
|
return unless @queries.empty?
|
158
163
|
|
@@ -160,18 +165,54 @@ module HTTPX
|
|
160
165
|
# This loop_time passed to the exception is bogus. Ideally we would pass the total
|
161
166
|
# resolve timeout, including from the previous retries.
|
162
167
|
raise ResolveTimeoutError.new(loop_time, "Timed out while resolving #{connection.origin.host}")
|
163
|
-
else
|
164
|
-
log { "resolver: timeout after #{timeout}s, retry(#{@timeouts[host].first}) #{host}..." }
|
165
|
-
resolve(connection)
|
166
168
|
end
|
167
169
|
end
|
168
170
|
|
169
171
|
def dread(wsize = @resolver_options[:packet_size])
|
170
172
|
loop do
|
173
|
+
wsize = @large_packet.capacity if @large_packet
|
174
|
+
|
171
175
|
siz = @io.read(wsize, @read_buffer)
|
172
|
-
return unless siz && siz.positive?
|
173
176
|
|
174
|
-
|
177
|
+
unless siz
|
178
|
+
ex = EOFError.new("descriptor closed")
|
179
|
+
ex.set_backtrace(caller)
|
180
|
+
raise ex
|
181
|
+
end
|
182
|
+
|
183
|
+
return unless siz.positive?
|
184
|
+
|
185
|
+
if @socket_type == :tcp
|
186
|
+
# packet may be incomplete, need to keep draining from the socket
|
187
|
+
if @large_packet
|
188
|
+
# large packet buffer already exists, continue pumping
|
189
|
+
@large_packet << @read_buffer
|
190
|
+
|
191
|
+
next unless @large_packet.full?
|
192
|
+
|
193
|
+
parse(@large_packet.to_s)
|
194
|
+
@large_packet = nil
|
195
|
+
# downgrade to udp again
|
196
|
+
downgrade_socket
|
197
|
+
return
|
198
|
+
else
|
199
|
+
size = @read_buffer[0, 2].unpack1("n")
|
200
|
+
buffer = @read_buffer.byteslice(2..-1)
|
201
|
+
|
202
|
+
if size > @read_buffer.bytesize
|
203
|
+
# only do buffer logic if it's worth it, and the whole packet isn't here already
|
204
|
+
@large_packet = Buffer.new(size)
|
205
|
+
@large_packet << buffer
|
206
|
+
|
207
|
+
next
|
208
|
+
else
|
209
|
+
parse(buffer)
|
210
|
+
end
|
211
|
+
end
|
212
|
+
else # udp
|
213
|
+
parse(@read_buffer)
|
214
|
+
end
|
215
|
+
|
175
216
|
return if @state == :closed
|
176
217
|
end
|
177
218
|
end
|
@@ -181,34 +222,68 @@ module HTTPX
|
|
181
222
|
return if @write_buffer.empty?
|
182
223
|
|
183
224
|
siz = @io.write(@write_buffer)
|
184
|
-
|
225
|
+
|
226
|
+
unless siz
|
227
|
+
ex = EOFError.new("descriptor closed")
|
228
|
+
ex.set_backtrace(caller)
|
229
|
+
raise ex
|
230
|
+
end
|
231
|
+
|
232
|
+
return unless siz.positive?
|
185
233
|
|
186
234
|
return if @state == :closed
|
187
235
|
end
|
188
236
|
end
|
189
237
|
|
190
238
|
def parse(buffer)
|
191
|
-
|
192
|
-
addresses = Resolver.decode_dns_answer(buffer)
|
193
|
-
rescue Resolv::DNS::DecodeError => e
|
194
|
-
hostname, connection = @queries.first
|
195
|
-
@queries.delete(hostname)
|
196
|
-
@timeouts.delete(hostname)
|
197
|
-
@connections.delete(connection)
|
198
|
-
ex = NativeResolveError.new(connection, connection.origin.host, e.message)
|
199
|
-
ex.set_backtrace(e.backtrace)
|
200
|
-
raise ex
|
201
|
-
end
|
239
|
+
code, result = Resolver.decode_dns_answer(buffer)
|
202
240
|
|
203
|
-
|
241
|
+
case code
|
242
|
+
when :ok
|
243
|
+
parse_addresses(result)
|
244
|
+
when :no_domain_found
|
245
|
+
# Indicates no such domain was found.
|
204
246
|
hostname, connection = @queries.first
|
205
|
-
|
206
|
-
@timeouts.delete(hostname)
|
247
|
+
reset_hostname(hostname, reset_candidates: false)
|
207
248
|
|
208
249
|
unless @queries.value?(connection)
|
209
250
|
@connections.delete(connection)
|
210
|
-
raise NativeResolveError.new(connection, connection.origin.host)
|
251
|
+
raise NativeResolveError.new(connection, connection.origin.host, "name or service not known")
|
211
252
|
end
|
253
|
+
|
254
|
+
resolve
|
255
|
+
when :message_truncated
|
256
|
+
# TODO: what to do if it's already tcp??
|
257
|
+
return if @socket_type == :tcp
|
258
|
+
|
259
|
+
@socket_type = :tcp
|
260
|
+
|
261
|
+
hostname, _ = @queries.first
|
262
|
+
reset_hostname(hostname)
|
263
|
+
transition(:closed)
|
264
|
+
when :dns_error
|
265
|
+
hostname, connection = @queries.first
|
266
|
+
reset_hostname(hostname)
|
267
|
+
@connections.delete(connection)
|
268
|
+
ex = NativeResolveError.new(connection, connection.origin.host, "unknown DNS error (error code #{result})")
|
269
|
+
raise ex
|
270
|
+
when :decode_error
|
271
|
+
hostname, connection = @queries.first
|
272
|
+
reset_hostname(hostname)
|
273
|
+
@connections.delete(connection)
|
274
|
+
ex = NativeResolveError.new(connection, connection.origin.host, result.message)
|
275
|
+
ex.set_backtrace(result.backtrace)
|
276
|
+
raise ex
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
def parse_addresses(addresses)
|
281
|
+
if addresses.empty?
|
282
|
+
# no address found, eliminate candidates
|
283
|
+
hostname, connection = @queries.first
|
284
|
+
reset_hostname(hostname)
|
285
|
+
@connections.delete(connection)
|
286
|
+
raise NativeResolveError.new(connection, connection.origin.host)
|
212
287
|
else
|
213
288
|
address = addresses.first
|
214
289
|
name = address["name"]
|
@@ -216,36 +291,45 @@ module HTTPX
|
|
216
291
|
connection = @queries.delete(name)
|
217
292
|
|
218
293
|
unless connection
|
294
|
+
orig_name = name
|
219
295
|
# absolute name
|
220
296
|
name_labels = Resolv::DNS::Name.create(name).to_a
|
221
|
-
name = @queries.
|
297
|
+
name = @queries.each_key.first { |hname| name_labels == Resolv::DNS::Name.create(hname).to_a }
|
222
298
|
|
223
299
|
# probably a retried query for which there's an answer
|
224
|
-
|
300
|
+
unless name
|
301
|
+
@timeouts.delete(orig_name)
|
302
|
+
return
|
303
|
+
end
|
225
304
|
|
226
305
|
address["name"] = name
|
227
306
|
connection = @queries.delete(name)
|
228
307
|
end
|
229
308
|
|
230
|
-
# eliminate other candidates
|
231
|
-
@queries.delete_if { |_, conn| connection == conn }
|
232
|
-
|
233
309
|
if address.key?("alias") # CNAME
|
310
|
+
hostname_alias = address["alias"]
|
234
311
|
# clean up intermediate queries
|
235
312
|
@timeouts.delete(name) unless connection.origin.host == name
|
236
313
|
|
237
|
-
if catch(:coalesced) { early_resolve(connection, hostname:
|
314
|
+
if catch(:coalesced) { early_resolve(connection, hostname: hostname_alias) }
|
238
315
|
@connections.delete(connection)
|
239
316
|
else
|
240
|
-
|
317
|
+
if @socket_type == :tcp
|
318
|
+
# must downgrade to udp if tcp
|
319
|
+
@socket_type = @resolver_options.fetch(:socket_type, :udp)
|
320
|
+
transition(:idle)
|
321
|
+
transition(:open)
|
322
|
+
end
|
323
|
+
log { "resolver: ALIAS #{hostname_alias} for #{name}" }
|
324
|
+
resolve(connection, hostname_alias)
|
241
325
|
return
|
242
326
|
end
|
243
327
|
else
|
244
|
-
|
328
|
+
reset_hostname(name, connection: connection)
|
245
329
|
@timeouts.delete(connection.origin.host)
|
246
330
|
@connections.delete(connection)
|
247
331
|
Resolver.cached_lookup_set(connection.origin.host, @family, addresses) if @resolver_options[:cache]
|
248
|
-
emit_addresses(connection, @family, addresses.map { |addr| addr["data"] })
|
332
|
+
catch(:coalesced) { emit_addresses(connection, @family, addresses.map { |addr| addr["data"] }) }
|
249
333
|
end
|
250
334
|
end
|
251
335
|
return emit(:close) if @connections.empty?
|
@@ -255,6 +339,7 @@ module HTTPX
|
|
255
339
|
|
256
340
|
def resolve(connection = @connections.first, hostname = nil)
|
257
341
|
raise Error, "no URI to resolve" unless connection
|
342
|
+
|
258
343
|
return unless @write_buffer.empty?
|
259
344
|
|
260
345
|
hostname ||= @queries.key(connection)
|
@@ -271,12 +356,19 @@ module HTTPX
|
|
271
356
|
end
|
272
357
|
log { "resolver: query #{@record_type.name.split("::").last} for #{hostname}" }
|
273
358
|
begin
|
274
|
-
@write_buffer <<
|
359
|
+
@write_buffer << encode_dns_query(hostname)
|
275
360
|
rescue Resolv::DNS::EncodeError => e
|
276
361
|
emit_resolve_error(connection, hostname, e)
|
277
362
|
end
|
278
363
|
end
|
279
364
|
|
365
|
+
def encode_dns_query(hostname)
|
366
|
+
message_id = Resolver.generate_id
|
367
|
+
msg = Resolver.encode_dns_query(hostname, type: @record_type, message_id: message_id)
|
368
|
+
msg[0, 2] = [msg.size, message_id].pack("nn") if @socket_type == :tcp
|
369
|
+
msg
|
370
|
+
end
|
371
|
+
|
280
372
|
def generate_candidates(name)
|
281
373
|
return [name] if name.end_with?(".")
|
282
374
|
|
@@ -284,21 +376,33 @@ module HTTPX
|
|
284
376
|
name_parts = name.scan(/[^.]+/)
|
285
377
|
candidates = [name] if @ndots <= name_parts.size - 1
|
286
378
|
candidates.concat(@search.map { |domain| [*name_parts, *domain].join(".") })
|
287
|
-
|
379
|
+
fname = "#{name}."
|
380
|
+
candidates << fname unless candidates.include?(fname)
|
288
381
|
|
289
382
|
candidates
|
290
383
|
end
|
291
384
|
|
292
385
|
def build_socket
|
293
|
-
return if @io
|
294
|
-
|
295
386
|
ip, port = @nameserver[@ns_index]
|
296
387
|
port ||= DNS_PORT
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
388
|
+
|
389
|
+
case @socket_type
|
390
|
+
when :udp
|
391
|
+
log { "resolver: server: udp://#{ip}:#{port}..." }
|
392
|
+
UDP.new(ip, port, @options)
|
393
|
+
when :tcp
|
394
|
+
log { "resolver: server: tcp://#{ip}:#{port}..." }
|
395
|
+
origin = URI("tcp://#{ip}:#{port}")
|
396
|
+
TCP.new(origin, [ip], @options)
|
397
|
+
end
|
398
|
+
end
|
399
|
+
|
400
|
+
def downgrade_socket
|
401
|
+
return unless @socket_type == :tcp
|
402
|
+
|
403
|
+
@socket_type = @resolver_options.fetch(:socket_type, :udp)
|
404
|
+
transition(:idle)
|
405
|
+
transition(:open)
|
302
406
|
end
|
303
407
|
|
304
408
|
def transition(nextstate)
|
@@ -308,11 +412,10 @@ module HTTPX
|
|
308
412
|
@io.close
|
309
413
|
@io = nil
|
310
414
|
end
|
311
|
-
@timeouts.clear
|
312
415
|
when :open
|
313
416
|
return unless @state == :idle
|
314
417
|
|
315
|
-
build_socket
|
418
|
+
@io ||= build_socket
|
316
419
|
|
317
420
|
@io.connect
|
318
421
|
return unless @io.connected?
|
@@ -339,5 +442,18 @@ module HTTPX
|
|
339
442
|
end
|
340
443
|
end
|
341
444
|
end
|
445
|
+
|
446
|
+
def reset_hostname(hostname, connection: @queries.delete(hostname), reset_candidates: true)
|
447
|
+
@timeouts.delete(hostname)
|
448
|
+
@timeouts.delete(hostname)
|
449
|
+
|
450
|
+
return unless connection && reset_candidates
|
451
|
+
|
452
|
+
# eliminate other candidates
|
453
|
+
candidates = @queries.select { |_, conn| connection == conn }.keys
|
454
|
+
@queries.delete_if { |h, _| candidates.include?(h) }
|
455
|
+
# reset timeouts
|
456
|
+
@timeouts.delete_if { |h, _| candidates.include?(h) }
|
457
|
+
end
|
342
458
|
end
|
343
459
|
end
|