httpx 0.21.0 → 1.2.1

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