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
data/lib/httpx/errors.rb CHANGED
@@ -1,18 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HTTPX
4
+ # the default exception class for exceptions raised by HTTPX.
4
5
  class Error < StandardError; end
5
6
 
6
7
  class UnsupportedSchemeError < Error; end
7
8
 
9
+ class ConnectionError < Error; end
10
+
11
+ # Error raised when there was a timeout. Its subclasses allow for finer-grained
12
+ # control of which timeout happened.
8
13
  class TimeoutError < Error
14
+ # The timeout value which caused this error to be raised.
9
15
  attr_reader :timeout
10
16
 
17
+ # initializes the timeout exception with the +timeout+ causing the error, and the
18
+ # error +message+ for it.
11
19
  def initialize(timeout, message)
12
20
  @timeout = timeout
13
21
  super(message)
14
22
  end
15
23
 
24
+ # clones this error into a HTTPX::ConnectionTimeoutError.
16
25
  def to_connection_error
17
26
  ex = ConnectTimeoutError.new(@timeout, message)
18
27
  ex.set_backtrace(backtrace)
@@ -20,13 +29,19 @@ module HTTPX
20
29
  end
21
30
  end
22
31
 
23
- class TotalTimeoutError < TimeoutError; end
24
-
32
+ # Error raised when there was a timeout establishing the connection to a server.
33
+ # This may be raised due to timeouts during TCP and TLS (when applicable) connection
34
+ # establishment.
25
35
  class ConnectTimeoutError < TimeoutError; end
26
36
 
37
+ # Error raised when there was a timeout while sending a request, or receiving a response
38
+ # from the server.
27
39
  class RequestTimeoutError < TimeoutError
40
+ # The HTTPX::Request request object this exception refers to.
28
41
  attr_reader :request
29
42
 
43
+ # initializes the exception with the +request+ and +response+ it refers to, and the
44
+ # +timeout+ causing the error, and the
30
45
  def initialize(request, response, timeout)
31
46
  @request = request
32
47
  @response = response
@@ -38,19 +53,28 @@ module HTTPX
38
53
  end
39
54
  end
40
55
 
56
+ # Error raised when there was a timeout while receiving a response from the server.
41
57
  class ReadTimeoutError < RequestTimeoutError; end
42
58
 
59
+ # Error raised when there was a timeout while sending a request from the server.
43
60
  class WriteTimeoutError < RequestTimeoutError; end
44
61
 
62
+ # Error raised when there was a timeout while waiting for the HTTP/2 settings frame from the server.
45
63
  class SettingsTimeoutError < TimeoutError; end
46
64
 
65
+ # Error raised when there was a timeout while resolving a domain to an IP.
47
66
  class ResolveTimeoutError < TimeoutError; end
48
67
 
68
+ # Error raised when there was an error while resolving a domain to an IP.
49
69
  class ResolveError < Error; end
50
70
 
71
+ # Error raised when there was an error while resolving a domain to an IP
72
+ # using a HTTPX::Resolver::Native resolver.
51
73
  class NativeResolveError < ResolveError
52
74
  attr_reader :connection, :host
53
75
 
76
+ # initializes the exception with the +connection+ it refers to, the +host+ domain
77
+ # which failed to resolve, and the error +message+.
54
78
  def initialize(connection, host, message = "Can't resolve #{host}")
55
79
  @connection = connection
56
80
  @host = host
@@ -58,18 +82,26 @@ module HTTPX
58
82
  end
59
83
  end
60
84
 
85
+ # The exception class for HTTP responses with 4xx or 5xx status.
61
86
  class HTTPError < Error
87
+ # The HTTPX::Response response object this exception refers to.
62
88
  attr_reader :response
63
89
 
90
+ # Creates the instance and assigns the HTTPX::Response +response+.
64
91
  def initialize(response)
65
92
  @response = response
66
93
  super("HTTP Error: #{@response.status} #{@response.headers}\n#{@response.body}")
67
94
  end
68
95
 
96
+ # The HTTP response status.
97
+ #
98
+ # error.status #=> 404
69
99
  def status
70
100
  @response.status
71
101
  end
72
102
  end
73
103
 
104
+ # error raised when a request was sent a server which can't reproduce a response, and
105
+ # has therefore returned an HTTP response using the 421 status code.
74
106
  class MisdirectedRequestError < HTTPError; end
75
107
  end
@@ -3,97 +3,10 @@
3
3
  require "uri"
4
4
 
5
5
  module HTTPX
