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
@@ -3,15 +3,20 @@
|
|
3
3
|
module HTTPX
|
4
4
|
module Plugins
|
5
5
|
#
|
6
|
-
# This plugin adds support for retrying requests when
|
6
|
+
# This plugin adds support for retrying requests when errors happen.
|
7
7
|
#
|
8
|
-
#
|
8
|
+
# It has a default max number of retries (see *MAX_RETRIES* and the *max_retries* option),
|
9
|
+
# after which it will return the last response, error or not. It will **not** raise an exception.
|
10
|
+
#
|
11
|
+
# It does not retry which are not considered idempotent (see *retry_change_requests* to override).
|
12
|
+
#
|
13
|
+
# https://gitlab.com/os85/httpx/wikis/Retries
|
9
14
|
#
|
10
15
|
module Retries
|
11
16
|
MAX_RETRIES = 3
|
12
17
|
# TODO: pass max_retries in a configure/load block
|
13
18
|
|
14
|
-
IDEMPOTENT_METHODS = %
|
19
|
+
IDEMPOTENT_METHODS = %w[GET OPTIONS HEAD PUT DELETE].freeze
|
15
20
|
RETRYABLE_ERRORS = [
|
16
21
|
IOError,
|
17
22
|
EOFError,
|
@@ -23,9 +28,10 @@ module HTTPX
|
|
23
28
|
Parser::Error,
|
24
29
|
TLSError,
|
25
30
|
TimeoutError,
|
31
|
+
ConnectionError,
|
26
32
|
Connection::HTTP2::GoawayError,
|
27
33
|
].freeze
|
28
|
-
DEFAULT_JITTER = ->(interval) { interval * (
|
34
|
+
DEFAULT_JITTER = ->(interval) { interval * ((rand + 1) * 0.5) }
|
29
35
|
|
30
36
|
if ENV.key?("HTTPX_NO_JITTER")
|
31
37
|
def self.extra_options(options)
|
@@ -37,6 +43,14 @@ module HTTPX
|
|
37
43
|
end
|
38
44
|
end
|
39
45
|
|
46
|
+
# adds support for the following options:
|
47
|
+
#
|
48
|
+
# :max_retries :: max number of times a request will be retried (defaults to <tt>3</tt>).
|
49
|
+
# :retry_change_requests :: whether idempotent requests are retried (defaults to <tt>false</tt>).
|
50
|
+
# :retry_after:: seconds after which a request is retried; can also be a callable object (i.e. <tt>->(req, res) { ... } </tt>)
|
51
|
+
# :retry_jitter :: number of seconds applied to *:retry_after* (must be a callable, i.e. <tt>->(retry_after) { ... } </tt>).
|
52
|
+
# :retry_on :: callable which alternatively defines a different rule for when a response is to be retried
|
53
|
+
# (i.e. <tt>->(res) { ... }</tt>).
|
40
54
|
module OptionsMethods
|
41
55
|
def option_retry_after(value)
|
42
56
|
# return early if callable
|
@@ -57,7 +71,7 @@ module HTTPX
|
|
57
71
|
|
58
72
|
def option_max_retries(value)
|
59
73
|
num = Integer(value)
|
60
|
-
raise TypeError, ":max_retries must be positive" unless num
|
74
|
+
raise TypeError, ":max_retries must be positive" unless num >= 0
|
61
75
|
|
62
76
|
num
|
63
77
|
end
|
@@ -67,7 +81,7 @@ module HTTPX
|
|
67
81
|
end
|
68
82
|
|
69
83
|
def option_retry_on(value)
|
70
|
-
raise ":retry_on must be called with the response" unless value.respond_to?(:call)
|
84
|
+
raise TypeError, ":retry_on must be called with the response" unless value.respond_to?(:call)
|
71
85
|
|
72
86
|
value
|
73
87
|
end
|
@@ -75,7 +89,7 @@ module HTTPX
|
|
75
89
|
|
76
90
|
module InstanceMethods
|
77
91
|
def max_retries(n)
|
78
|
-
with(max_retries: n
|
92
|
+
with(max_retries: n)
|
79
93
|
end
|
80
94
|
|
81
95
|
private
|
@@ -87,15 +101,14 @@ module HTTPX
|
|
87
101
|
request.retries.positive? &&
|
88
102
|
__repeatable_request?(request, options) &&
|
89
103
|
(
|
90
|
-
# rubocop:disable Style/MultilineTernaryOperator
|
91
|
-
options.retry_on ?
|
92
|
-
options.retry_on.call(response) :
|
93
104
|
(
|
94
105
|
response.is_a?(ErrorResponse) && __retryable_error?(response.error)
|
106
|
+
) ||
|
107
|
+
(
|
108
|
+
options.retry_on && options.retry_on.call(response)
|
95
109
|
)
|
96
|
-
# rubocop:enable Style/MultilineTernaryOperator
|
97
110
|
)
|
98
|
-
|
111
|
+
__try_partial_retry(request, response)
|
99
112
|
log { "failed to get response, #{request.retries} tries to go..." }
|
100
113
|
request.retries -= 1
|
101
114
|
request.transition(:idle)
|
@@ -111,14 +124,20 @@ module HTTPX
|
|
111
124
|
|
112
125
|
retry_start = Utils.now
|
113
126
|
log { "retrying after #{retry_after} secs..." }
|
127
|
+
|
128
|
+
deactivate_connection(request, connections, options)
|
129
|
+
|
114
130
|
pool.after(retry_after) do
|
115
|
-
|
116
|
-
|
117
|
-
|
131
|
+
if request.response
|
132
|
+
# request has terminated abruptly meanwhile
|
133
|
+
request.emit(:response, request.response)
|
134
|
+
else
|
135
|
+
log { "retrying (elapsed time: #{Utils.elapsed_time(retry_start)})!!" }
|
136
|
+
send_request(request, connections, options)
|
137
|
+
end
|
118
138
|
end
|
119
139
|
else
|
120
|
-
|
121
|
-
connection.send(request)
|
140
|
+
send_request(request, connections, options)
|
122
141
|
end
|
123
142
|
|
124
143
|
return
|
@@ -133,15 +152,66 @@ module HTTPX
|
|
133
152
|
def __retryable_error?(ex)
|
134
153
|
RETRYABLE_ERRORS.any? { |klass| ex.is_a?(klass) }
|
135
154
|
end
|
155
|
+
|
156
|
+
def proxy_error?(request, response)
|
157
|
+
super && !request.retries.positive?
|
158
|
+
end
|
159
|
+
|
160
|
+
#
|
161
|
+
# Atttempt to set the request to perform a partial range request.
|
162
|
+
# This happens if the peer server accepts byte-range requests, and
|
163
|
+
# the last response contains some body payload.
|
164
|
+
#
|
165
|
+
def __try_partial_retry(request, response)
|
166
|
+
response = response.response if response.is_a?(ErrorResponse)
|
167
|
+
|
168
|
+
return unless response
|
169
|
+
|
170
|
+
unless response.headers.key?("accept-ranges") &&
|
171
|
+
response.headers["accept-ranges"] == "bytes" && # there's nothing else supported though...
|
172
|
+
(original_body = response.body)
|
173
|
+
response.close if response.respond_to?(:close)
|
174
|
+
return
|
175
|
+
end
|
176
|
+
|
177
|
+
request.partial_response = response
|
178
|
+
|
179
|
+
size = original_body.bytesize
|
180
|
+
|
181
|
+
request.headers["range"] = "bytes=#{size}-"
|
182
|
+
end
|
136
183
|
end
|
137
184
|
|
138
185
|
module RequestMethods
|
139
186
|
attr_accessor :retries
|
140
187
|
|
188
|
+
attr_writer :partial_response
|
189
|
+
|
141
190
|
def initialize(*args)
|
142
191
|
super
|
143
192
|
@retries = @options.max_retries
|
144
193
|
end
|
194
|
+
|
195
|
+
def response=(response)
|
196
|
+
if @partial_response
|
197
|
+
if response.is_a?(Response) && response.status == 206
|
198
|
+
response.from_partial_response(@partial_response)
|
199
|
+
else
|
200
|
+
@partial_response.close
|
201
|
+
end
|
202
|
+
@partial_response = nil
|
203
|
+
end
|
204
|
+
|
205
|
+
super
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
module ResponseMethods
|
210
|
+
def from_partial_response(response)
|
211
|
+
@status = response.status
|
212
|
+
@headers = response.headers
|
213
|
+
@body = response.body
|
214
|
+
end
|
145
215
|
end
|
146
216
|
end
|
147
217
|
register_plugin :retries, Retries
|
@@ -0,0 +1,145 @@
|
|
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
|
+
# adds support for the following options:
|
91
|
+
#
|
92
|
+
# :allowed_schemes :: list of URI schemes allowed (defaults to <tt>["https", "http"]</tt>)
|
93
|
+
module OptionsMethods
|
94
|
+
def option_allowed_schemes(value)
|
95
|
+
Array(value)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
module InstanceMethods
|
100
|
+
def send_requests(*requests)
|
101
|
+
responses = requests.map do |request|
|
102
|
+
next if @options.allowed_schemes.include?(request.uri.scheme)
|
103
|
+
|
104
|
+
error = ServerSideRequestForgeryError.new("#{request.uri} URI scheme not allowed")
|
105
|
+
error.set_backtrace(caller)
|
106
|
+
response = ErrorResponse.new(request, error)
|
107
|
+
request.emit(:response, response)
|
108
|
+
response
|
109
|
+
end
|
110
|
+
allowed_requests = requests.select { |req| responses[requests.index(req)].nil? }
|
111
|
+
allowed_responses = super(*allowed_requests)
|
112
|
+
allowed_responses.each_with_index do |res, idx|
|
113
|
+
req = allowed_requests[idx]
|
114
|
+
responses[requests.index(req)] = res
|
115
|
+
end
|
116
|
+
|
117
|
+
responses
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
module ConnectionMethods
|
122
|
+
def initialize(*)
|
123
|
+
begin
|
124
|
+
super
|
125
|
+
rescue ServerSideRequestForgeryError => e
|
126
|
+
# may raise when IPs are passed as options via :addresses
|
127
|
+
throw(:resolve_error, e)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def addresses=(addrs)
|
132
|
+
addrs = addrs.map { |addr| addr.is_a?(IPAddr) ? addr : IPAddr.new(addr) }
|
133
|
+
|
134
|
+
addrs.reject!(&SsrfFilter.method(:unsafe_ip_address?))
|
135
|
+
|
136
|
+
raise ServerSideRequestForgeryError, "#{@origin.host} has no public IP addresses" if addrs.empty?
|
137
|
+
|
138
|
+
super
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
register_plugin :ssrf_filter, SsrfFilter
|
144
|
+
end
|
145
|
+
end
|
data/lib/httpx/plugins/stream.rb
CHANGED
@@ -2,17 +2,15 @@
|
|
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
|
@@ -20,7 +18,7 @@ module HTTPX
|
|
20
18
|
|
21
19
|
if @request.response
|
22
20
|
# if we've already started collecting the payload, yield it first
|
23
|
-
# before proceeding
|
21
|
+
# before proceeding.
|
24
22
|
body = @request.response.body
|
25
23
|
|
26
24
|
body.each do |chunk|
|
@@ -29,7 +27,6 @@ module HTTPX
|
|
29
27
|
end
|
30
28
|
|
31
29
|
response.raise_for_status
|
32
|
-
response.close
|
33
30
|
ensure
|
34
31
|
@on_chunk = nil
|
35
32
|
end
|
@@ -38,7 +35,7 @@ module HTTPX
|
|
38
35
|
def each_line
|
39
36
|
return enum_for(__method__) unless block_given?
|
40
37
|
|
41
|
-
line =
|
38
|
+
line = "".b
|
42
39
|
|
43
40
|
each do |chunk|
|
44
41
|
line << chunk
|
@@ -49,6 +46,8 @@ module HTTPX
|
|
49
46
|
line = line.byteslice(idx + 1..-1)
|
50
47
|
end
|
51
48
|
end
|
49
|
+
|
50
|
+
yield line unless line.empty?
|
52
51
|
end
|
53
52
|
|
54
53
|
# This is a ghost method. It's to be used ONLY internally, when processing streams
|
@@ -71,9 +70,11 @@ module HTTPX
|
|
71
70
|
private
|
72
71
|
|
73
72
|
def response
|
74
|
-
|
73
|
+
return @response if @response
|
75
74
|
|
76
|
-
@request.response
|
75
|
+
@request.response || begin
|
76
|
+
@response = @session.request(@request)
|
77
|
+
end
|
77
78
|
end
|
78
79
|
|
79
80
|
def respond_to_missing?(meth, *args)
|
@@ -91,12 +92,14 @@ module HTTPX
|
|
91
92
|
#
|
92
93
|
# This plugin adds support for stream response (text/event-stream).
|
93
94
|
#
|
94
|
-
# https://gitlab.com/
|
95
|
+
# https://gitlab.com/os85/httpx/wikis/Stream
|
95
96
|
#
|
96
97
|
module Stream
|
97
|
-
|
98
|
-
|
98
|
+
def self.extra_options(options)
|
99
|
+
options.merge(timeout: { read_timeout: Float::INFINITY, operation_timeout: 60 })
|
100
|
+
end
|
99
101
|
|
102
|
+
module InstanceMethods
|
100
103
|
def request(*args, stream: false, **options)
|
101
104
|
return super(*args, **options) unless stream
|
102
105
|
|
@@ -105,9 +108,7 @@ module HTTPX
|
|
105
108
|
|
106
109
|
request = requests.first
|
107
110
|
|
108
|
-
|
109
|
-
|
110
|
-
StreamResponse.new(request, self, connections)
|
111
|
+
StreamResponse.new(request, self)
|
111
112
|
end
|
112
113
|
end
|
113
114
|
|
@@ -117,7 +118,10 @@ module HTTPX
|
|
117
118
|
|
118
119
|
module ResponseMethods
|
119
120
|
def stream
|
120
|
-
@request.
|
121
|
+
request = @request.root_request if @request.respond_to?(:root_request)
|
122
|
+
request ||= @request
|
123
|
+
|
124
|
+
request.stream
|
121
125
|
end
|
122
126
|
end
|
123
127
|
|
@@ -130,7 +134,13 @@ module HTTPX
|
|
130
134
|
def write(chunk)
|
131
135
|
return super unless @stream
|
132
136
|
|
133
|
-
|
137
|
+
return 0 if chunk.empty?
|
138
|
+
|
139
|
+
chunk = decode_chunk(chunk)
|
140
|
+
|
141
|
+
@stream.on_chunk(chunk.dup)
|
142
|
+
|
143
|
+
chunk.size
|
134
144
|
end
|
135
145
|
|
136
146
|
private
|
@@ -141,12 +151,6 @@ module HTTPX
|
|
141
151
|
super
|
142
152
|
end
|
143
153
|
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
154
|
end
|
151
155
|
register_plugin :stream, Stream
|
152
156
|
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
|
@@ -0,0 +1,80 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module HTTPX
|
4
|
+
module Plugins
|
5
|
+
#
|
6
|
+
# This plugin implements convenience methods for performing WEBDAV requests.
|
7
|
+
#
|
8
|
+
# https://gitlab.com/os85/httpx/wikis/WebDav
|
9
|
+
#
|
10
|
+
module WebDav
|
11
|
+
module InstanceMethods
|
12
|
+
def copy(src, dest)
|
13
|
+
request("COPY", src, headers: { "destination" => @options.origin.merge(dest) })
|
14
|
+
end
|
15
|
+
|
16
|
+
def move(src, dest)
|
17
|
+
request("MOVE", src, headers: { "destination" => @options.origin.merge(dest) })
|
18
|
+
end
|
19
|
+
|
20
|
+
def lock(path, timeout: nil, &blk)
|
21
|
+
headers = {}
|
22
|
+
headers["timeout"] = if timeout && timeout.positive?
|
23
|
+
"Second-#{timeout}"
|
24
|
+
else
|
25
|
+
"Infinite, Second-4100000000"
|
26
|
+
end
|
27
|
+
xml = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>" \
|
28
|
+
"<D:lockinfo xmlns:D=\"DAV:\">" \
|
29
|
+
"<D:lockscope><D:exclusive/></D:lockscope>" \
|
30
|
+
"<D:locktype><D:write/></D:locktype>" \
|
31
|
+
"<D:owner>null</D:owner>" \
|
32
|
+
"</D:lockinfo>"
|
33
|
+
response = request("LOCK", path, headers: headers, xml: xml)
|
34
|
+
|
35
|
+
return response unless response.is_a?(Response)
|
36
|
+
|
37
|
+
return response unless blk && response.status == 200
|
38
|
+
|
39
|
+
lock_token = response.headers["lock-token"]
|
40
|
+
|
41
|
+
begin
|
42
|
+
blk.call(response)
|
43
|
+
ensure
|
44
|
+
unlock(path, lock_token)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def unlock(path, lock_token)
|
49
|
+
request("UNLOCK", path, headers: { "lock-token" => lock_token })
|
50
|
+
end
|
51
|
+
|
52
|
+
def mkcol(dir)
|
53
|
+
request("MKCOL", dir)
|
54
|
+
end
|
55
|
+
|
56
|
+
def propfind(path, xml = nil)
|
57
|
+
body = case xml
|
58
|
+
when :acl
|
59
|
+
'<?xml version="1.0" encoding="utf-8" ?><D:propfind xmlns:D="DAV:"><D:prop><D:owner/>' \
|
60
|
+
"<D:supported-privilege-set/><D:current-user-privilege-set/><D:acl/></D:prop></D:propfind>"
|
61
|
+
when nil
|
62
|
+
'<?xml version="1.0" encoding="utf-8"?><DAV:propfind xmlns:DAV="DAV:"><DAV:allprop/></DAV:propfind>'
|
63
|
+
else
|
64
|
+
xml
|
65
|
+
end
|
66
|
+
|
67
|
+
request("PROPFIND", path, headers: { "depth" => "1" }, xml: body)
|
68
|
+
end
|
69
|
+
|
70
|
+
def proppatch(path, xml)
|
71
|
+
body = "<?xml version=\"1.0\"?>" \
|
72
|
+
"<D:propertyupdate xmlns:D=\"DAV:\" xmlns:Z=\"http://ns.example.com/standards/z39.50/\">#{xml}</D:propertyupdate>"
|
73
|
+
request("PROPPATCH", path, xml: body)
|
74
|
+
end
|
75
|
+
# %i[ orderpatch acl report search]
|
76
|
+
end
|
77
|
+
end
|
78
|
+
register_plugin(:webdav, WebDav)
|
79
|
+
end
|
80
|
+
end
|