httpx 0.20.0 → 1.3.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.
Files changed (250) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +0 -48
  3. data/README.md +54 -45
  4. data/doc/release_notes/0_10_0.md +2 -2
  5. data/doc/release_notes/0_11_0.md +3 -5
  6. data/doc/release_notes/0_12_0.md +5 -5
  7. data/doc/release_notes/0_13_0.md +5 -5
  8. data/doc/release_notes/0_14_0.md +2 -2
  9. data/doc/release_notes/0_16_0.md +3 -3
  10. data/doc/release_notes/0_17_0.md +1 -1
  11. data/doc/release_notes/0_18_0.md +4 -4
  12. data/doc/release_notes/0_18_2.md +1 -1
  13. data/doc/release_notes/0_19_0.md +1 -1
  14. data/doc/release_notes/0_19_8.md +1 -1
  15. data/doc/release_notes/0_20_0.md +2 -2
  16. data/doc/release_notes/0_20_1.md +5 -0
  17. data/doc/release_notes/0_20_2.md +7 -0
  18. data/doc/release_notes/0_20_3.md +6 -0
  19. data/doc/release_notes/0_20_4.md +17 -0
  20. data/doc/release_notes/0_20_5.md +3 -0
  21. data/doc/release_notes/0_21_0.md +96 -0
  22. data/doc/release_notes/0_21_1.md +12 -0
  23. data/doc/release_notes/0_22_0.md +13 -0
  24. data/doc/release_notes/0_22_1.md +11 -0
  25. data/doc/release_notes/0_22_2.md +5 -0
  26. data/doc/release_notes/0_22_3.md +55 -0
  27. data/doc/release_notes/0_22_4.md +6 -0
  28. data/doc/release_notes/0_22_5.md +6 -0
  29. data/doc/release_notes/0_23_0.md +42 -0
  30. data/doc/release_notes/0_23_1.md +5 -0
  31. data/doc/release_notes/0_23_2.md +5 -0
  32. data/doc/release_notes/0_23_3.md +6 -0
  33. data/doc/release_notes/0_23_4.md +5 -0
  34. data/doc/release_notes/0_24_0.md +48 -0
  35. data/doc/release_notes/0_24_1.md +12 -0
  36. data/doc/release_notes/0_24_2.md +12 -0
  37. data/doc/release_notes/0_24_3.md +12 -0
  38. data/doc/release_notes/0_24_4.md +18 -0
  39. data/doc/release_notes/0_24_5.md +6 -0
  40. data/doc/release_notes/0_24_6.md +5 -0
  41. data/doc/release_notes/0_24_7.md +10 -0
  42. data/doc/release_notes/1_0_0.md +60 -0
  43. data/doc/release_notes/1_0_1.md +5 -0
  44. data/doc/release_notes/1_0_2.md +7 -0
  45. data/doc/release_notes/1_1_0.md +32 -0
  46. data/doc/release_notes/1_1_1.md +17 -0
  47. data/doc/release_notes/1_1_2.md +12 -0
  48. data/doc/release_notes/1_1_3.md +18 -0
  49. data/doc/release_notes/1_1_4.md +6 -0
  50. data/doc/release_notes/1_1_5.md +12 -0
  51. data/doc/release_notes/1_2_0.md +49 -0
  52. data/doc/release_notes/1_2_1.md +6 -0
  53. data/doc/release_notes/1_2_2.md +10 -0
  54. data/doc/release_notes/1_2_3.md +16 -0
  55. data/doc/release_notes/1_2_4.md +8 -0
  56. data/doc/release_notes/1_2_5.md +7 -0
  57. data/doc/release_notes/1_2_6.md +13 -0
  58. data/doc/release_notes/1_3_0.md +18 -0
  59. data/doc/release_notes/1_3_1.md +17 -0
  60. data/lib/httpx/adapters/datadog.rb +215 -122
  61. data/lib/httpx/adapters/faraday.rb +145 -107
  62. data/lib/httpx/adapters/sentry.rb +26 -7
  63. data/lib/httpx/adapters/webmock.rb +34 -18
  64. data/lib/httpx/altsvc.rb +63 -26
  65. data/lib/httpx/base64.rb +27 -0
  66. data/lib/httpx/buffer.rb +12 -0
  67. data/lib/httpx/callbacks.rb +5 -3
  68. data/lib/httpx/chainable.rb +54 -39
  69. data/lib/httpx/connection/http1.rb +75 -44
  70. data/lib/httpx/connection/http2.rb +31 -38
  71. data/lib/httpx/connection.rb +287 -117
  72. data/lib/httpx/domain_name.rb +10 -13
  73. data/lib/httpx/errors.rb +52 -2
  74. data/lib/httpx/extensions.rb +24 -131
  75. data/lib/httpx/io/ssl.rb +83 -77
  76. data/lib/httpx/io/tcp.rb +48 -71
  77. data/lib/httpx/io/udp.rb +18 -52
  78. data/lib/httpx/io/unix.rb +10 -15
  79. data/lib/httpx/io.rb +3 -9
  80. data/lib/httpx/loggable.rb +4 -19
  81. data/lib/httpx/options.rb +176 -118
  82. data/lib/httpx/parser/http1.rb +4 -0
  83. data/lib/httpx/plugins/{authentication → auth}/basic.rb +1 -5
  84. data/lib/httpx/plugins/{authentication → auth}/digest.rb +14 -14
  85. data/lib/httpx/plugins/{authentication → auth}/ntlm.rb +1 -3
  86. data/lib/httpx/plugins/{authentication → auth}/socks5.rb +0 -2
  87. data/lib/httpx/plugins/auth.rb +25 -0
  88. data/lib/httpx/plugins/aws_sdk_authentication.rb +4 -3
  89. data/lib/httpx/plugins/aws_sigv4.rb +12 -9
  90. data/lib/httpx/plugins/basic_auth.rb +29 -0
  91. data/lib/httpx/plugins/brotli.rb +50 -0
  92. data/lib/httpx/plugins/callbacks.rb +91 -0
  93. data/lib/httpx/plugins/circuit_breaker/circuit.rb +100 -0
  94. data/lib/httpx/plugins/circuit_breaker/circuit_store.rb +53 -0
  95. data/lib/httpx/plugins/circuit_breaker.rb +148 -0
  96. data/lib/httpx/plugins/cookies/set_cookie_parser.rb +0 -2
  97. data/lib/httpx/plugins/cookies.rb +30 -17
  98. data/lib/httpx/plugins/{digest_authentication.rb → digest_auth.rb} +14 -12
  99. data/lib/httpx/plugins/expect.rb +21 -14
  100. data/lib/httpx/plugins/follow_redirects.rb +140 -41
  101. data/lib/httpx/plugins/grpc/call.rb +2 -3
  102. data/lib/httpx/plugins/grpc/grpc_encoding.rb +88 -0
  103. data/lib/httpx/plugins/grpc/message.rb +7 -37
  104. data/lib/httpx/plugins/grpc.rb +36 -29
  105. data/lib/httpx/plugins/h2c.rb +26 -19
  106. data/lib/httpx/plugins/internal_telemetry.rb +16 -0
  107. data/lib/httpx/plugins/{ntlm_authentication.rb → ntlm_auth.rb} +7 -5
  108. data/lib/httpx/plugins/oauth.rb +175 -0
  109. data/lib/httpx/plugins/persistent.rb +1 -1
  110. data/lib/httpx/plugins/proxy/http.rb +23 -13
  111. data/lib/httpx/plugins/proxy/socks4.rb +9 -7
  112. data/lib/httpx/plugins/proxy/socks5.rb +11 -9
  113. data/lib/httpx/plugins/proxy.rb +80 -61
  114. data/lib/httpx/plugins/push_promise.rb +1 -1
  115. data/lib/httpx/plugins/rate_limiter.rb +5 -1
  116. data/lib/httpx/plugins/response_cache/file_store.rb +40 -0
  117. data/lib/httpx/plugins/response_cache/store.rb +62 -25
  118. data/lib/httpx/plugins/response_cache.rb +105 -12
  119. data/lib/httpx/plugins/retries.rb +87 -17
  120. data/lib/httpx/plugins/ssrf_filter.rb +145 -0
  121. data/lib/httpx/plugins/stream.rb +27 -23
  122. data/lib/httpx/plugins/upgrade/h2.rb +4 -4
  123. data/lib/httpx/plugins/upgrade.rb +8 -10
  124. data/lib/httpx/plugins/webdav.rb +80 -0
  125. data/lib/httpx/pool/synch_pool.rb +93 -0
  126. data/lib/httpx/pool.rb +102 -27
  127. data/lib/httpx/punycode.rb +9 -291
  128. data/lib/httpx/request/body.rb +154 -0
  129. data/lib/httpx/request.rb +130 -146
  130. data/lib/httpx/resolver/https.rb +62 -27
  131. data/lib/httpx/resolver/multi.rb +9 -13
  132. data/lib/httpx/resolver/native.rb +192 -76
  133. data/lib/httpx/resolver/resolver.rb +34 -9
  134. data/lib/httpx/resolver/system.rb +16 -11
  135. data/lib/httpx/resolver.rb +38 -16
  136. data/lib/httpx/response/body.rb +242 -0
  137. data/lib/httpx/response/buffer.rb +96 -0
  138. data/lib/httpx/response.rb +159 -217
  139. data/lib/httpx/selector.rb +9 -4
  140. data/lib/httpx/session.rb +137 -89
  141. data/lib/httpx/session_extensions.rb +4 -1
  142. data/lib/httpx/timers.rb +34 -8
  143. data/lib/httpx/transcoder/body.rb +0 -2
  144. data/lib/httpx/transcoder/chunker.rb +0 -1
  145. data/lib/httpx/transcoder/deflate.rb +37 -0
  146. data/lib/httpx/transcoder/form.rb +52 -33
  147. data/lib/httpx/transcoder/gzip.rb +74 -0
  148. data/lib/httpx/transcoder/json.rb +21 -8
  149. data/lib/httpx/transcoder/multipart/decoder.rb +139 -0
  150. data/lib/httpx/{plugins → transcoder}/multipart/encoder.rb +4 -4
  151. data/lib/httpx/{plugins → transcoder}/multipart/mime_type_detector.rb +1 -1
  152. data/lib/httpx/{plugins → transcoder}/multipart/part.rb +3 -2
  153. data/lib/httpx/transcoder/multipart.rb +17 -0
  154. data/lib/httpx/transcoder/utils/body_reader.rb +46 -0
  155. data/lib/httpx/transcoder/utils/deflater.rb +72 -0
  156. data/lib/httpx/transcoder/utils/inflater.rb +19 -0
  157. data/lib/httpx/transcoder/xml.rb +52 -0
  158. data/lib/httpx/transcoder.rb +5 -6
  159. data/lib/httpx/utils.rb +36 -16
  160. data/lib/httpx/version.rb +1 -1
  161. data/lib/httpx.rb +12 -14
  162. data/sig/altsvc.rbs +33 -0
  163. data/sig/buffer.rbs +2 -1
  164. data/sig/callbacks.rbs +3 -3
  165. data/sig/chainable.rbs +11 -9
  166. data/sig/connection/http1.rbs +8 -7
  167. data/sig/connection/http2.rbs +19 -19
  168. data/sig/connection.rbs +64 -24
  169. data/sig/errors.rbs +22 -3
  170. data/sig/httpx.rbs +5 -4
  171. data/sig/io/ssl.rbs +27 -0
  172. data/sig/io/tcp.rbs +60 -0
  173. data/sig/io/udp.rbs +20 -0
  174. data/sig/io/unix.rbs +27 -0
  175. data/sig/io.rbs +6 -0
  176. data/sig/options.rbs +32 -22
  177. data/sig/parser/http1.rbs +1 -1
  178. data/sig/plugins/{authentication → auth}/basic.rbs +0 -2
  179. data/sig/plugins/{authentication → auth}/digest.rbs +2 -1
  180. data/sig/plugins/auth.rbs +13 -0
  181. data/sig/plugins/{basic_authentication.rbs → basic_auth.rbs} +2 -2
  182. data/sig/plugins/brotli.rbs +22 -0
  183. data/sig/plugins/callbacks.rbs +38 -0
  184. data/sig/plugins/circuit_breaker.rbs +71 -0
  185. data/sig/plugins/compression.rbs +7 -5
  186. data/sig/plugins/cookies/jar.rbs +2 -2
  187. data/sig/plugins/cookies.rbs +2 -0
  188. data/sig/plugins/{digest_authentication.rbs → digest_auth.rbs} +2 -2
  189. data/sig/plugins/follow_redirects.rbs +18 -4
  190. data/sig/plugins/grpc/call.rbs +19 -0
  191. data/sig/plugins/grpc/grpc_encoding.rbs +37 -0
  192. data/sig/plugins/grpc/message.rbs +17 -0
  193. data/sig/plugins/grpc.rbs +7 -32
  194. data/sig/plugins/h2c.rbs +1 -1
  195. data/sig/plugins/{ntlm_authentication.rbs → ntlm_auth.rbs} +2 -2
  196. data/sig/plugins/oauth.rbs +54 -0
  197. data/sig/plugins/proxy/http.rbs +3 -0
  198. data/sig/plugins/proxy/socks4.rbs +9 -6
  199. data/sig/plugins/proxy/socks5.rbs +10 -6
  200. data/sig/plugins/proxy/ssh.rbs +1 -1
  201. data/sig/plugins/proxy.rbs +13 -5
  202. data/sig/plugins/push_promise.rbs +3 -3
  203. data/sig/plugins/rate_limiter.rbs +1 -1
  204. data/sig/plugins/response_cache.rbs +36 -7
  205. data/sig/plugins/retries.rbs +30 -8
  206. data/sig/plugins/stream.rbs +24 -17
  207. data/sig/plugins/upgrade.rbs +5 -3
  208. data/sig/pool.rbs +10 -7
  209. data/sig/request/body.rbs +38 -0
  210. data/sig/request.rbs +15 -24
  211. data/sig/resolver/https.rbs +8 -3
  212. data/sig/resolver/native.rbs +17 -4
  213. data/sig/resolver/resolver.rbs +8 -6
  214. data/sig/resolver/system.rbs +2 -0
  215. data/sig/resolver.rbs +9 -5
  216. data/sig/response/body.rbs +53 -0
  217. data/sig/response/buffer.rbs +24 -0
  218. data/sig/response.rbs +24 -39
  219. data/sig/selector.rbs +1 -1
  220. data/sig/session.rbs +29 -18
  221. data/sig/timers.rbs +18 -8
  222. data/sig/transcoder/body.rbs +4 -3
  223. data/sig/transcoder/deflate.rbs +11 -0
  224. data/sig/transcoder/form.rbs +5 -3
  225. data/sig/transcoder/gzip.rbs +24 -0
  226. data/sig/transcoder/json.rbs +8 -3
  227. data/sig/{plugins → transcoder}/multipart.rbs +15 -19
  228. data/sig/transcoder/utils/body_reader.rbs +15 -0
  229. data/sig/transcoder/utils/deflater.rbs +29 -0
  230. data/sig/transcoder/utils/inflater.rbs +12 -0
  231. data/sig/transcoder/xml.rbs +22 -0
  232. data/sig/transcoder.rbs +24 -9
  233. data/sig/utils.rbs +8 -2
  234. metadata +163 -41
  235. data/lib/httpx/plugins/authentication.rb +0 -20
  236. data/lib/httpx/plugins/basic_authentication.rb +0 -30
  237. data/lib/httpx/plugins/compression/brotli.rb +0 -54
  238. data/lib/httpx/plugins/compression/deflate.rb +0 -49
  239. data/lib/httpx/plugins/compression/gzip.rb +0 -88
  240. data/lib/httpx/plugins/compression.rb +0 -164
  241. data/lib/httpx/plugins/multipart/decoder.rb +0 -187
  242. data/lib/httpx/plugins/multipart.rb +0 -84
  243. data/lib/httpx/registry.rb +0 -85
  244. data/sig/plugins/authentication.rbs +0 -11
  245. data/sig/plugins/compression/brotli.rbs +0 -21
  246. data/sig/plugins/compression/deflate.rbs +0 -17
  247. data/sig/plugins/compression/gzip.rbs +0 -29
  248. data/sig/registry.rbs +0 -12
  249. /data/sig/plugins/{authentication → auth}/ntlm.rbs +0 -0
  250. /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 certain errors happen.