6
- unless Method.method_defined?(:curry)
7
-
8
- # Backport
9
- #
10
- # Ruby 2.1 and lower implement curry only for Procs.
11
- #
12
- # Why not using Refinements? Because they don't work for Method (tested with ruby 2.1.9).
13
- #
14
- module CurryMethods
15
- # Backport for the Method#curry method, which is part of ruby core since 2.2 .
16
- #
17
- def curry(*args)
18
- to_proc.curry(*args)
19
- end
20
- end
21
- Method.__send__(:include, CurryMethods)
22
- end
23
-
24
- unless String.method_defined?(:+@)
25
- # Backport for +"", to initialize unfrozen strings from the string literal.
26
- #
27
- module LiteralStringExtensions
28
- def +@
29
- frozen? ? dup : self
30
- end
31
- end
32
- String.__send__(:include, LiteralStringExtensions)
33
- end
34
-
35
- unless Numeric.method_defined?(:positive?)
36
- # Ruby 2.3 Backport (Numeric#positive?)
37
- #
38
- module PosMethods
39
- def positive?
40
- self > 0
41
- end
42
- end
43
- Numeric.__send__(:include, PosMethods)
44
- end
45
-
46
- unless Numeric.method_defined?(:negative?)
47
- # Ruby 2.3 Backport (Numeric#negative?)
48
- #
49
- module NegMethods
50
- def negative?
51
- self < 0
52
- end
53
- end
54
- Numeric.__send__(:include, NegMethods)
55
- end
56
-
57
- module NumericExtensions
58
- refine Numeric do
59
- def infinite?
60
- self == Float::INFINITY
61
- end unless Numeric.method_defined?(:infinite?)
62
- end
63
- end
64
-
65
- module StringExtensions
66
- refine String do
67
- def delete_suffix!(suffix)
68
- suffix = Backports.coerce_to_str(suffix)
69
- chomp! if frozen?
70
- len = suffix.length
71
- if len > 0 && index(suffix, -len)
72
- self[-len..-1] = ''
73
- self
74
- else
75
- nil
76
- end
77
- end unless String.method_defined?(:delete_suffix!)
78
- end
79
- end
80
-
81
- module HashExtensions
82
- refine Hash do
83
- def compact
84
- h = {}
85
- each do |key, value|
86
- h[key] = value unless value == nil
87
- end
88
- h
89
- end unless Hash.method_defined?(:compact)
90
- end
91
- end
92
-
93
6
  module ArrayExtensions
94
7
  module FilterMap
95
8
  refine Array do
96
-
9
+ # Ruby 2.7 backport
97
10
  def filter_map
98
11
  return to_enum(:filter_map) unless block_given?
99
12
 
@@ -105,17 +18,9 @@ module HTTPX
105
18
  end unless Array.method_defined?(:filter_map)
106
19
  end
107
20
 
108
- module Sum
109
- refine Array do
110
- def sum(accumulator = 0, &block)
111
- values = block_given? ? map(&block) : self
112
- values.inject(accumulator, :+)
113
- end
114
- end unless Array.method_defined?(:sum)
115
- end
116
-
117
21
  module Intersect
118
22
  refine Array do
23
+ # Ruby 3.1 backport
119
24
  def intersect?(arr)
120
25
  if size < arr.size
121
26
  smaller = self
@@ -128,30 +33,10 @@ module HTTPX
128
33
  end
129
34
  end
130
35
 
131
- module IOExtensions
132
- refine IO do
133
- # provides a fallback for rubies where IO#wait isn't implemented,
134
- # but IO#wait_readable and IO#wait_writable are.
135
- def wait(timeout = nil, _mode = :read_write)
136
- r, w = IO.select([self], [self], nil, timeout)
137
-
138
- return unless r || w
139
-
140
- self
141
- end unless IO.method_defined?(:wait) && IO.instance_method(:wait).arity == 2
142
- end
143
- end
144
-
145
- module RegexpExtensions
146
- refine(Regexp) do
147
- def match?(*args)
148
- !match(*args).nil?
149
- end
150
- end
151
- end
152
-
153
36
  module URIExtensions
37
+ # uri 0.11 backport, ships with ruby 3.1
154
38
  refine URI::Generic do
39
+
155
40
  def non_ascii_hostname
156
41
  @non_ascii_hostname
157
42
  end
@@ -169,21 +54,6 @@ module HTTPX
169
54
  def origin
170
55
  "#{scheme}://#{authority}"
171
56
  end unless URI::HTTP.method_defined?(:origin)
