httpx 0.21.0 → 1.2.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 +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
|