6
+ # This plugin adds support for retrying requests when errors happen.
7
7
  #
8
- # https://gitlab.com/honeyryderchuck/httpx/wikis/Retries
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 = %i[get options head put delete].freeze
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 * (0.5 * (1 + rand)) }
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.positive?
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.to_i)
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
- response.close if response.respond_to?(:close)
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
- log { "retrying (elapsed time: #{Utils.elapsed_time(retry_start)})!!" }
116
- connection = find_connection(request, connections, options)
117
- connection.send(request)
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
- connection = find_connection(request, connections, options)
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
@@ -2,17 +2,15 @@
2
2
 
3
3
  module HTTPX
4
4
  class StreamResponse
5
- def initialize(request, session, connections)
5
+ def initialize(request, session)
6
6
  @request = request
7
7
  @session = session
8
- @connections = connections
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
- @session.__send__(:receive_requests, [@request], @connections) until @request.response
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/honeyryderchuck/httpx/wikis/Stream
95
+ # https://gitlab.com/os85/httpx/wikis/Stream
95
96
  #
96
97
  module Stream
97
- module InstanceMethods
98
- private
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
- connections = _send_requests(requests)
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.stream
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
- @stream.on_chunk(chunk.to_s.dup)
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/honeyryderchuck/httpx/wikis/Upgrade#h2
9
+ # https://gitlab.com/os85/httpx/wikis/Connection-Upgrade#h2
10
10
  #
11
11
  module H2
12
12
  class << self
13
- def configure(klass)
14
- klass.default_options.upgrade_handlers.register "h2", self
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 = :h2
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/honeyryderchuck/httpx/wikis/Upgrade
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 = Module.new do
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 registry" unless value.respond_to?(:registry)
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.respond_to?(:headers) && response.headers.key?("upgrade")
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.registry.key?(upgrade_protocol)
41
+ return response unless upgrade_protocol && options.upgrade_handlers.key?(upgrade_protocol)
43
42
 
44
- protocol_handler = options.upgrade_handlers.registry(upgrade_protocol)
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