172
-
173
- def altsvc_match?(uri)
174
- uri = URI.parse(uri)
175
-
176
- origin == uri.origin || begin
177
- case scheme
178
- when "h2"
179
- (uri.scheme == "https" || uri.scheme == "h2") &&
180
- host == uri.host &&
181
- (port || default_port) == (uri.port || uri.default_port)
182
- else
183
- false
184
- end
185
- end
186
- end
187
57
  end
188
58
  end
189
59
  end
data/lib/httpx/io/ssl.rb CHANGED
@@ -4,26 +4,48 @@ require "openssl"
4
4
 
5
5
  module HTTPX
6
6
  TLSError = OpenSSL::SSL::SSLError
7
- IPRegex = Regexp.union(Resolv::IPv4::Regex, Resolv::IPv6::Regex)
8
7
 
9
8
  class SSL < TCP
10
- using RegexpExtensions unless Regexp.method_defined?(:match?)
9
+ # rubocop:disable Style/MutableConstant
10
+ TLS_OPTIONS = { alpn_protocols: %w[h2 http/1.1].freeze }
11
+ # https://github.com/jruby/jruby-openssl/issues/284
12
+ TLS_OPTIONS[:verify_hostname] = true if RUBY_ENGINE == "jruby"
13
+ # rubocop:enable Style/MutableConstant
14
+ TLS_OPTIONS.freeze
11
15
 
12
- TLS_OPTIONS = if OpenSSL::SSL::SSLContext.instance_methods.include?(:alpn_protocols)
13
- { alpn_protocols: %w[h2 http/1.1].freeze }.freeze
14
- else
15
- {}.freeze
16
- end
16
+ attr_writer :ssl_session
17
17
 
18
18
  def initialize(_, _, options)
19
19
  super
20
- @ctx = OpenSSL::SSL::SSLContext.new
20
+
21
21
  ctx_options = TLS_OPTIONS.merge(options.ssl)
22
22
  @sni_hostname = ctx_options.delete(:hostname) || @hostname
23
- @ctx.set_params(ctx_options) unless ctx_options.empty?
24
- @state = :negotiated if @keep_open
25
23
 
26
- @hostname_is_ip = IPRegex.match?(@sni_hostname)
24
+ if @keep_open && @io.is_a?(OpenSSL::SSL::SSLSocket)
25
+ # externally initiated ssl socket
26
+ @ctx = @io.context
27
+ @state = :negotiated
28
+ else
29
+ @ctx = OpenSSL::SSL::SSLContext.new
30
+ @ctx.set_params(ctx_options) unless ctx_options.empty?
31
+ unless @ctx.session_cache_mode.nil? # a dummy method on JRuby
32
+ @ctx.session_cache_mode =
33
+ OpenSSL::SSL::SSLContext::SESSION_CACHE_CLIENT | OpenSSL::SSL::SSLContext::SESSION_CACHE_NO_INTERNAL_STORE
34
+ end
35
+
36
+ yield(self) if block_given?
37
+ end
38
+
39
+ @verify_hostname = @ctx.verify_hostname
40
+ end
41
+
42
+ if OpenSSL::SSL::SSLContext.method_defined?(:session_new_cb=)
43
+ def session_new_cb(&pr)
44
+ @ctx.session_new_cb = proc { |_, sess| pr.call(sess) }
45
+ end
46
+ else
47
+ # session_new_cb not implemented under JRuby
48
+ def session_new_cb; end
27
49
  end
28
50
 
29
51
  def protocol
@@ -32,6 +54,20 @@ module HTTPX
32
54
  super
33
55
  end
34
56
 
57
+ if RUBY_ENGINE == "jruby"
58
+ # in jruby, alpn_protocol may return ""
59
+ # https://github.com/jruby/jruby-openssl/issues/287
60
+ def protocol
61
+ proto = @io.alpn_protocol
62
+
63
+ return super if proto.nil? || proto.empty?
64
+
65
+ proto
66
+ rescue StandardError
67
+ super
68
+ end
69
+ end
70
+
35
71
  def can_verify_peer?
36
72
  @ctx.verify_mode == OpenSSL::SSL::VERIFY_PEER
37
73
  end
@@ -43,85 +79,54 @@ module HTTPX
43
79
  OpenSSL::SSL.verify_certificate_identity(@io.peer_cert, host)
44
80
  end
45
81
 
46
- def close
47
- super
48
- # allow reconnections
49
- # connect only works if initial @io is a socket
50
- @io = @io.io if @io.respond_to?(:io)
51
- end
52
-
53
82
  def connected?
54
83
  @state == :negotiated
55
84
  end
56
85
 
