httpx 0.20.0 → 1.3.1

Sign up to get free protection for your applications and to get access to all the features.
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