httpx 0.21.0 → 1.2.1

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