86
+ def expired?
87
+ super || ssl_session_expired?
88
+ end
89
+
90
+ def ssl_session_expired?
91
+ @ssl_session.nil? || Process.clock_gettime(Process::CLOCK_REALTIME) >= (@ssl_session.time.to_f + @ssl_session.timeout)
92
+ end
93
+
57
94
  def connect
58
95
  super
59
96
  return if @state == :negotiated ||
60
97
  @state != :connected
61
98
 
62
99
  unless @io.is_a?(OpenSSL::SSL::SSLSocket)
100
+ if (hostname_is_ip = (@ip == @sni_hostname))
101
+ # IPv6 address would be "[::1]", must turn to "0000:0000:0000:0000:0000:0000:0000:0001" for cert SAN check
102
+ @sni_hostname = @ip.to_string
103
+ # IP addresses in SNI is not valid per RFC 6066, section 3.
104
+ @ctx.verify_hostname = false
105
+ end
106
+
63
107
  @io = OpenSSL::SSL::SSLSocket.new(@io, @ctx)
64
- @io.hostname = @sni_hostname unless @hostname_is_ip
108
+
109
+ @io.hostname = @sni_hostname unless hostname_is_ip
110
+ @io.session = @ssl_session unless ssl_session_expired?
65
111
  @io.sync_close = true
66
112
  end
67
113
  try_ssl_connect
68
114
  end
69
115
 
70
- if RUBY_VERSION < "2.3"
71
- # :nocov:
72
- def try_ssl_connect
73
- @io.connect_nonblock
74
- @io.post_connection_check(@sni_hostname) if @ctx.verify_mode != OpenSSL::SSL::VERIFY_NONE && !@hostname_is_ip
75
- transition(:negotiated)
76
- @interests = :w
77
- rescue ::IO::WaitReadable
116
+ def try_ssl_connect
117
+ ret = @io.connect_nonblock(exception: false)
118
+ log(level: 3, color: :cyan) { "TLS CONNECT: #{ret}..." }
119
+ case ret
120
+ when :wait_readable
78
121
  @interests = :r
79
- rescue ::IO::WaitWritable
122
+ return
123
+ when :wait_writable
80
124
  @interests = :w
125
+ return
81
126
  end
82
-
83
- def read(_, buffer)
84
- super
85
- rescue ::IO::WaitWritable
86
- buffer.clear
87
- 0
88
- end
89
-
90
- def write(*)
91
- super
92
- rescue ::IO::WaitReadable
93
- 0
94
- end
95
- # :nocov:
96
- else
97
- def try_ssl_connect
98
- case @io.connect_nonblock(exception: false)
99
- when :wait_readable
100
- @interests = :r
101
- return
102
- when :wait_writable
103
- @interests = :w
104
- return
105
- end
106
- @io.post_connection_check(@sni_hostname) if @ctx.verify_mode != OpenSSL::SSL::VERIFY_NONE && !@hostname_is_ip
107
- transition(:negotiated)
108
- @interests = :w
109
- end
110
-
111
- # :nocov:
112
- if OpenSSL::VERSION < "2.0.6"
113
- def read(size, buffer)
114
- @io.read_nonblock(size, buffer)
115
- buffer.bytesize
116
- rescue ::IO::WaitReadable,
117
- ::IO::WaitWritable
118
- buffer.clear
119
- 0
120
- rescue EOFError
121
- nil
122
- end
123
- end
124
- # :nocov:
127
+ @io.post_connection_check(@sni_hostname) if @ctx.verify_mode != OpenSSL::SSL::VERIFY_NONE && @verify_hostname
128
+ transition(:negotiated)
129
+ @interests = :w
125
130
  end
126
131
 
127
132
  private
@@ -130,6 +135,7 @@ module HTTPX
130
135
  case nextstate
131
136
  when :negotiated
132
137
  return unless @state == :connected
138
+
133
139
  when :closed
134
140
  return unless @state == :negotiated ||
135
141
  @state == :connected
data/lib/httpx/io/tcp.rb CHANGED
@@ -38,7 +38,10 @@ module HTTPX
38
38
  add_addresses(addresses)
39
39
  end
40
40
  @ip_index = @addresses.size - 1
41
- # @io ||= build_socket
41
+ end
42
+
43
+ def socket
44
+ @io
42
45
  end
43
46
 
44
47
  def add_addresses(addrs)
@@ -74,7 +77,8 @@ module HTTPX
74
77
  try_connect
75
78
  rescue Errno::ECONNREFUSED,
76
79
  Errno::EADDRNOTAVAIL,
