httpx 0.20.0 → 1.3.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|