httpx 1.2.6 → 1.4.4
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/README.md +1 -2
- data/doc/release_notes/1_3_0.md +18 -0
- data/doc/release_notes/1_3_1.md +17 -0
- data/doc/release_notes/1_3_2.md +6 -0
- data/doc/release_notes/1_3_3.md +5 -0
- data/doc/release_notes/1_3_4.md +6 -0
- data/doc/release_notes/1_4_0.md +43 -0
- data/doc/release_notes/1_4_1.md +19 -0
- data/doc/release_notes/1_4_2.md +20 -0
- data/doc/release_notes/1_4_3.md +11 -0
- data/doc/release_notes/1_4_4.md +14 -0
- data/lib/httpx/adapters/datadog.rb +56 -80
- data/lib/httpx/adapters/faraday.rb +5 -2
- data/lib/httpx/adapters/webmock.rb +24 -8
- data/lib/httpx/callbacks.rb +2 -7
- data/lib/httpx/chainable.rb +3 -1
- data/lib/httpx/connection/http1.rb +11 -7
- data/lib/httpx/connection/http2.rb +57 -34
- data/lib/httpx/connection.rb +270 -71
- data/lib/httpx/errors.rb +15 -4
- data/lib/httpx/io/ssl.rb +6 -3
- data/lib/httpx/io/tcp.rb +1 -1
- data/lib/httpx/io/unix.rb +1 -1
- data/lib/httpx/loggable.rb +17 -10
- data/lib/httpx/options.rb +30 -23
- data/lib/httpx/plugins/aws_sdk_authentication.rb +3 -0
- data/lib/httpx/plugins/aws_sigv4.rb +36 -17
- data/lib/httpx/plugins/callbacks.rb +13 -2
- data/lib/httpx/plugins/circuit_breaker.rb +11 -5
- data/lib/httpx/plugins/content_digest.rb +202 -0
- data/lib/httpx/plugins/cookies.rb +9 -6
- data/lib/httpx/plugins/digest_auth.rb +3 -0
- data/lib/httpx/plugins/expect.rb +10 -4
- data/lib/httpx/plugins/follow_redirects.rb +68 -33
- data/lib/httpx/plugins/grpc/grpc_encoding.rb +2 -0
- data/lib/httpx/plugins/grpc.rb +2 -2
- data/lib/httpx/plugins/h2c.rb +23 -20
- data/lib/httpx/plugins/internal_telemetry.rb +48 -1
- data/lib/httpx/plugins/oauth.rb +1 -1
- data/lib/httpx/plugins/persistent.rb +16 -0
- data/lib/httpx/plugins/proxy/http.rb +19 -16
- data/lib/httpx/plugins/proxy/socks4.rb +1 -1
- data/lib/httpx/plugins/proxy/socks5.rb +1 -1
- data/lib/httpx/plugins/proxy.rb +96 -85
- data/lib/httpx/plugins/retries.rb +28 -10
- data/lib/httpx/plugins/ssrf_filter.rb +4 -1
- data/lib/httpx/plugins/stream.rb +42 -18
- data/lib/httpx/plugins/upgrade.rb +5 -10
- data/lib/httpx/plugins/webdav.rb +6 -0
- data/lib/httpx/plugins/xml.rb +76 -0
- data/lib/httpx/pool.rb +73 -244
- data/lib/httpx/request/body.rb +50 -55
- data/lib/httpx/request.rb +77 -14
- data/lib/httpx/resolver/https.rb +17 -20
- data/lib/httpx/resolver/multi.rb +34 -16
- data/lib/httpx/resolver/native.rb +140 -61
- data/lib/httpx/resolver/resolver.rb +64 -19
- data/lib/httpx/resolver/system.rb +32 -16
- data/lib/httpx/resolver.rb +21 -14
- data/lib/httpx/response/body.rb +12 -1
- data/lib/httpx/response.rb +16 -9
- data/lib/httpx/selector.rb +170 -91
- data/lib/httpx/session.rb +282 -139
- data/lib/httpx/timers.rb +17 -2
- data/lib/httpx/transcoder/body.rb +15 -29
- data/lib/httpx/transcoder/form.rb +2 -0
- data/lib/httpx/transcoder/gzip.rb +0 -3
- data/lib/httpx/transcoder/json.rb +16 -2
- data/lib/httpx/transcoder/multipart/encoder.rb +11 -2
- data/lib/httpx/transcoder/multipart/part.rb +1 -1
- data/lib/httpx/transcoder/utils/deflater.rb +7 -4
- data/lib/httpx/transcoder.rb +0 -1
- data/lib/httpx/version.rb +1 -1
- data/lib/httpx.rb +20 -21
- data/sig/callbacks.rbs +2 -3
- data/sig/chainable.rbs +6 -2
- data/sig/connection/http1.rbs +2 -2
- data/sig/connection/http2.rbs +22 -18
- data/sig/connection.rbs +40 -9
- data/sig/errors.rbs +9 -3
- data/sig/httpx.rbs +3 -3
- data/sig/io/tcp.rbs +1 -1
- data/sig/io/unix.rbs +1 -1
- data/sig/loggable.rbs +4 -2
- data/sig/options.rbs +8 -13
- data/sig/plugins/aws_sigv4.rbs +8 -2
- data/sig/plugins/content_digest.rbs +51 -0
- data/sig/plugins/cookies/cookie.rbs +9 -0
- data/sig/plugins/follow_redirects.rbs +1 -1
- data/sig/plugins/grpc/call.rbs +4 -0
- data/sig/plugins/persistent.rbs +4 -1
- data/sig/plugins/proxy/http.rbs +3 -0
- data/sig/plugins/proxy/socks5.rbs +11 -3
- data/sig/plugins/proxy.rbs +18 -9
- data/sig/plugins/push_promise.rbs +6 -3
- data/sig/plugins/rate_limiter.rbs +2 -0
- data/sig/plugins/retries.rbs +1 -1
- data/sig/plugins/ssrf_filter.rbs +26 -0
- data/sig/plugins/stream.rbs +3 -0
- data/sig/plugins/webdav.rbs +23 -0
- data/sig/plugins/xml.rbs +37 -0
- data/sig/pool.rbs +27 -33
- data/sig/request/body.rbs +4 -10
- data/sig/request.rbs +14 -1
- data/sig/resolver/multi.rbs +26 -1
- data/sig/resolver/native.rbs +6 -3
- data/sig/resolver/resolver.rbs +22 -3
- data/sig/resolver.rbs +5 -1
- data/sig/response/body.rbs +2 -2
- data/sig/response/buffer.rbs +2 -2
- data/sig/response.rbs +9 -4
- data/sig/selector.rbs +31 -4
- data/sig/session.rbs +54 -20
- data/sig/timers.rbs +15 -4
- data/sig/transcoder/body.rbs +2 -4
- data/sig/transcoder/chunker.rbs +1 -1
- data/sig/transcoder/deflate.rbs +1 -0
- data/sig/transcoder/form.rbs +8 -0
- data/sig/transcoder/gzip.rbs +4 -1
- data/sig/transcoder/json.rbs +1 -1
- data/sig/transcoder/multipart.rbs +6 -4
- data/sig/transcoder/utils/body_reader.rbs +3 -3
- data/sig/transcoder/utils/deflater.rbs +2 -3
- metadata +32 -14
- data/lib/httpx/session2.rb +0 -23
- data/lib/httpx/transcoder/utils/inflater.rb +0 -19
- data/lib/httpx/transcoder/xml.rb +0 -52
- data/sig/transcoder/utils/inflater.rbs +0 -12
- data/sig/transcoder/xml.rbs +0 -22
data/lib/httpx/options.rb
CHANGED
@@ -25,14 +25,14 @@ module HTTPX
|
|
25
25
|
end
|
26
26
|
rescue NotImplementedError
|
27
27
|
[Socket::AF_INET]
|
28
|
-
end
|
28
|
+
end.freeze
|
29
29
|
|
30
30
|
DEFAULT_OPTIONS = {
|
31
31
|
:max_requests => Float::INFINITY,
|
32
|
-
:debug =>
|
32
|
+
:debug => nil,
|
33
33
|
:debug_level => (ENV["HTTPX_DEBUG"] || 1).to_i,
|
34
|
-
:ssl =>
|
35
|
-
:http2_settings => { settings_enable_push: 0 },
|
34
|
+
:ssl => EMPTY_HASH,
|
35
|
+
:http2_settings => { settings_enable_push: 0 }.freeze,
|
36
36
|
:fallback_protocol => "http/1.1",
|
37
37
|
:supported_compression_formats => %w[gzip deflate],
|
38
38
|
:decompress_response_body => true,
|
@@ -56,13 +56,15 @@ module HTTPX
|
|
56
56
|
:response_class => Class.new(Response),
|
57
57
|
:request_body_class => Class.new(Request::Body),
|
58
58
|
:response_body_class => Class.new(Response::Body),
|
59
|
+
:pool_class => Class.new(Pool),
|
59
60
|
:connection_class => Class.new(Connection),
|
60
61
|
:options_class => Class.new(self),
|
61
62
|
:transport => nil,
|
62
63
|
:addresses => nil,
|
63
64
|
:persistent => false,
|
64
65
|
:resolver_class => (ENV["HTTPX_RESOLVER"] || :native).to_sym,
|
65
|
-
:resolver_options => { cache: true },
|
66
|
+
:resolver_options => { cache: true }.freeze,
|
67
|
+
:pool_options => EMPTY_HASH,
|
66
68
|
:ip_families => ip_address_families,
|
67
69
|
}.freeze
|
68
70
|
|
@@ -91,7 +93,7 @@ module HTTPX
|
|
91
93
|
# :debug :: an object which log messages are written to (must respond to <tt><<</tt>)
|
92
94
|
# :debug_level :: the log level of messages (can be 1, 2, or 3).
|
93
95
|
# :ssl :: a hash of options which can be set as params of OpenSSL::SSL::SSLContext (see HTTPX::IO::SSL)
|
94
|
-
# :http2_settings :: a hash of options to be passed to a
|
96
|
+
# :http2_settings :: a hash of options to be passed to a HTTP2::Connection (ex: <tt>{ max_concurrent_streams: 2 }</tt>)
|
95
97
|
# :fallback_protocol :: version of HTTP protocol to use by default in the absence of protocol negotiation
|
96
98
|
# like ALPN (defaults to <tt>"http/1.1"</tt>)
|
97
99
|
# :supported_compression_formats :: list of compressions supported by the transcoder layer (defaults to <tt>%w[gzip deflate]</tt>).
|
@@ -110,6 +112,7 @@ module HTTPX
|
|
110
112
|
# :request_body_class :: class used to instantiate a request body
|
111
113
|
# :response_body_class :: class used to instantiate a response body
|
112
114
|
# :connection_class :: class used to instantiate connections
|
115
|
+
# :pool_class :: class used to instantiate the session connection pool
|
113
116
|
# :options_class :: class used to instantiate options
|
114
117
|
# :transport :: type of transport to use (set to "unix" for UNIX sockets)
|
115
118
|
# :addresses :: bucket of peer addresses (can be a list of IP addresses, a hash of domain to list of adddresses;
|
@@ -118,16 +121,13 @@ module HTTPX
|
|
118
121
|
# :persistent :: whether to persist connections in between requests (defaults to <tt>true</tt>)
|
119
122
|
# :resolver_class :: which resolver to use (defaults to <tt>:native</tt>, can also be <tt>:system<tt> for
|
120
123
|
# using getaddrinfo or <tt>:https</tt> for DoH resolver, or a custom class)
|
121
|
-
# :resolver_options :: hash of options passed to the resolver
|
124
|
+
# :resolver_options :: hash of options passed to the resolver. Accepted keys depend on the resolver type.
|
125
|
+
# :pool_options :: hash of options passed to the connection pool (See Pool#initialize).
|
122
126
|
# :ip_families :: which socket families are supported (system-dependent)
|
123
127
|
# :origin :: HTTP origin to set on requests with relative path (ex: "https://api.serv.com")
|
124
128
|
# :base_path :: path to prefix given relative paths with (ex: "/v2")
|
125
129
|
# :max_concurrent_requests :: max number of requests which can be set concurrently
|
126
130
|
# :max_requests :: max number of requests which can be made on socket before it reconnects.
|
127
|
-
# :params :: hash or array of key-values which will be encoded and set in the query string of request uris.
|
128
|
-
# :form :: hash of array of key-values which will be form-or-multipart-encoded in requests body payload.
|
129
|
-
# :json :: hash of array of key-values which will be JSON-encoded in requests body payload.
|
130
|
-
# :xml :: Nokogiri XML nodes which will be encoded in requests body payload.
|
131
131
|
#
|
132
132
|
# This list of options are enhanced with each loaded plugin, see the plugin docs for details.
|
133
133
|
def initialize(options = {})
|
@@ -216,19 +216,21 @@ module HTTPX
|
|
216
216
|
end
|
217
217
|
|
218
218
|
%i[
|
219
|
-
|
219
|
+
ssl http2_settings
|
220
220
|
request_class response_class headers_class request_body_class
|
221
221
|
response_body_class connection_class options_class
|
222
|
+
pool_class pool_options
|
222
223
|
io fallback_protocol debug debug_level resolver_class resolver_options
|
223
224
|
compress_request_body decompress_response_body
|
224
225
|
persistent
|
225
226
|
].each do |method_name|
|
226
227
|
class_eval(<<-OUT, __FILE__, __LINE__ + 1)
|
228
|
+
# sets +v+ as the value of #{method_name}
|
227
229
|
def option_#{method_name}(v); v; end # def option_smth(v); v; end
|
228
230
|
OUT
|
229
231
|
end
|
230
232
|
|
231
|
-
REQUEST_BODY_IVARS = %i[@headers
|
233
|
+
REQUEST_BODY_IVARS = %i[@headers].freeze
|
232
234
|
|
233
235
|
def ==(other)
|
234
236
|
super || options_equals?(other)
|
@@ -249,14 +251,6 @@ module HTTPX
|
|
249
251
|
end
|
250
252
|
end
|
251
253
|
|
252
|
-
OTHER_LOOKUP = ->(obj, k, ivar_map) {
|
253
|
-
case obj
|
254
|
-
when Hash
|
255
|
-
obj[ivar_map[k]]
|
256
|
-
else
|
257
|
-
obj.instance_variable_get(k)
|
258
|
-
end
|
259
|
-
}
|
260
254
|
def merge(other)
|
261
255
|
ivar_map = nil
|
262
256
|
other_ivars = case other
|
@@ -269,12 +263,12 @@ module HTTPX
|
|
269
263
|
|
270
264
|
return self if other_ivars.empty?
|
271
265
|
|
272
|
-
return self if other_ivars.all? { |ivar| instance_variable_get(ivar) ==
|
266
|
+
return self if other_ivars.all? { |ivar| instance_variable_get(ivar) == access_option(other, ivar, ivar_map) }
|
273
267
|
|
274
268
|
opts = dup
|
275
269
|
|
276
270
|
other_ivars.each do |ivar|
|
277
|
-
v =
|
271
|
+
v = access_option(other, ivar, ivar_map)
|
278
272
|
|
279
273
|
unless v
|
280
274
|
opts.instance_variable_set(ivar, v)
|
@@ -325,6 +319,10 @@ module HTTPX
|
|
325
319
|
@response_body_class.__send__(:include, pl::ResponseBodyMethods) if defined?(pl::ResponseBodyMethods)
|
326
320
|
@response_body_class.extend(pl::ResponseBodyClassMethods) if defined?(pl::ResponseBodyClassMethods)
|
327
321
|
end
|
322
|
+
if defined?(pl::PoolMethods)
|
323
|
+
@pool_class = @pool_class.dup
|
324
|
+
@pool_class.__send__(:include, pl::PoolMethods)
|
325
|
+
end
|
328
326
|
if defined?(pl::ConnectionMethods)
|
329
327
|
@connection_class = @connection_class.dup
|
330
328
|
@connection_class.__send__(:include, pl::ConnectionMethods)
|
@@ -349,5 +347,14 @@ module HTTPX
|
|
349
347
|
instance_variable_set(:"@#{k}", value)
|
350
348
|
end
|
351
349
|
end
|
350
|
+
|
351
|
+
def access_option(obj, k, ivar_map)
|
352
|
+
case obj
|
353
|
+
when Hash
|
354
|
+
obj[ivar_map[k]]
|
355
|
+
else
|
356
|
+
obj.instance_variable_get(k)
|
357
|
+
end
|
358
|
+
end
|
352
359
|
end
|
353
360
|
end
|
@@ -12,6 +12,7 @@ module HTTPX
|
|
12
12
|
module AWSSigV4
|
13
13
|
Credentials = Struct.new(:username, :password, :security_token)
|
14
14
|
|
15
|
+
# Signs requests using the AWS sigv4 signing.
|
15
16
|
class Signer
|
16
17
|
def initialize(
|
17
18
|
service:,
|
@@ -88,7 +89,7 @@ module HTTPX
|
|
88
89
|
sts = "#{algo_line}" \
|
89
90
|
"\n#{datetime}" \
|
90
91
|
"\n#{credential_scope}" \
|
91
|
-
"\n#{hexdigest(creq)}"
|
92
|
+
"\n#{OpenSSL::Digest.new(@algorithm).hexdigest(creq)}"
|
92
93
|
|
93
94
|
# signature
|
94
95
|
k_date = hmac("#{upper_provider_prefix}#{@credentials.password}", date)
|
@@ -109,22 +110,38 @@ module HTTPX
|
|
109
110
|
private
|
110
111
|
|
111
112
|
def hexdigest(value)
|
112
|
-
|
113
|
-
# files, pathnames
|
114
|
-
OpenSSL::Digest.new(@algorithm).file(value.to_path).hexdigest
|
115
|
-
elsif value.respond_to?(:each)
|
116
|
-
digest = OpenSSL::Digest.new(@algorithm)
|
117
|
-
|
118
|
-
mb_buffer = value.each.with_object("".b) do |chunk, buffer|
|
119
|
-
buffer << chunk
|
120
|
-
break if buffer.bytesize >= 1024 * 1024
|
121
|
-
end
|
113
|
+
digest = OpenSSL::Digest.new(@algorithm)
|
122
114
|
|
123
|
-
|
124
|
-
value.
|
125
|
-
|
115
|
+
if value.respond_to?(:read)
|
116
|
+
if value.respond_to?(:to_path)
|
117
|
+
# files, pathnames
|
118
|
+
digest.file(value.to_path).hexdigest
|
119
|
+
else
|
120
|
+
# gzipped request bodies
|
121
|
+
raise Error, "request body must be rewindable" unless value.respond_to?(:rewind)
|
122
|
+
|
123
|
+
buffer = Tempfile.new("httpx", encoding: Encoding::BINARY, mode: File::RDWR)
|
124
|
+
begin
|
125
|
+
IO.copy_stream(value, buffer)
|
126
|
+
buffer.flush
|
127
|
+
|
128
|
+
digest.file(buffer.to_path).hexdigest
|
129
|
+
ensure
|
130
|
+
value.rewind
|
131
|
+
buffer.close
|
132
|
+
buffer.unlink
|
133
|
+
end
|
134
|
+
end
|
126
135
|
else
|
127
|
-
|
136
|
+
# error on endless generators
|
137
|
+
raise Error, "hexdigest for endless enumerators is not supported" if value.unbounded_body?
|
138
|
+
|
139
|
+
mb_buffer = value.each.with_object("".b) do |chunk, b|
|
140
|
+
b << chunk
|
141
|
+
break if b.bytesize >= 1024 * 1024
|
142
|
+
end
|
143
|
+
|
144
|
+
digest.hexdigest(mb_buffer)
|
128
145
|
end
|
129
146
|
end
|
130
147
|
|
@@ -141,7 +158,6 @@ module HTTPX
|
|
141
158
|
def load_dependencies(*)
|
142
159
|
require "set"
|
143
160
|
require "digest/sha2"
|
144
|
-
require "openssl"
|
145
161
|
end
|
146
162
|
|
147
163
|
def configure(klass)
|
@@ -149,6 +165,9 @@ module HTTPX
|
|
149
165
|
end
|
150
166
|
end
|
151
167
|
|
168
|
+
# adds support for the following options:
|
169
|
+
#
|
170
|
+
# :sigv4_signer :: instance of HTTPX::Plugins::AWSSigV4 used to sign requests.
|
152
171
|
module OptionsMethods
|
153
172
|
def option_sigv4_signer(value)
|
154
173
|
value.is_a?(Signer) ? value : Signer.new(value)
|
@@ -160,7 +179,7 @@ module HTTPX
|
|
160
179
|
with(sigv4_signer: Signer.new(**options))
|
161
180
|
end
|
162
181
|
|
163
|
-
def build_request(
|
182
|
+
def build_request(*)
|
164
183
|
request = super
|
165
184
|
|
166
185
|
return request if request.headers.key?("authorization")
|
@@ -25,18 +25,23 @@ module HTTPX
|
|
25
25
|
class_eval(<<-MOD, __FILE__, __LINE__ + 1)
|
26
26
|
def on_#{meth}(&blk) # def on_connection_opened(&blk)
|
27
27
|
on(:#{meth}, &blk) # on(:connection_opened, &blk)
|
28
|
+
self # self
|
28
29
|
end # end
|
29
30
|
MOD
|
30
31
|
end
|
31
32
|
|
32
33
|
private
|
33
34
|
|
34
|
-
def
|
35
|
-
|
35
|
+
def do_init_connection(connection, selector)
|
36
|
+
super
|
36
37
|
connection.on(:open) do
|
38
|
+
next unless connection.current_session == self
|
39
|
+
|
37
40
|
emit_or_callback_error(:connection_opened, connection.origin, connection.io.socket)
|
38
41
|
end
|
39
42
|
connection.on(:close) do
|
43
|
+
next unless connection.current_session == self
|
44
|
+
|
40
45
|
emit_or_callback_error(:connection_closed, connection.origin) if connection.used?
|
41
46
|
end
|
42
47
|
|
@@ -84,6 +89,12 @@ module HTTPX
|
|
84
89
|
rescue CallbackError => e
|
85
90
|
raise e.cause
|
86
91
|
end
|
92
|
+
|
93
|
+
def close(*)
|
94
|
+
super
|
95
|
+
rescue CallbackError => e
|
96
|
+
raise e.cause
|
97
|
+
end
|
87
98
|
end
|
88
99
|
end
|
89
100
|
register_plugin :callbacks, Callbacks
|
@@ -32,15 +32,11 @@ module HTTPX
|
|
32
32
|
@circuit_store = CircuitStore.new(@options)
|
33
33
|
end
|
34
34
|
|
35
|
-
def initialize_dup(orig)
|
36
|
-
super
|
37
|
-
@circuit_store = orig.instance_variable_get(:@circuit_store).dup
|
38
|
-
end
|
39
|
-
|
40
35
|
%i[circuit_open].each do |meth|
|
41
36
|
class_eval(<<-MOD, __FILE__, __LINE__ + 1)
|
42
37
|
def on_#{meth}(&blk) # def on_circuit_open(&blk)
|
43
38
|
on(:#{meth}, &blk) # on(:circuit_open, &blk)
|
39
|
+
self # self
|
44
40
|
end # end
|
45
41
|
MOD
|
46
42
|
end
|
@@ -97,6 +93,16 @@ module HTTPX
|
|
97
93
|
end
|
98
94
|
end
|
99
95
|
|
96
|
+
# adds support for the following options:
|
97
|
+
#
|
98
|
+
# :circuit_breaker_max_attempts :: the number of attempts the circuit allows, before it is opened (defaults to <tt>3</tt>).
|
99
|
+
# :circuit_breaker_reset_attempts_in :: the time a circuit stays open at most, before it resets (defaults to <tt>60</tt>).
|
100
|
+
# :circuit_breaker_break_on :: callable defining an alternative rule for a response to break
|
101
|
+
# (i.e. <tt>->(res) { res.status == 429 } </tt>)
|
102
|
+
# :circuit_breaker_break_in :: the time that must elapse before an open circuit can transit to the half-open state
|
103
|
+
# (defaults to <tt><60</tt>).
|
104
|
+
# :circuit_breaker_half_open_drip_rate :: the rate of requests a circuit allows to be performed when in an half-open state
|
105
|
+
# (defaults to <tt>1</tt>).
|
100
106
|
module OptionsMethods
|
101
107
|
def option_circuit_breaker_max_attempts(value)
|
102
108
|
attempts = Integer(value)
|
@@ -0,0 +1,202 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module HTTPX
|
4
|
+
module Plugins
|
5
|
+
#
|
6
|
+
# This plugin adds `Content-Digest` headers to requests
|
7
|
+
# and can validate these headers on responses
|
8
|
+
#
|
9
|
+
# https://datatracker.ietf.org/doc/html/rfc9530
|
10
|
+
#
|
11
|
+
module ContentDigest
|
12
|
+
class Error < HTTPX::Error; end
|
13
|
+
|
14
|
+
# Error raised on response "content-digest" header validation.
|
15
|
+
class ValidationError < Error
|
16
|
+
attr_reader :response
|
17
|
+
|
18
|
+
def initialize(message, response)
|
19
|
+
super(message)
|
20
|
+
@response = response
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class MissingContentDigestError < ValidationError; end
|
25
|
+
class InvalidContentDigestError < ValidationError; end
|
26
|
+
|
27
|
+
SUPPORTED_ALGORITHMS = {
|
28
|
+
"sha-256" => OpenSSL::Digest::SHA256,
|
29
|
+
"sha-512" => OpenSSL::Digest::SHA512,
|
30
|
+
}.freeze
|
31
|
+
|
32
|
+
class << self
|
33
|
+
def extra_options(options)
|
34
|
+
options.merge(encode_content_digest: true, validate_content_digest: false, content_digest_algorithm: "sha-256")
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# add support for the following options:
|
39
|
+
#
|
40
|
+
# :content_digest_algorithm :: the digest algorithm to use. Currently supports `sha-256` and `sha-512`. (defaults to `sha-256`)
|
41
|
+
# :encode_content_digest :: whether a <tt>Content-Digest</tt> header should be computed for the request;
|
42
|
+
# can also be a callable object (i.e. <tt>->(req) { ... }</tt>, defaults to <tt>true</tt>)
|
43
|
+
# :validate_content_digest :: whether a <tt>Content-Digest</tt> header in the response should be validated;
|
44
|
+
# can also be a callable object (i.e. <tt>->(res) { ... }</tt>, defaults to <tt>false</tt>)
|
45
|
+
module OptionsMethods
|
46
|
+
def option_content_digest_algorithm(value)
|
47
|
+
raise TypeError, ":content_digest_algorithm must be one of 'sha-256', 'sha-512'" unless SUPPORTED_ALGORITHMS.key?(value)
|
48
|
+
|
49
|
+
value
|
50
|
+
end
|
51
|
+
|
52
|
+
def option_encode_content_digest(value)
|
53
|
+
value
|
54
|
+
end
|
55
|
+
|
56
|
+
def option_validate_content_digest(value)
|
57
|
+
value
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
module ResponseBodyMethods
|
62
|
+
attr_reader :content_digest_buffer
|
63
|
+
|
64
|
+
def initialize(response, options)
|
65
|
+
super
|
66
|
+
|
67
|
+
return unless response.headers.key?("content-digest")
|
68
|
+
|
69
|
+
should_validate = options.validate_content_digest
|
70
|
+
should_validate = should_validate.call(response) if should_validate.respond_to?(:call)
|
71
|
+
|
72
|
+
return unless should_validate
|
73
|
+
|
74
|
+
@content_digest_buffer = Response::Buffer.new(
|
75
|
+
threshold_size: @options.body_threshold_size,
|
76
|
+
bytesize: @length,
|
77
|
+
encoding: @encoding
|
78
|
+
)
|
79
|
+
end
|
80
|
+
|
81
|
+
def write(chunk)
|
82
|
+
@content_digest_buffer.write(chunk) if @content_digest_buffer
|
83
|
+
super
|
84
|
+
end
|
85
|
+
|
86
|
+
def close
|
87
|
+
if @content_digest_buffer
|
88
|
+
@content_digest_buffer.close
|
89
|
+
@content_digest_buffer = nil
|
90
|
+
end
|
91
|
+
super
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
module InstanceMethods
|
96
|
+
def build_request(*)
|
97
|
+
request = super
|
98
|
+
|
99
|
+
return request if request.empty?
|
100
|
+
|
101
|
+
return request if request.headers.key?("content-digest")
|
102
|
+
|
103
|
+
perform_encoding = @options.encode_content_digest
|
104
|
+
perform_encoding = perform_encoding.call(request) if perform_encoding.respond_to?(:call)
|
105
|
+
|
106
|
+
return request unless perform_encoding
|
107
|
+
|
108
|
+
digest = base64digest(request.body)
|
109
|
+
request.headers.add("content-digest", "#{@options.content_digest_algorithm}=:#{digest}:")
|
110
|
+
|
111
|
+
request
|
112
|
+
end
|
113
|
+
|
114
|
+
private
|
115
|
+
|
116
|
+
def fetch_response(request, _, _)
|
117
|
+
response = super
|
118
|
+
return response unless response.is_a?(Response)
|
119
|
+
|
120
|
+
perform_validation = @options.validate_content_digest
|
121
|
+
perform_validation = perform_validation.call(response) if perform_validation.respond_to?(:call)
|
122
|
+
|
123
|
+
validate_content_digest(response) if perform_validation
|
124
|
+
|
125
|
+
response
|
126
|
+
rescue ValidationError => e
|
127
|
+
ErrorResponse.new(request, e)
|
128
|
+
end
|
129
|
+
|
130
|
+
def validate_content_digest(response)
|
131
|
+
content_digest_header = response.headers["content-digest"]
|
132
|
+
|
133
|
+
raise MissingContentDigestError.new("response is missing a `content-digest` header", response) unless content_digest_header
|
134
|
+
|
135
|
+
digests = extract_content_digests(content_digest_header)
|
136
|
+
|
137
|
+
included_algorithms = SUPPORTED_ALGORITHMS.keys & digests.keys
|
138
|
+
|
139
|
+
raise MissingContentDigestError.new("unsupported algorithms: #{digests.keys.join(", ")}", response) if included_algorithms.empty?
|
140
|
+
|
141
|
+
content_buffer = response.body.content_digest_buffer
|
142
|
+
|
143
|
+
included_algorithms.each do |algorithm|
|
144
|
+
digest = SUPPORTED_ALGORITHMS.fetch(algorithm).new
|
145
|
+
digest_received = digests[algorithm]
|
146
|
+
digest_computed =
|
147
|
+
if content_buffer.respond_to?(:to_path)
|
148
|
+
content_buffer.flush
|
149
|
+
digest.file(content_buffer.to_path).base64digest
|
150
|
+
else
|
151
|
+
digest.base64digest(content_buffer.to_s)
|
152
|
+
end
|
153
|
+
|
154
|
+
raise InvalidContentDigestError.new("#{algorithm} digest does not match content",
|
155
|
+
response) unless digest_received == digest_computed
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
def extract_content_digests(header)
|
160
|
+
header.split(",").to_h do |entry|
|
161
|
+
algorithm, digest = entry.split("=", 2)
|
162
|
+
raise Error, "#{entry} is an invalid digest format" unless algorithm && digest
|
163
|
+
|
164
|
+
[algorithm, digest.byteslice(1..-2)]
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
def base64digest(body)
|
169
|
+
digest = SUPPORTED_ALGORITHMS.fetch(@options.content_digest_algorithm).new
|
170
|
+
|
171
|
+
if body.respond_to?(:read)
|
172
|
+
if body.respond_to?(:to_path)
|
173
|
+
digest.file(body.to_path).base64digest
|
174
|
+
else
|
175
|
+
raise ContentDigestError, "request body must be rewindable" unless body.respond_to?(:rewind)
|
176
|
+
|
177
|
+
buffer = Tempfile.new("httpx", encoding: Encoding::BINARY, mode: File::RDWR)
|
178
|
+
begin
|
179
|
+
IO.copy_stream(body, buffer)
|
180
|
+
buffer.flush
|
181
|
+
|
182
|
+
digest.file(buffer.to_path).base64digest
|
183
|
+
ensure
|
184
|
+
body.rewind
|
185
|
+
buffer.close
|
186
|
+
buffer.unlink
|
187
|
+
end
|
188
|
+
end
|
189
|
+
else
|
190
|
+
raise ContentDigestError, "base64digest for endless enumerators is not supported" if body.unbounded_body?
|
191
|
+
|
192
|
+
buffer = "".b
|
193
|
+
body.each { |chunk| buffer << chunk }
|
194
|
+
|
195
|
+
digest.base64digest(buffer)
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
register_plugin :content_digest, ContentDigest
|
201
|
+
end
|
202
|
+
end
|
@@ -40,6 +40,12 @@ module HTTPX
|
|
40
40
|
end
|
41
41
|
end
|
42
42
|
|
43
|
+
def build_request(*)
|
44
|
+
request = super
|
45
|
+
request.headers.set_cookie(request.options.cookies[request.uri])
|
46
|
+
request
|
47
|
+
end
|
48
|
+
|
43
49
|
private
|
44
50
|
|
45
51
|
def on_response(_request, response)
|
@@ -52,12 +58,6 @@ module HTTPX
|
|
52
58
|
|
53
59
|
super
|
54
60
|
end
|
55
|
-
|
56
|
-
def build_request(*, _)
|
57
|
-
request = super
|
58
|
-
request.headers.set_cookie(request.options.cookies[request.uri])
|
59
|
-
request
|
60
|
-
end
|
61
61
|
end
|
62
62
|
|
63
63
|
module HeadersMethods
|
@@ -70,6 +70,9 @@ module HTTPX
|
|
70
70
|
end
|
71
71
|
end
|
72
72
|
|
73
|
+
# adds support for the following options:
|
74
|
+
#
|
75
|
+
# :cookies :: cookie jar for the session (can be a Hash, an Array, an instance of HTTPX::Plugins::Cookies::CookieJar)
|
73
76
|
module OptionsMethods
|
74
77
|
def option_headers(*)
|
75
78
|
value = super
|
@@ -20,6 +20,9 @@ module HTTPX
|
|
20
20
|
end
|
21
21
|
end
|
22
22
|
|
23
|
+
# adds support for the following options:
|
24
|
+
#
|
25
|
+
# :digest :: instance of HTTPX::Plugins::Authentication::Digest, used to authenticate requests in the session.
|
23
26
|
module OptionsMethods
|
24
27
|
def option_digest(value)
|
25
28
|
raise TypeError, ":digest must be a #{Authentication::Digest}" unless value.is_a?(Authentication::Digest)
|
data/lib/httpx/plugins/expect.rb
CHANGED
@@ -20,6 +20,11 @@ module HTTPX
|
|
20
20
|
end
|
21
21
|
end
|
22
22
|
|
23
|
+
# adds support for the following options:
|
24
|
+
#
|
25
|
+
# :expect_timeout :: time (in seconds) to wait for a 100-expect response,
|
26
|
+
# before retrying without the Expect header (defaults to <tt>2</tt>).
|
27
|
+
# :expect_threshold_size :: min threshold (in bytes) of the request payload to enable the 100-continue negotiation on.
|
23
28
|
module OptionsMethods
|
24
29
|
def option_expect_timeout(value)
|
25
30
|
seconds = Float(value)
|
@@ -79,7 +84,7 @@ module HTTPX
|
|
79
84
|
|
80
85
|
return if expect_timeout.nil? || expect_timeout.infinite?
|
81
86
|
|
82
|
-
set_request_timeout(request, expect_timeout, :expect, %i[body response]) do
|
87
|
+
set_request_timeout(:expect_timeout, request, expect_timeout, :expect, %i[body response]) do
|
83
88
|
# expect timeout expired
|
84
89
|
if request.state == :expect && !request.expects?
|
85
90
|
Expect.no_expect_store << request.origin
|
@@ -91,15 +96,16 @@ module HTTPX
|
|
91
96
|
end
|
92
97
|
|
93
98
|
module InstanceMethods
|
94
|
-
def fetch_response(request,
|
95
|
-
response =
|
99
|
+
def fetch_response(request, selector, options)
|
100
|
+
response = super
|
101
|
+
|
96
102
|
return unless response
|
97
103
|
|
98
104
|
if response.is_a?(Response) && response.status == 417 && request.headers.key?("expect")
|
99
105
|
response.close
|
100
106
|
request.headers.delete("expect")
|
101
107
|
request.transition(:idle)
|
102
|
-
send_request(request,
|
108
|
+
send_request(request, selector, options)
|
103
109
|
return
|
104
110
|
end
|
105
111
|
|