77
- Errno::EHOSTUNREACH => e
80
+ Errno::EHOSTUNREACH,
81
+ SocketError => e
78
82
  raise e if @ip_index <= 0
79
83
 
80
84
  log { "failed connecting to #{@ip} (#{e.message}), trying next..." }
@@ -90,84 +94,45 @@ module HTTPX
90
94
  retry
91
95
  end
92
96
 
93
- if RUBY_VERSION < "2.3"
94
- # :nocov:
95
- def try_connect
96
- @io.connect_nonblock(Socket.sockaddr_in(@port, @ip.to_s))
97
- rescue ::IO::WaitWritable, Errno::EALREADY
98
- @interests = :w
99
- rescue ::IO::WaitReadable
97
+ def try_connect
98
+ ret = @io.connect_nonblock(Socket.sockaddr_in(@port, @ip.to_s), exception: false)
99
+ log(level: 3, color: :cyan) { "TCP CONNECT: #{ret}..." }
100
+ case ret
101
+ when :wait_readable
100
102
  @interests = :r
101
- rescue Errno::EISCONN
102
- transition(:connected)
103
- @interests = :w
104
- else
105
- transition(:connected)
103
+ return
104
+ when :wait_writable
106
105
  @interests = :w
106
+ return
107
107
  end
108
- private :try_connect
108
+ transition(:connected)
109
+ @interests = :w
110
+ rescue Errno::EALREADY
111
+ @interests = :w
112
+ end
113
+ private :try_connect
109
114
 
110
- def read(size, buffer)
111
- @io.read_nonblock(size, buffer)
112
- log { "READ: #{buffer.bytesize} bytes..." }
113
- buffer.bytesize
114
- rescue ::IO::WaitReadable
115
+ def read(size, buffer)
116
+ ret = @io.read_nonblock(size, buffer, exception: false)
117
+ if ret == :wait_readable
115
118
  buffer.clear
116
- 0
117
- rescue EOFError
118
- nil
119
- end
120
-
121
- def write(buffer)
122
- siz = @io.write_nonblock(buffer)
123
- log { "WRITE: #{siz} bytes..." }
124
- buffer.shift!(siz)
125
- siz
126
- rescue ::IO::WaitWritable
127
- 0
128
- rescue EOFError
129
- nil
130
- end
131
- # :nocov:
132
- else
133
- def try_connect
134
- case @io.connect_nonblock(Socket.sockaddr_in(@port, @ip.to_s), exception: false)
135
- when :wait_readable
136
- @interests = :r
137
- return
138
- when :wait_writable
139
- @interests = :w
140
- return
141
- end
142
- transition(:connected)
143
- @interests = :w
144
- rescue Errno::EALREADY
145
- @interests = :w
119
+ return 0
146
120
  end
147
- private :try_connect
148
-
149
- def read(size, buffer)
150
- ret = @io.read_nonblock(size, buffer, exception: false)
151
- if ret == :wait_readable
152
- buffer.clear
153
- return 0
154
- end
155
- return if ret.nil?
121
+ return if ret.nil?
156
122
 
157
- log { "READ: #{buffer.bytesize} bytes..." }
158
- buffer.bytesize
159
- end
123
+ log { "READ: #{buffer.bytesize} bytes..." }
124
+ buffer.bytesize
125
+ end
160
126
 
161
- def write(buffer)
162
- siz = @io.write_nonblock(buffer, exception: false)
163
- return 0 if siz == :wait_writable
164
- return if siz.nil?
127
+ def write(buffer)
128
+ siz = @io.write_nonblock(buffer, exception: false)
129
+ return 0 if siz == :wait_writable
130
+ return if siz.nil?
165
131
 
166
- log { "WRITE: #{siz} bytes..." }
132
+ log { "WRITE: #{siz} bytes..." }
167
133
 
168
- buffer.shift!(siz)
169
- siz
170
- end
134
+ buffer.shift!(siz)
135
+ siz
171
136
  end
172
137
 
173
138
  def close
@@ -188,6 +153,17 @@ module HTTPX
188
153
  @state == :idle || @state == :closed
189
154
  end
190
155
 
156
+ def expired?
157
+ # do not mess with external sockets
158
+ return false if @options.io
159
+
160
+ return true unless @addresses
161
+
162
+ resolver_addresses = Resolver.nolookup_resolve(@hostname)
163
+
164
+ (Array(resolver_addresses) & @addresses).empty?
165
+ end
166
+
191
167
  # :nocov:
192
168
  def inspect
193
169
  "#<#{self.class}: #{@ip}:#{@port} (state: #{@state})>"