httpx 0.21.0 → 1.2.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 +4 -4
- 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_20_0.md +1 -1
- data/doc/release_notes/0_21_0.md +7 -5
- 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/lib/httpx/adapters/datadog.rb +100 -106
- data/lib/httpx/adapters/faraday.rb +143 -107
- data/lib/httpx/adapters/sentry.rb +26 -7
- data/lib/httpx/adapters/webmock.rb +33 -17
- data/lib/httpx/altsvc.rb +61 -24
- 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 +62 -37
- data/lib/httpx/connection/http2.rb +16 -27
- data/lib/httpx/connection.rb +213 -120
- data/lib/httpx/domain_name.rb +10 -13
- data/lib/httpx/errors.rb +34 -2
- data/lib/httpx/extensions.rb +4 -134
- data/lib/httpx/io/ssl.rb +77 -71
- data/lib/httpx/io/tcp.rb +46 -70
- data/lib/httpx/io/udp.rb +18 -52
- data/lib/httpx/io/unix.rb +6 -13
- data/lib/httpx/io.rb +3 -9
- data/lib/httpx/loggable.rb +4 -19
- data/lib/httpx/options.rb +168 -110
- data/lib/httpx/plugins/{authentication → auth}/basic.rb +1 -5
- data/lib/httpx/plugins/{authentication → auth}/digest.rb +13 -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 +1 -3
- data/lib/httpx/plugins/aws_sigv4.rb +5 -6
- 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 +40 -16
- data/lib/httpx/plugins/circuit_breaker/circuit_store.rb +14 -5
- data/lib/httpx/plugins/circuit_breaker.rb +30 -7
- data/lib/httpx/plugins/cookies/set_cookie_parser.rb +0 -2
- data/lib/httpx/plugins/cookies.rb +20 -10
- data/lib/httpx/plugins/{digest_authentication.rb → digest_auth.rb} +11 -12
- data/lib/httpx/plugins/expect.rb +15 -13
- data/lib/httpx/plugins/follow_redirects.rb +71 -29
- 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 +35 -29
- data/lib/httpx/plugins/h2c.rb +25 -18
- 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 +170 -0
- data/lib/httpx/plugins/persistent.rb +1 -1
- data/lib/httpx/plugins/proxy/http.rb +15 -10
- data/lib/httpx/plugins/proxy/socks4.rb +8 -6
- data/lib/httpx/plugins/proxy/socks5.rb +10 -8
- data/lib/httpx/plugins/proxy.rb +69 -67
- data/lib/httpx/plugins/push_promise.rb +1 -1
- data/lib/httpx/plugins/rate_limiter.rb +3 -1
- data/lib/httpx/plugins/response_cache/file_store.rb +40 -0
- data/lib/httpx/plugins/response_cache/store.rb +34 -17
- data/lib/httpx/plugins/response_cache.rb +6 -6
- data/lib/httpx/plugins/retries.rb +61 -12
- data/lib/httpx/plugins/ssrf_filter.rb +142 -0
- data/lib/httpx/plugins/stream.rb +27 -32
- data/lib/httpx/plugins/upgrade/h2.rb +4 -4
- data/lib/httpx/plugins/upgrade.rb +8 -10
- data/lib/httpx/plugins/webdav.rb +10 -8
- data/lib/httpx/pool.rb +85 -23
- data/lib/httpx/punycode.rb +9 -291
- data/lib/httpx/request/body.rb +158 -0
- data/lib/httpx/request.rb +86 -121
- data/lib/httpx/resolver/https.rb +54 -17
- data/lib/httpx/resolver/multi.rb +8 -12
- data/lib/httpx/resolver/native.rb +163 -70
- data/lib/httpx/resolver/resolver.rb +28 -13
- data/lib/httpx/resolver/system.rb +15 -10
- 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 +113 -211
- data/lib/httpx/selector.rb +2 -4
- data/lib/httpx/session.rb +91 -64
- data/lib/httpx/session_extensions.rb +4 -1
- data/lib/httpx/timers.rb +28 -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 +2 -5
- data/lib/httpx/transcoder/multipart/decoder.rb +139 -0
- data/lib/httpx/{plugins → transcoder}/multipart/encoder.rb +3 -3
- 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 +0 -5
- data/lib/httpx/transcoder.rb +4 -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 +1 -0
- data/sig/callbacks.rbs +3 -3
- data/sig/chainable.rbs +10 -9
- data/sig/connection/http1.rbs +5 -4
- data/sig/connection/http2.rbs +1 -1
- data/sig/connection.rbs +46 -24
- data/sig/errors.rbs +9 -3
- data/sig/httpx.rbs +5 -4
- data/sig/io/ssl.rbs +26 -0
- data/sig/io/tcp.rbs +60 -0
- data/sig/io/udp.rbs +20 -0
- data/sig/io/unix.rbs +10 -0
- data/sig/options.rbs +28 -12
- 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 +13 -3
- data/sig/plugins/compression.rbs +6 -4
- 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 +11 -2
- 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 +2 -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/socks4.rbs +4 -4
- data/sig/plugins/proxy/socks5.rbs +2 -2
- data/sig/plugins/proxy/ssh.rbs +1 -1
- data/sig/plugins/proxy.rbs +10 -4
- data/sig/plugins/response_cache.rbs +12 -3
- data/sig/plugins/retries.rbs +28 -8
- data/sig/plugins/stream.rbs +24 -17
- data/sig/plugins/upgrade.rbs +5 -3
- data/sig/pool.rbs +5 -4
- data/sig/request/body.rbs +40 -0
- data/sig/request.rbs +12 -28
- data/sig/resolver/https.rbs +7 -2
- data/sig/resolver/native.rbs +10 -4
- data/sig/resolver/resolver.rbs +6 -4
- 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 +17 -38
- data/sig/session.rbs +24 -18
- data/sig/timers.rbs +17 -7
- 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 +4 -2
- data/sig/{plugins → transcoder}/multipart.rbs +3 -12
- 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 +1 -1
- data/sig/transcoder.rbs +22 -7
- data/sig/utils.rbs +2 -0
- metadata +127 -40
- 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 -13
- /data/sig/plugins/{authentication → auth}/ntlm.rbs +0 -0
- /data/sig/plugins/{authentication → auth}/socks5.rbs +0 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
require_relative "store"
|
|
5
|
+
|
|
6
|
+
module HTTPX::Plugins
|
|
7
|
+
module ResponseCache
|
|
8
|
+
class FileStore < Store
|
|
9
|
+
def initialize(dir = Dir.tmpdir)
|
|
10
|
+
@dir = Pathname.new(dir)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def clear
|
|
14
|
+
# delete all files
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def cached?(request)
|
|
18
|
+
file_path = @dir.join(request.response_cache_key)
|
|
19
|
+
|
|
20
|
+
exist?(file_path)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def _get(request)
|
|
26
|
+
return unless cached?(request)
|
|
27
|
+
|
|
28
|
+
File.open(@dir.join(request.response_cache_key))
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def _set(request, response)
|
|
32
|
+
file_path = @dir.join(request.response_cache_key)
|
|
33
|
+
|
|
34
|
+
response.copy_to(file_path)
|
|
35
|
+
|
|
36
|
+
response.body.rewind
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -1,28 +1,23 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "forwardable"
|
|
4
|
-
|
|
5
3
|
module HTTPX::Plugins
|
|
6
4
|
module ResponseCache
|
|
7
5
|
class Store
|
|
8
|
-
extend Forwardable
|
|
9
|
-
|
|
10
|
-
def_delegator :@store, :clear
|
|
11
|
-
|
|
12
6
|
def initialize
|
|
13
7
|
@store = {}
|
|
8
|
+
@store_mutex = Thread::Mutex.new
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def clear
|
|
12
|
+
@store_mutex.synchronize { @store.clear }
|
|
14
13
|
end
|
|
15
14
|
|
|
16
15
|
def lookup(request)
|
|
17
|
-
responses =
|
|
16
|
+
responses = _get(request)
|
|
18
17
|
|
|
19
18
|
return unless responses
|
|
20
19
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
return unless response && response.fresh?
|
|
24
|
-
|
|
25
|
-
response
|
|
20
|
+
responses.find(&method(:match_by_vary?).curry(2)[request])
|
|
26
21
|
end
|
|
27
22
|
|
|
28
23
|
def cached?(request)
|
|
@@ -32,11 +27,7 @@ module HTTPX::Plugins
|
|
|
32
27
|
def cache(request, response)
|
|
33
28
|
return unless ResponseCache.cacheable_request?(request) && ResponseCache.cacheable_response?(response)
|
|
34
29
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
responses.reject!(&method(:match_by_vary?).curry(2)[request])
|
|
38
|
-
|
|
39
|
-
responses << response
|
|
30
|
+
_set(request, response)
|
|
40
31
|
end
|
|
41
32
|
|
|
42
33
|
def prepare(request)
|
|
@@ -71,6 +62,32 @@ module HTTPX::Plugins
|
|
|
71
62
|
!original_request.headers.key?(cache_field) || request.headers[cache_field] == original_request.headers[cache_field]
|
|
72
63
|
end
|
|
73
64
|
end
|
|
65
|
+
|
|
66
|
+
def _get(request)
|
|
67
|
+
@store_mutex.synchronize do
|
|
68
|
+
responses = @store[request.response_cache_key]
|
|
69
|
+
|
|
70
|
+
return unless responses
|
|
71
|
+
|
|
72
|
+
responses.select! do |res|
|
|
73
|
+
!res.body.closed? && res.fresh?
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
responses
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def _set(request, response)
|
|
81
|
+
@store_mutex.synchronize do
|
|
82
|
+
responses = (@store[request.response_cache_key] ||= [])
|
|
83
|
+
|
|
84
|
+
responses.reject! do |res|
|
|
85
|
+
res.body.closed? || !res.fresh? || match_by_vary?(request, res)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
responses << response
|
|
89
|
+
end
|
|
90
|
+
end
|
|
74
91
|
end
|
|
75
92
|
end
|
|
76
93
|
end
|
|
@@ -5,10 +5,10 @@ module HTTPX
|
|
|
5
5
|
#
|
|
6
6
|
# This plugin adds support for retrying requests when certain errors happen.
|
|
7
7
|
#
|
|
8
|
-
# https://gitlab.com/
|
|
8
|
+
# https://gitlab.com/os85/httpx/wikis/Response-Cache
|
|
9
9
|
#
|
|
10
10
|
module ResponseCache
|
|
11
|
-
CACHEABLE_VERBS = %
|
|
11
|
+
CACHEABLE_VERBS = %w[GET HEAD].freeze
|
|
12
12
|
CACHEABLE_STATUS_CODES = [200, 203, 206, 300, 301, 410].freeze
|
|
13
13
|
private_constant :CACHEABLE_VERBS
|
|
14
14
|
private_constant :CACHEABLE_STATUS_CODES
|
|
@@ -96,15 +96,15 @@ module HTTPX
|
|
|
96
96
|
|
|
97
97
|
module RequestMethods
|
|
98
98
|
def response_cache_key
|
|
99
|
-
@response_cache_key ||= Digest::SHA1.hexdigest("httpx-response-cache-#{@verb}
|
|
99
|
+
@response_cache_key ||= Digest::SHA1.hexdigest("httpx-response-cache-#{@verb}-#{@uri}")
|
|
100
100
|
end
|
|
101
101
|
end
|
|
102
102
|
|
|
103
103
|
module ResponseMethods
|
|
104
104
|
def copy_from_cached(other)
|
|
105
|
-
@body = other.body
|
|
105
|
+
@body = other.body.dup
|
|
106
106
|
|
|
107
|
-
@body.
|
|
107
|
+
@body.rewind
|
|
108
108
|
end
|
|
109
109
|
|
|
110
110
|
# A response is fresh if its age has not yet exceeded its freshness lifetime.
|
|
@@ -169,7 +169,7 @@ module HTTPX
|
|
|
169
169
|
def date
|
|
170
170
|
@date ||= Time.httpdate(@headers["date"])
|
|
171
171
|
rescue NoMethodError, ArgumentError
|
|
172
|
-
Time.now
|
|
172
|
+
Time.now
|
|
173
173
|
end
|
|
174
174
|
end
|
|
175
175
|
end
|
|
@@ -5,13 +5,13 @@ module HTTPX
|
|
|
5
5
|
#
|
|
6
6
|
# This plugin adds support for retrying requests when certain errors happen.
|
|
7
7
|
#
|
|
8
|
-
# https://gitlab.com/
|
|
8
|
+
# https://gitlab.com/os85/httpx/wikis/Retries
|
|
9
9
|
#
|
|
10
10
|
module Retries
|
|
11
11
|
MAX_RETRIES = 3
|
|
12
12
|
# TODO: pass max_retries in a configure/load block
|
|
13
13
|
|
|
14
|
-
IDEMPOTENT_METHODS = %
|
|
14
|
+
IDEMPOTENT_METHODS = %w[GET OPTIONS HEAD PUT DELETE].freeze
|
|
15
15
|
RETRYABLE_ERRORS = [
|
|
16
16
|
IOError,
|
|
17
17
|
EOFError,
|
|
@@ -23,9 +23,10 @@ module HTTPX
|
|
|
23
23
|
Parser::Error,
|
|
24
24
|
TLSError,
|
|
25
25
|
TimeoutError,
|
|
26
|
+
ConnectionError,
|
|
26
27
|
Connection::HTTP2::GoawayError,
|
|
27
28
|
].freeze
|
|
28
|
-
DEFAULT_JITTER = ->(interval) { interval * (
|
|
29
|
+
DEFAULT_JITTER = ->(interval) { interval * ((rand + 1) * 0.5) }
|
|
29
30
|
|
|
30
31
|
if ENV.key?("HTTPX_NO_JITTER")
|
|
31
32
|
def self.extra_options(options)
|
|
@@ -87,15 +88,14 @@ module HTTPX
|
|
|
87
88
|
request.retries.positive? &&
|
|
88
89
|
__repeatable_request?(request, options) &&
|
|
89
90
|
(
|
|
90
|
-
# rubocop:disable Style/MultilineTernaryOperator
|
|
91
|
-
options.retry_on ?
|
|
92
|
-
options.retry_on.call(response) :
|
|
93
91
|
(
|
|
94
92
|
response.is_a?(ErrorResponse) && __retryable_error?(response.error)
|
|
93
|
+
) ||
|
|
94
|
+
(
|
|
95
|
+
options.retry_on && options.retry_on.call(response)
|
|
95
96
|
)
|
|
96
|
-
# rubocop:enable Style/MultilineTernaryOperator
|
|
97
97
|
)
|
|
98
|
-
|
|
98
|
+
__try_partial_retry(request, response)
|
|
99
99
|
log { "failed to get response, #{request.retries} tries to go..." }
|
|
100
100
|
request.retries -= 1
|
|
101
101
|
request.transition(:idle)
|
|
@@ -113,12 +113,10 @@ module HTTPX
|
|
|
113
113
|
log { "retrying after #{retry_after} secs..." }
|
|
114
114
|
pool.after(retry_after) do
|
|
115
115
|
log { "retrying (elapsed time: #{Utils.elapsed_time(retry_start)})!!" }
|
|
116
|
-
|
|
117
|
-
connection.send(request)
|
|
116
|
+
send_request(request, connections, options)
|
|
118
117
|
end
|
|
119
118
|
else
|
|
120
|
-
|
|
121
|
-
connection.send(request)
|
|
119
|
+
send_request(request, connections, options)
|
|
122
120
|
end
|
|
123
121
|
|
|
124
122
|
return
|
|
@@ -133,15 +131,66 @@ module HTTPX
|
|
|
133
131
|
def __retryable_error?(ex)
|
|
134
132
|
RETRYABLE_ERRORS.any? { |klass| ex.is_a?(klass) }
|
|
135
133
|
end
|
|
134
|
+
|
|
135
|
+
def proxy_error?(request, response)
|
|
136
|
+
super && !request.retries.positive?
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
#
|
|
140
|
+
# Atttempt to set the request to perform a partial range request.
|
|
141
|
+
# This happens if the peer server accepts byte-range requests, and
|
|
142
|
+
# the last response contains some body payload.
|
|
143
|
+
#
|
|
144
|
+
def __try_partial_retry(request, response)
|
|
145
|
+
response = response.response if response.is_a?(ErrorResponse)
|
|
146
|
+
|
|
147
|
+
return unless response
|
|
148
|
+
|
|
149
|
+
unless response.headers.key?("accept-ranges") &&
|
|
150
|
+
response.headers["accept-ranges"] == "bytes" && # there's nothing else supported though...
|
|
151
|
+
(original_body = response.body)
|
|
152
|
+
response.close if response.respond_to?(:close)
|
|
153
|
+
return
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
request.partial_response = response
|
|
157
|
+
|
|
158
|
+
size = original_body.bytesize
|
|
159
|
+
|
|
160
|
+
request.headers["range"] = "bytes=#{size}-"
|
|
161
|
+
end
|
|
136
162
|
end
|
|
137
163
|
|
|
138
164
|
module RequestMethods
|
|
139
165
|
attr_accessor :retries
|
|
140
166
|
|
|
167
|
+
attr_writer :partial_response
|
|
168
|
+
|
|
141
169
|
def initialize(*args)
|
|
142
170
|
super
|
|
143
171
|
@retries = @options.max_retries
|
|
144
172
|
end
|
|
173
|
+
|
|
174
|
+
def response=(response)
|
|
175
|
+
if @partial_response
|
|
176
|
+
if response.is_a?(Response) && response.status == 206
|
|
177
|
+
response.from_partial_response(@partial_response)
|
|
178
|
+
else
|
|
179
|
+
@partial_response.close
|
|
180
|
+
end
|
|
181
|
+
@partial_response = nil
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
super
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
module ResponseMethods
|
|
189
|
+
def from_partial_response(response)
|
|
190
|
+
@status = response.status
|
|
191
|
+
@headers = response.headers
|
|
192
|
+
@body = response.body
|
|
193
|
+
end
|
|
145
194
|
end
|
|
146
195
|
end
|
|
147
196
|
register_plugin :retries, Retries
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HTTPX
|
|
4
|
+
class ServerSideRequestForgeryError < Error; end
|
|
5
|
+
|
|
6
|
+
module Plugins
|
|
7
|
+
#
|
|
8
|
+
# This plugin adds support for preventing Server-Side Request Forgery attacks.
|
|
9
|
+
#
|
|
10
|
+
# https://gitlab.com/os85/httpx/wikis/Server-Side-Request-Forgery-Filter
|
|
11
|
+
#
|
|
12
|
+
module SsrfFilter
|
|
13
|
+
module IPAddrExtensions
|
|
14
|
+
refine IPAddr do
|
|
15
|
+
def prefixlen
|
|
16
|
+
mask_addr = @mask_addr
|
|
17
|
+
raise "Invalid mask" if mask_addr.zero?
|
|
18
|
+
|
|
19
|
+
mask_addr >>= 1 while (mask_addr & 0x1).zero?
|
|
20
|
+
|
|
21
|
+
length = 0
|
|
22
|
+
while mask_addr & 0x1 == 0x1
|
|
23
|
+
length += 1
|
|
24
|
+
mask_addr >>= 1
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
length
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
using IPAddrExtensions
|
|
33
|
+
|
|
34
|
+
# https://en.wikipedia.org/wiki/Reserved_IP_addresses
|
|
35
|
+
IPV4_BLACKLIST = [
|
|
36
|
+
IPAddr.new("0.0.0.0/8"), # Current network (only valid as source address)
|
|
37
|
+
IPAddr.new("10.0.0.0/8"), # Private network
|
|
38
|
+
IPAddr.new("100.64.0.0/10"), # Shared Address Space
|
|
39
|
+
IPAddr.new("127.0.0.0/8"), # Loopback
|
|
40
|
+
IPAddr.new("169.254.0.0/16"), # Link-local
|
|
41
|
+
IPAddr.new("172.16.0.0/12"), # Private network
|
|
42
|
+
IPAddr.new("192.0.0.0/24"), # IETF Protocol Assignments
|
|
43
|
+
IPAddr.new("192.0.2.0/24"), # TEST-NET-1, documentation and examples
|
|
44
|
+
IPAddr.new("192.88.99.0/24"), # IPv6 to IPv4 relay (includes 2002::/16)
|
|
45
|
+
IPAddr.new("192.168.0.0/16"), # Private network
|
|
46
|
+
IPAddr.new("198.18.0.0/15"), # Network benchmark tests
|
|
47
|
+
IPAddr.new("198.51.100.0/24"), # TEST-NET-2, documentation and examples
|
|
48
|
+
IPAddr.new("203.0.113.0/24"), # TEST-NET-3, documentation and examples
|
|
49
|
+
IPAddr.new("224.0.0.0/4"), # IP multicast (former Class D network)
|
|
50
|
+
IPAddr.new("240.0.0.0/4"), # Reserved (former Class E network)
|
|
51
|
+
IPAddr.new("255.255.255.255"), # Broadcast
|
|
52
|
+
].freeze
|
|
53
|
+
|
|
54
|
+
IPV6_BLACKLIST = ([
|
|
55
|
+
IPAddr.new("::1/128"), # Loopback
|
|
56
|
+
IPAddr.new("64:ff9b::/96"), # IPv4/IPv6 translation (RFC 6052)
|
|
57
|
+
IPAddr.new("100::/64"), # Discard prefix (RFC 6666)
|
|
58
|
+
IPAddr.new("2001::/32"), # Teredo tunneling
|
|
59
|
+
IPAddr.new("2001:10::/28"), # Deprecated (previously ORCHID)
|
|
60
|
+
IPAddr.new("2001:20::/28"), # ORCHIDv2
|
|
61
|
+
IPAddr.new("2001:db8::/32"), # Addresses used in documentation and example source code
|
|
62
|
+
IPAddr.new("2002::/16"), # 6to4
|
|
63
|
+
IPAddr.new("fc00::/7"), # Unique local address
|
|
64
|
+
IPAddr.new("fe80::/10"), # Link-local address
|
|
65
|
+
IPAddr.new("ff00::/8"), # Multicast
|
|
66
|
+
] + IPV4_BLACKLIST.flat_map do |ipaddr|
|
|
67
|
+
prefixlen = ipaddr.prefixlen
|
|
68
|
+
|
|
69
|
+
ipv4_compatible = ipaddr.ipv4_compat.mask(96 + prefixlen)
|
|
70
|
+
ipv4_mapped = ipaddr.ipv4_mapped.mask(80 + prefixlen)
|
|
71
|
+
|
|
72
|
+
[ipv4_compatible, ipv4_mapped]
|
|
73
|
+
end).freeze
|
|
74
|
+
|
|
75
|
+
class << self
|
|
76
|
+
def extra_options(options)
|
|
77
|
+
options.merge(allowed_schemes: %w[https http])
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def unsafe_ip_address?(ipaddr)
|
|
81
|
+
range = ipaddr.to_range
|
|
82
|
+
return true if range.first != range.last
|
|
83
|
+
|
|
84
|
+
return IPV6_BLACKLIST.any? { |r| r.include?(ipaddr) } if ipaddr.ipv6?
|
|
85
|
+
|
|
86
|
+
IPV4_BLACKLIST.any? { |r| r.include?(ipaddr) } # then it's IPv4
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
module OptionsMethods
|
|
91
|
+
def option_allowed_schemes(value)
|
|
92
|
+
Array(value)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
module InstanceMethods
|
|
97
|
+
def send_requests(*requests)
|
|
98
|
+
responses = requests.map do |request|
|
|
99
|
+
next if @options.allowed_schemes.include?(request.uri.scheme)
|
|
100
|
+
|
|
101
|
+
error = ServerSideRequestForgeryError.new("#{request.uri} URI scheme not allowed")
|
|
102
|
+
error.set_backtrace(caller)
|
|
103
|
+
response = ErrorResponse.new(request, error, request.options)
|
|
104
|
+
request.emit(:response, response)
|
|
105
|
+
response
|
|
106
|
+
end
|
|
107
|
+
allowed_requests = requests.select { |req| responses[requests.index(req)].nil? }
|
|
108
|
+
allowed_responses = super(*allowed_requests)
|
|
109
|
+
allowed_responses.each_with_index do |res, idx|
|
|
110
|
+
req = allowed_requests[idx]
|
|
111
|
+
responses[requests.index(req)] = res
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
responses
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
module ConnectionMethods
|
|
119
|
+
def initialize(*)
|
|
120
|
+
begin
|
|
121
|
+
super
|
|
122
|
+
rescue ServerSideRequestForgeryError => e
|
|
123
|
+
# may raise when IPs are passed as options via :addresses
|
|
124
|
+
throw(:resolve_error, e)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def addresses=(addrs)
|
|
129
|
+
addrs = addrs.map { |addr| addr.is_a?(IPAddr) ? addr : IPAddr.new(addr) }
|
|
130
|
+
|
|
131
|
+
addrs.reject!(&SsrfFilter.method(:unsafe_ip_address?))
|
|
132
|
+
|
|
133
|
+
raise ServerSideRequestForgeryError, "#{@origin.host} has no public IP addresses" if addrs.empty?
|
|
134
|
+
|
|
135
|
+
super
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
register_plugin :ssrf_filter, SsrfFilter
|
|
141
|
+
end
|
|
142
|
+
end
|
data/lib/httpx/plugins/stream.rb
CHANGED
|
@@ -2,35 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
module HTTPX
|
|
4
4
|
class StreamResponse
|
|
5
|
-
def initialize(request, session
|
|
5
|
+
def initialize(request, session)
|
|
6
6
|
@request = request
|
|
7
7
|
@session = session
|
|
8
|
-
@
|
|
8
|
+
@response = nil
|
|
9
9
|
end
|
|
10
10
|
|
|
11
11
|
def each(&block)
|
|
12
12
|
return enum_for(__method__) unless block
|
|
13
13
|
|
|
14
|
-
raise Error, "response already streamed" if @response
|
|
15
|
-
|
|
16
14
|
@request.stream = self
|
|
17
15
|
|
|
18
16
|
begin
|
|
19
17
|
@on_chunk = block
|
|
20
18
|
|
|
21
|
-
if @request.response
|
|
22
|
-
# if we've already started collecting the payload, yield it first
|
|
23
|
-
# before proceeding
|
|
24
|
-
body = @request.response.body
|
|
25
|
-
|
|
26
|
-
body.each do |chunk|
|
|
27
|
-
on_chunk(chunk)
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
|
|
31
19
|
response.raise_for_status
|
|
32
|
-
response.close
|
|
33
20
|
ensure
|
|
21
|
+
response.close if @response
|
|
34
22
|
@on_chunk = nil
|
|
35
23
|
end
|
|
36
24
|
end
|
|
@@ -38,7 +26,7 @@ module HTTPX
|
|
|
38
26
|
def each_line
|
|
39
27
|
return enum_for(__method__) unless block_given?
|
|
40
28
|
|
|
41
|
-
line =
|
|
29
|
+
line = "".b
|
|
42
30
|
|
|
43
31
|
each do |chunk|
|
|
44
32
|
line << chunk
|
|
@@ -49,6 +37,8 @@ module HTTPX
|
|
|
49
37
|
line = line.byteslice(idx + 1..-1)
|
|
50
38
|
end
|
|
51
39
|
end
|
|
40
|
+
|
|
41
|
+
yield line unless line.empty?
|
|
52
42
|
end
|
|
53
43
|
|
|
54
44
|
# This is a ghost method. It's to be used ONLY internally, when processing streams
|
|
@@ -71,9 +61,11 @@ module HTTPX
|
|
|
71
61
|
private
|
|
72
62
|
|
|
73
63
|
def response
|
|
74
|
-
|
|
64
|
+
return @response if @response
|
|
75
65
|
|
|
76
|
-
@request.response
|
|
66
|
+
@request.response || begin
|
|
67
|
+
@response = @session.request(@request)
|
|
68
|
+
end
|
|
77
69
|
end
|
|
78
70
|
|
|
79
71
|
def respond_to_missing?(meth, *args)
|
|
@@ -91,12 +83,14 @@ module HTTPX
|
|
|
91
83
|
#
|
|
92
84
|
# This plugin adds support for stream response (text/event-stream).
|
|
93
85
|
#
|
|
94
|
-
# https://gitlab.com/
|
|
86
|
+
# https://gitlab.com/os85/httpx/wikis/Stream
|
|
95
87
|
#
|
|
96
88
|
module Stream
|
|
97
|
-
|
|
98
|
-
|
|
89
|
+
def self.extra_options(options)
|
|
90
|
+
options.merge(timeout: { read_timeout: Float::INFINITY, operation_timeout: 60 })
|
|
91
|
+
end
|
|
99
92
|
|
|
93
|
+
module InstanceMethods
|
|
100
94
|
def request(*args, stream: false, **options)
|
|
101
95
|
return super(*args, **options) unless stream
|
|
102
96
|
|
|
@@ -105,9 +99,7 @@ module HTTPX
|
|
|
105
99
|
|
|
106
100
|
request = requests.first
|
|
107
101
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
StreamResponse.new(request, self, connections)
|
|
102
|
+
StreamResponse.new(request, self)
|
|
111
103
|
end
|
|
112
104
|
end
|
|
113
105
|
|
|
@@ -117,7 +109,10 @@ module HTTPX
|
|
|
117
109
|
|
|
118
110
|
module ResponseMethods
|
|
119
111
|
def stream
|
|
120
|
-
@request.
|
|
112
|
+
request = @request.root_request if @request.respond_to?(:root_request)
|
|
113
|
+
request ||= @request
|
|
114
|
+
|
|
115
|
+
request.stream
|
|
121
116
|
end
|
|
122
117
|
end
|
|
123
118
|
|
|
@@ -130,7 +125,13 @@ module HTTPX
|
|
|
130
125
|
def write(chunk)
|
|
131
126
|
return super unless @stream
|
|
132
127
|
|
|
133
|
-
|
|
128
|
+
return 0 if chunk.empty?
|
|
129
|
+
|
|
130
|
+
chunk = decode_chunk(chunk)
|
|
131
|
+
|
|
132
|
+
@stream.on_chunk(chunk.dup)
|
|
133
|
+
|
|
134
|
+
chunk.size
|
|
134
135
|
end
|
|
135
136
|
|
|
136
137
|
private
|
|
@@ -141,12 +142,6 @@ module HTTPX
|
|
|
141
142
|
super
|
|
142
143
|
end
|
|
143
144
|
end
|
|
144
|
-
|
|
145
|
-
def self.const_missing(const_name)
|
|
146
|
-
super unless const_name == :StreamResponse
|
|
147
|
-
warn "DEPRECATION WARNING: the class #{self}::StreamResponse is deprecated. Use HTTPX::StreamResponse instead."
|
|
148
|
-
HTTPX::StreamResponse
|
|
149
|
-
end
|
|
150
145
|
end
|
|
151
146
|
register_plugin :stream, Stream
|
|
152
147
|
end
|
|
@@ -6,12 +6,12 @@ module HTTPX
|
|
|
6
6
|
# This plugin adds support for upgrading an HTTP/1.1 connection to HTTP/2
|
|
7
7
|
# via an Upgrade: h2 response declaration
|
|
8
8
|
#
|
|
9
|
-
# https://gitlab.com/
|
|
9
|
+
# https://gitlab.com/os85/httpx/wikis/Connection-Upgrade#h2
|
|
10
10
|
#
|
|
11
11
|
module H2
|
|
12
12
|
class << self
|
|
13
|
-
def
|
|
14
|
-
|
|
13
|
+
def extra_options(options)
|
|
14
|
+
options.merge(upgrade_handlers: options.upgrade_handlers.merge("h2" => self))
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
def call(connection, _request, _response)
|
|
@@ -32,7 +32,7 @@ module HTTPX
|
|
|
32
32
|
|
|
33
33
|
@parser = Connection::HTTP2.new(@write_buffer, @options)
|
|
34
34
|
set_parser_callbacks(@parser)
|
|
35
|
-
@upgrade_protocol =
|
|
35
|
+
@upgrade_protocol = "h2"
|
|
36
36
|
|
|
37
37
|
# what's happening here:
|
|
38
38
|
# a deviation from the state machine is done to perform the actions when a
|
|
@@ -6,7 +6,7 @@ module HTTPX
|
|
|
6
6
|
# This plugin helps negotiating a new protocol from an HTTP/1.1 connection, via the
|
|
7
7
|
# Upgrade header.
|
|
8
8
|
#
|
|
9
|
-
# https://gitlab.com/
|
|
9
|
+
# https://gitlab.com/os85/httpx/wikis/Upgrade
|
|
10
10
|
#
|
|
11
11
|
module Upgrade
|
|
12
12
|
class << self
|
|
@@ -15,16 +15,13 @@ module HTTPX
|
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
def extra_options(options)
|
|
18
|
-
upgrade_handlers
|
|
19
|
-
extend Registry
|
|
20
|
-
end
|
|
21
|
-
options.merge(upgrade_handlers: upgrade_handlers)
|
|
18
|
+
options.merge(upgrade_handlers: {})
|
|
22
19
|
end
|
|
23
20
|
end
|
|
24
21
|
|
|
25
22
|
module OptionsMethods
|
|
26
23
|
def option_upgrade_handlers(value)
|
|
27
|
-
raise TypeError, ":upgrade_handlers must be a
|
|
24
|
+
raise TypeError, ":upgrade_handlers must be a Hash" unless value.is_a?(Hash)
|
|
28
25
|
|
|
29
26
|
value
|
|
30
27
|
end
|
|
@@ -35,19 +32,20 @@ module HTTPX
|
|
|
35
32
|
response = super
|
|
36
33
|
|
|
37
34
|
if response
|
|
38
|
-
return response unless response.
|
|
35
|
+
return response unless response.is_a?(Response)
|
|
36
|
+
|
|
37
|
+
return response unless response.headers.key?("upgrade")
|
|
39
38
|
|
|
40
39
|
upgrade_protocol = response.headers["upgrade"].split(/ *, */).first
|
|
41
40
|
|
|
42
|
-
return response unless upgrade_protocol && options.upgrade_handlers.
|
|
41
|
+
return response unless upgrade_protocol && options.upgrade_handlers.key?(upgrade_protocol)
|
|
43
42
|
|
|
44
|
-
protocol_handler = options.upgrade_handlers
|
|
43
|
+
protocol_handler = options.upgrade_handlers[upgrade_protocol]
|
|
45
44
|
|
|
46
45
|
return response unless protocol_handler
|
|
47
46
|
|
|
48
47
|
log { "upgrading to #{upgrade_protocol}..." }
|
|
49
48
|
connection = find_connection(request, connections, options)
|
|
50
|
-
connections << connection unless connections.include?(connection)
|
|
51
49
|
|
|
52
50
|
# do not upgrade already upgraded connections
|
|
53
51
|
return if connection.upgrade_protocol == upgrade_protocol
|