httpx 0.11.0 → 0.13.0

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 (78) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/doc/release_notes/0_11_1.md +5 -0
  4. data/doc/release_notes/0_11_2.md +5 -0
  5. data/doc/release_notes/0_11_3.md +5 -0
  6. data/doc/release_notes/0_12_0.md +55 -0
  7. data/doc/release_notes/0_13_0.md +58 -0
  8. data/lib/httpx.rb +2 -1
  9. data/lib/httpx/adapters/faraday.rb +4 -6
  10. data/lib/httpx/altsvc.rb +1 -0
  11. data/lib/httpx/chainable.rb +2 -2
  12. data/lib/httpx/connection.rb +80 -28
  13. data/lib/httpx/connection/http1.rb +19 -6
  14. data/lib/httpx/connection/http2.rb +32 -25
  15. data/lib/httpx/io.rb +16 -3
  16. data/lib/httpx/io/ssl.rb +35 -24
  17. data/lib/httpx/io/tcp.rb +48 -28
  18. data/lib/httpx/io/tls.rb +218 -0
  19. data/lib/httpx/io/tls/box.rb +365 -0
  20. data/lib/httpx/io/tls/context.rb +199 -0
  21. data/lib/httpx/io/tls/ffi.rb +390 -0
  22. data/lib/httpx/io/udp.rb +3 -2
  23. data/lib/httpx/io/unix.rb +27 -12
  24. data/lib/httpx/options.rb +11 -23
  25. data/lib/httpx/parser/http1.rb +4 -4
  26. data/lib/httpx/plugins/aws_sdk_authentication.rb +81 -0
  27. data/lib/httpx/plugins/aws_sigv4.rb +218 -0
  28. data/lib/httpx/plugins/compression.rb +21 -9
  29. data/lib/httpx/plugins/compression/brotli.rb +8 -6
  30. data/lib/httpx/plugins/compression/deflate.rb +4 -7
  31. data/lib/httpx/plugins/compression/gzip.rb +2 -2
  32. data/lib/httpx/plugins/cookies/set_cookie_parser.rb +1 -1
  33. data/lib/httpx/plugins/digest_authentication.rb +1 -1
  34. data/lib/httpx/plugins/follow_redirects.rb +1 -1
  35. data/lib/httpx/plugins/h2c.rb +43 -58
  36. data/lib/httpx/plugins/internal_telemetry.rb +93 -0
  37. data/lib/httpx/plugins/multipart.rb +2 -0
  38. data/lib/httpx/plugins/multipart/encoder.rb +4 -9
  39. data/lib/httpx/plugins/proxy.rb +1 -1
  40. data/lib/httpx/plugins/proxy/http.rb +1 -1
  41. data/lib/httpx/plugins/proxy/socks4.rb +8 -0
  42. data/lib/httpx/plugins/proxy/socks5.rb +8 -0
  43. data/lib/httpx/plugins/push_promise.rb +3 -2
  44. data/lib/httpx/plugins/retries.rb +2 -2
  45. data/lib/httpx/plugins/stream.rb +6 -6
  46. data/lib/httpx/plugins/upgrade.rb +83 -0
  47. data/lib/httpx/plugins/upgrade/h2.rb +54 -0
  48. data/lib/httpx/pool.rb +14 -6
  49. data/lib/httpx/registry.rb +1 -7
  50. data/lib/httpx/request.rb +11 -1
  51. data/lib/httpx/resolver/https.rb +3 -11
  52. data/lib/httpx/response.rb +14 -7
  53. data/lib/httpx/selector.rb +5 -0
  54. data/lib/httpx/session.rb +25 -2
  55. data/lib/httpx/transcoder/body.rb +3 -5
  56. data/lib/httpx/version.rb +1 -1
  57. data/sig/chainable.rbs +2 -1
  58. data/sig/connection/http1.rbs +3 -2
  59. data/sig/connection/http2.rbs +5 -3
  60. data/sig/options.rbs +7 -20
  61. data/sig/plugins/aws_sdk_authentication.rbs +17 -0
  62. data/sig/plugins/aws_sigv4.rbs +64 -0
  63. data/sig/plugins/compression.rbs +5 -3
  64. data/sig/plugins/compression/brotli.rbs +1 -1
  65. data/sig/plugins/compression/deflate.rbs +1 -1
  66. data/sig/plugins/compression/gzip.rbs +1 -1
  67. data/sig/plugins/cookies.rbs +0 -1
  68. data/sig/plugins/digest_authentication.rbs +0 -1
  69. data/sig/plugins/expect.rbs +0 -2
  70. data/sig/plugins/follow_redirects.rbs +0 -2
  71. data/sig/plugins/h2c.rbs +5 -10
  72. data/sig/plugins/persistent.rbs +0 -1
  73. data/sig/plugins/proxy.rbs +0 -1
  74. data/sig/plugins/push_promise.rbs +1 -1
  75. data/sig/plugins/retries.rbs +0 -4
  76. data/sig/plugins/upgrade.rbs +23 -0
  77. data/sig/response.rbs +3 -1
  78. metadata +48 -26
@@ -42,11 +42,15 @@ module HTTPX
42
42
  return @buffer.empty? ? :r : :rw
43
43
  end
44
44
 
45
- return :w unless @pending.empty?
45
+ return :w if !@pending.empty? && can_buffer_more_requests?
46
46
 
47
47
  return :w if @streams.each_key.any? { |r| r.interests == :w }
48
48
 
49
- return :r if @buffer.empty?
49
+ if @buffer.empty?
50
+ return if @streams.empty? && @pings.empty?
51
+
52
+ return :r
53
+ end
50
54
 
51
55
  :rw
52
56
  end
@@ -70,10 +74,14 @@ module HTTPX
70
74
  @connection << data
71
75
  end
72
76
 
77
+ def can_buffer_more_requests?
78
+ @handshake_completed &&
79
+ @streams.size < @max_concurrent_requests &&
80
+ @streams.size < @max_requests
81
+ end
82
+
73
83
  def send(request)
74
- if !@handshake_completed ||
75
- @streams.size >= @max_concurrent_requests ||
76
- @streams.size >= @max_requests
84
+ unless can_buffer_more_requests?
77
85
  @pending << request
78
86
  return
79
87
  end
@@ -126,8 +134,6 @@ module HTTPX
126
134
  request.path
127
135
  end
128
136
 
129
- def set_request_headers(request); end
130
-
131
137
  def handle(request, stream)
132
138
  catch(:buffer_full) do
133
139
  request.transition(:headers)
@@ -172,18 +178,19 @@ module HTTPX
172
178
  stream.on(:data, &method(:on_stream_data).curry(3)[stream, request])
173
179
  end
174
180
 
181
+ def set_protocol_headers(request)
182
+ request.headers[":scheme"] = request.scheme
183
+ request.headers[":method"] = request.verb.to_s.upcase
184
+ request.headers[":path"] = headline_uri(request)
185
+ request.headers[":authority"] = request.authority
186
+ end
187
+
175
188
  def join_headers(stream, request)
176
- set_request_headers(request)
177
- headers = {}
178
- headers[":scheme"] = request.scheme
179
- headers[":method"] = request.verb.to_s.upcase
180
- headers[":path"] = headline_uri(request)
181
- headers[":authority"] = request.authority
182
- headers = headers.merge(request.headers)
189
+ set_protocol_headers(request)
183
190
  log(level: 1, color: :yellow) do
184
- headers.map { |k, v| "#{stream.id}: -> HEADER: #{k}: #{v}" }.join("\n")
191
+ request.headers.each.map { |k, v| "#{stream.id}: -> HEADER: #{k}: #{v}" }.join("\n")
185
192
  end
186
- stream.headers(headers, end_stream: request.empty?)
193
+ stream.headers(request.headers.each, end_stream: request.empty?)
187
194
  end
188
195
 
189
196
  def join_body(stream, request)
@@ -227,10 +234,15 @@ module HTTPX
227
234
  end
228
235
 
229
236
  def on_stream_close(stream, request, error)
237
+ log(level: 2) { "#{stream.id}: closing stream" }
238
+ @drains.delete(request)
239
+ @streams.delete(request)
240
+
230
241
  if error && error != :no_error
231
242
  ex = Error.new(stream.id, error)
232
243
  ex.set_backtrace(caller)
233
- emit(:error, request, ex)
244
+ response = ErrorResponse.new(request, ex, request.options)
245
+ emit(:response, request, response)
234
246
  else
235
247
  response = request.response
236
248
  if response.status == 421
@@ -241,9 +253,6 @@ module HTTPX
241
253
  emit(:response, request, response)
242
254
  end
243
255
  end
244
- log(level: 2) { "#{stream.id}: closing stream" }
245
-
246
- @streams.delete(request)
247
256
  send(@pending.shift) unless @pending.empty?
248
257
  return unless @streams.empty? && exhausted?
249
258
 
@@ -328,11 +337,9 @@ module HTTPX
328
337
  end
329
338
 
330
339
  def method_missing(meth, *args, &blk)
331
- if @connection.respond_to?(meth)
332
- @connection.__send__(meth, *args, &blk)
333
- else
334
- super
335
- end
340
+ return super unless @connection.respond_to?(meth)
341
+
342
+ @connection.__send__(meth, *args, &blk)
336
343
  end
337
344
  end
338
345
  Connection.register "h2", Connection::HTTP2
data/lib/httpx/io.rb CHANGED
@@ -2,16 +2,29 @@
2
2
 
3
3
  require "socket"
4
4
  require "httpx/io/tcp"
5
- require "httpx/io/ssl"
6
5
  require "httpx/io/unix"
7
6
  require "httpx/io/udp"
8
7
 
9
8
  module HTTPX
10
9
  module IO
11
10
  extend Registry
12
- register "tcp", TCP
13
- register "ssl", SSL
14
11
  register "udp", UDP
15
12
  register "unix", HTTPX::UNIX
13
+ register "tcp", TCP
14
+
15
+ if RUBY_ENGINE == "jruby"
16
+ begin
17
+ require "httpx/io/tls"
18
+ register "ssl", TLS
19
+ rescue LoadError
20
+ # :nocov:
21
+ require "httpx/io/ssl"
22
+ register "ssl", SSL
23
+ # :nocov:
24
+ end
25
+ else
26
+ require "httpx/io/ssl"
27
+ register "ssl", SSL
28
+ end
16
29
  end
17
30
  end
data/lib/httpx/io/ssl.rb CHANGED
@@ -3,6 +3,8 @@
3
3
  require "openssl"
4
4
 
5
5
  module HTTPX
6
+ TLSError = OpenSSL::SSL::SSLError
7
+
6
8
  class SSL < TCP
7
9
  TLS_OPTIONS = if OpenSSL::SSL::SSLContext.instance_methods.include?(:alpn_protocols)
8
10
  { alpn_protocols: %w[h2 http/1.1] }
@@ -11,19 +13,14 @@ module HTTPX
11
13
  end
12
14
 
13
15
  def initialize(_, _, options)
16
+ super
14
17
  @ctx = OpenSSL::SSL::SSLContext.new
15
18
  ctx_options = TLS_OPTIONS.merge(options.ssl)
16
- @tls_hostname = ctx_options.delete(:hostname)
19
+ @sni_hostname = ctx_options.delete(:hostname) || @hostname
17
20
  @ctx.set_params(ctx_options) unless ctx_options.empty?
18
- super
19
- @tls_hostname ||= @hostname
20
21
  @state = :negotiated if @keep_open
21
22
  end
22
23
 
23
- def interests
24
- @interests || super
25
- end
26
-
27
24
  def protocol
28
25
  @io.alpn_protocol || super
29
26
  rescue StandardError
@@ -50,28 +47,30 @@ module HTTPX
50
47
 
51
48
  def connect
52
49
  super
53
- if @keep_open
54
- @state = :negotiated
55
- return
56
- end
57
50
  return if @state == :negotiated ||
58
51
  @state != :connected
59
52
 
60
53
  unless @io.is_a?(OpenSSL::SSL::SSLSocket)
61
54
  @io = OpenSSL::SSL::SSLSocket.new(@io, @ctx)
62
- @io.hostname = @tls_hostname
55
+ @io.hostname = @sni_hostname
63
56
  @io.sync_close = true
64
57
  end
65
- @io.connect_nonblock
66
- @io.post_connection_check(@tls_hostname) if @ctx.verify_mode != OpenSSL::SSL::VERIFY_NONE
67
- transition(:negotiated)
68
- rescue ::IO::WaitReadable
69
- @interests = :r
70
- rescue ::IO::WaitWritable
71
- @interests = :w
58
+ try_ssl_connect
72
59
  end
73
60
 
74
61
  if RUBY_VERSION < "2.3"
62
+ # :nocov:
63
+ def try_ssl_connect
64
+ @io.connect_nonblock
65
+ @io.post_connection_check(@sni_hostname) if @ctx.verify_mode != OpenSSL::SSL::VERIFY_NONE
66
+ transition(:negotiated)
67
+ @interests = :w
68
+ rescue ::IO::WaitReadable
69
+ @interests = :r
70
+ rescue ::IO::WaitWritable
71
+ @interests = :w
72
+ end
73
+
75
74
  def read(_, buffer)
76
75
  super
77
76
  rescue ::IO::WaitWritable
@@ -84,7 +83,23 @@ module HTTPX
84
83
  rescue ::IO::WaitReadable
85
84
  0
86
85
  end
86
+ # :nocov:
87
87
  else
88
+ def try_ssl_connect
89
+ case @io.connect_nonblock(exception: false)
90
+ when :wait_readable
91
+ @interests = :r
92
+ return
93
+ when :wait_writable
94
+ @interests = :w
95
+ return
96
+ end
97
+ @io.post_connection_check(@sni_hostname) if @ctx.verify_mode != OpenSSL::SSL::VERIFY_NONE
98
+ transition(:negotiated)
99
+ @interests = :w
100
+ end
101
+
102
+ # :nocov:
88
103
  if OpenSSL::VERSION < "2.0.6"
89
104
  def read(size, buffer)
90
105
  @io.read_nonblock(size, buffer)
@@ -97,11 +112,7 @@ module HTTPX
97
112
  nil
98
113
  end
99
114
  end
100
- end
101
-
102
- def inspect
103
- id = @io.closed? ? "closed" : @io.to_io.fileno
104
- "#<SSL(fd: #{id}): #{@ip}:#{@port} state: #{@state}>"
115
+ # :nocov:
105
116
  end
106
117
 
107
118
  private
data/lib/httpx/io/tcp.rb CHANGED
@@ -7,17 +7,19 @@ module HTTPX
7
7
  class TCP
8
8
  include Loggable
9
9
 
10
- attr_reader :ip, :port, :addresses, :state
10
+ using URIExtensions
11
+
12
+ attr_reader :ip, :port, :addresses, :state, :interests
11
13
 
12
14
  alias_method :host, :ip
13
15
 
14
16
  def initialize(origin, addresses, options)
15
17
  @state = :idle
16
18
  @hostname = origin.host
17
- @addresses = addresses
18
19
  @options = Options.new(options)
19
20
  @fallback_protocol = @options.fallback_protocol
20
21
  @port = origin.port
22
+ @interests = :w
21
23
  if @options.io
22
24
  @io = case @options.io
23
25
  when Hash
@@ -25,24 +27,20 @@ module HTTPX
25
27
  else
26
28
  @options.io
27
29
  end
30
+ raise Error, "Given IO objects do not match the request authority" unless @io
31
+
28
32
  _, _, _, @ip = @io.addr
29
33
  @addresses ||= [@ip]
30
34
  @ip_index = @addresses.size - 1
31
- unless @io.nil?
32
- @keep_open = true
33
- @state = :connected
34
- end
35
+ @keep_open = true
36
+ @state = :connected
35
37
  else
36
- @ip_index = @addresses.size - 1
37
- @ip = @addresses[@ip_index]
38
+ @addresses = addresses.map { |addr| addr.is_a?(IPAddr) ? addr : IPAddr.new(addr) }
38
39
  end
40
+ @ip_index = @addresses.size - 1
39
41
  @io ||= build_socket
40
42
  end
41
43
 
42
- def interests
43
- :w
44
- end
45
-
46
44
  def to_io
47
45
  @io.to_io
48
46
  end
@@ -54,32 +52,40 @@ module HTTPX
54
52
  def connect
55
53
  return unless closed?
56
54
 
57
- begin
58
- if @io.closed?
59
- transition(:idle)
60
- @io = build_socket
61
- end
62
- @io.connect_nonblock(Socket.sockaddr_in(@port, @ip.to_s))
63
- rescue Errno::EISCONN
55
+ if @io.closed?
56
+ transition(:idle)
57
+ @io = build_socket
64
58
  end
65
- transition(:connected)
59
+ try_connect
66
60
  rescue Errno::EHOSTUNREACH => e
67
61
  raise e if @ip_index <= 0
68
62
 
69
63
  @ip_index -= 1
70
64
  retry
71
65
  rescue Errno::ETIMEDOUT => e
72
- raise ConnectTimeoutError, e.message if @ip_index <= 0
66
+ raise ConnectTimeoutError.new(@options.timeout.connect_timeout, e.message) if @ip_index <= 0
73
67
 
74
68
  @ip_index -= 1
75
69
  retry
76
- rescue Errno::EINPROGRESS,
77
- Errno::EALREADY,
78
- ::IO::WaitReadable
79
70
  end
80
71
 
81
72
  if RUBY_VERSION < "2.3"
82
73
  # :nocov:
74
+ def try_connect
75
+ @io.connect_nonblock(Socket.sockaddr_in(@port, @ip.to_s))
76
+ rescue ::IO::WaitWritable, Errno::EALREADY
77
+ @interests = :w
78
+ rescue ::IO::WaitReadable
79
+ @interests = :r
80
+ rescue Errno::EISCONN
81
+ transition(:connected)
82
+ @interests = :w
83
+ else
84
+ transition(:connected)
85
+ @interests = :w
86
+ end
87
+ private :try_connect
88
+
83
89
  def read(size, buffer)
84
90
  @io.read_nonblock(size, buffer)
85
91
  log { "READ: #{buffer.bytesize} bytes..." }
@@ -103,6 +109,20 @@ module HTTPX
103
109
  end
104
110
  # :nocov:
105
111
  else
112
+ def try_connect
113
+ case @io.connect_nonblock(Socket.sockaddr_in(@port, @ip.to_s), exception: false)
114
+ when :wait_readable
115
+ @interests = :r
116
+ return
117
+ when :wait_writable
118
+ @interests = :w
119
+ return
120
+ end
121
+ transition(:connected)
122
+ @interests = :w
123
+ end
124
+ private :try_connect
125
+
106
126
  def read(size, buffer)
107
127
  ret = @io.read_nonblock(size, buffer, exception: false)
108
128
  if ret == :wait_readable
@@ -147,14 +167,14 @@ module HTTPX
147
167
 
148
168
  # :nocov:
149
169
  def inspect
150
- id = @io.closed? ? "closed" : @io.fileno
151
- "#<TCP(fd: #{id}): #{@ip}:#{@port} (state: #{@state})>"
170
+ "#<#{self.class}: #{@ip}:#{@port} (state: #{@state})>"
152
171
  end
153
172
  # :nocov:
154
173
 
155
174
  private
156
175
 
157
176
  def build_socket
177
+ @ip = @addresses[@ip_index]
158
178
  Socket.new(@ip.family, :STREAM, 0)
159
179
  end
160
180
 
@@ -177,9 +197,9 @@ module HTTPX
177
197
  def log_transition_state(nextstate)
178
198
  case nextstate
179
199
  when :connected
180
- "Connected to #{@hostname} (#{@ip}) port #{@port} (##{@io.fileno})"
200
+ "Connected to #{host} (##{@io.fileno})"
181
201
  else
182
- "#{@ip}:#{@port} #{@state} -> #{nextstate}"
202
+ "#{host} #{@state} -> #{nextstate}"
183
203
  end
184
204
  end
185
205
  end
@@ -0,0 +1,218 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "openssl"
4
+
5
+ module HTTPX
6
+ class TLS < TCP
7
+ Error = Class.new(StandardError)
8
+
9
+ def initialize(_, _, options)
10
+ super
11
+ @encrypted = Buffer.new(Connection::BUFFER_SIZE)
12
+ @decrypted = "".b
13
+ tls_options = convert_tls_options(options.ssl)
14
+ @sni_hostname = tls_options[:hostname]
15
+ @ctx = TLS::Box.new(false, self, tls_options)
16
+ @state = :negotiated if @keep_open
17
+ end
18
+
19
+ def interests
20
+ @interests || super
21
+ end
22
+
23
+ def protocol
24
+ @protocol || super
25
+ end
26
+
27
+ def connected?
28
+ @state == :negotiated
29
+ end
30
+
31
+ def connect
32
+ super
33
+ if @keep_open
34
+ @state = :negotiated
35
+ return
36
+ end
37
+ return if @state == :negotiated ||
38
+ @state != :connected
39
+
40
+ super
41
+ @ctx.start
42
+ @interests = :r
43
+ read(@options.window_size, @decrypted)
44
+ end
45
+
46
+ # :nocov:
47
+ def inspect
48
+ id = @io.closed? ? "closed" : @io
49
+ "#<TLS(fd: #{id}): #{@ip}:#{@port} state: #{@state}>"
50
+ end
51
+ # :nocov:
52
+
53
+ alias_method :transport_close, :close
54
+ def close
55
+ transport_close
56
+ @ctx.cleanup
57
+ end
58
+
59
+ def read(*, buffer)
60
+ ret = super
61
+ return ret if !ret || ret.zero?
62
+
63
+ @ctx.decrypt(buffer.to_s.dup)
64
+ buffer.replace(@decrypted)
65
+ @decrypted.clear
66
+ buffer.bytesize
67
+ end
68
+
69
+ alias_method :unencrypted_write, :write
70
+ def write(buffer)
71
+ @ctx.encrypt(buffer.to_s.dup)
72
+ buffer.clear
73
+ do_write
74
+ end
75
+
76
+ # TLS callback.
77
+ #
78
+ # buffers the encrypted +data+
79
+ def transmit_cb(data)
80
+ log { "TLS encrypted: #{data.bytesize} bytes" }
81
+ log(level: 2) { data.inspect }
82
+ @encrypted << data
83
+ do_write
84
+ end
85
+
86
+ # TLS callback.
87
+ #
88
+ # buffers the decrypted +data+
89
+ def dispatch_cb(data)
90
+ log { "TLS decrypted: #{data.bytesize} bytes" }
91
+ log(level: 2) { data.inspect }
92
+
93
+ @decrypted << data
94
+ end
95
+
96
+ # TLS callback.
97
+ #
98
+ # signals TLS invalid status / shutdown.
99
+ def close_cb(msg = nil)
100
+ log { "TLS Error: #{msg}, closing" }
101
+ raise Error, "certificate verify failed (#{msg})"
102
+ end
103
+
104
+ # TLS callback.
105
+ #
106
+ # alpn protocol negotiation (+protocol+).
107
+ #
108
+ def alpn_protocol_cb(protocol)
109
+ @protocol = protocol
110
+ log { "TLS ALPN protocol negotiated: #{@protocol}" }
111
+ end
112
+
113
+ # TLS callback.
114
+ #
115
+ # handshake finished.
116
+ #
117
+ def handshake_cb
118
+ log { "TLS handshake completed" }
119
+ transition(:negotiated)
120
+ end
121
+
122
+ # TLS callback.
123
+ #
124
+ # passed the peer +cert+ to be verified.
125
+ #
126
+ def verify_cb(cert)
127
+ raise Error, "Peer verification enabled, but no certificate received." if cert.nil?
128
+
129
+ log { "TLS verifying #{cert}" }
130
+ @peer_cert = OpenSSL::X509::Certificate.new(cert)
131
+
132
+ # by default one doesn't verify client certificates in the server
133
+ verify_hostname(@sni_hostname)
134
+ end
135
+
136
+ # copied from:
137
+ # https://github.com/ruby/ruby/blob/8cbf2dae5aadfa5d6241b0df2bf44d55db46704f/ext/openssl/lib/openssl/ssl.rb#L395-L409
138
+ #
139
+ def verify_hostname(host)
140
+ return false unless @ctx.verify_peer && @peer_cert
141
+
142
+ OpenSSL::SSL.verify_certificate_identity(@peer_cert, host)
143
+ end
144
+
145
+ private
146
+
147
+ def do_write
148
+ nwritten = 0
149
+ until @encrypted.empty?
150
+ siz = unencrypted_write(@encrypted)
151
+ break unless !siz || siz.zero?
152
+
153
+ nwritten += siz
154
+ end
155
+ nwritten
156
+ end
157
+
158
+ def convert_tls_options(ssl_options)
159
+ options = {}
160
+ options[:verify_peer] = !ssl_options.key?(:verify_mode) || ssl_options[:verify_mode] != OpenSSL::SSL::VERIFY_NONE
161
+ options[:version] = ssl_options[:ssl_version] if ssl_options.key?(:ssl_version)
162
+
163
+ if ssl_options.key?(:key)
164
+ private_key = ssl_options[:key]
165
+ private_key = private_key.to_pem if private_key.respond_to?(:to_pem)
166
+ options[:private_key] = private_key
167
+ end
168
+
169
+ if ssl_options.key?(:ca_path) || ssl_options.key?(:ca_file)
170
+ ca_path = ssl_options[:ca_path] || ssl_options[:ca_file].path
171
+ options[:cert_chain] = ca_path
172
+ end
173
+
174
+ options[:ciphers] = ssl_options[:ciphers] if ssl_options.key?(:ciphers)
175
+ options[:protocols] = ssl_options.fetch(:alpn_protocols, %w[h2 http/1.1])
176
+ options[:hostname] = ssl_options.fetch(:hostname, @hostname)
177
+ options
178
+ end
179
+
180
+ def transition(nextstate)
181
+ case nextstate
182
+ when :negotiated
183
+ return unless @state == :connected
184
+ when :closed
185
+ return unless @state == :negotiated ||
186
+ @state == :connected
187
+ end
188
+ do_transition(nextstate)
189
+ end
190
+
191
+ def log_transition_state(nextstate)
192
+ return super unless nextstate == :negotiated
193
+
194
+ server_cert = @peer_cert
195
+
196
+ "#{super}\n\n" \
197
+ "SSL connection using #{@ctx.ssl_version} / #{Array(@ctx.cipher).first}\n" \
198
+ "ALPN, server accepted to use #{protocol}\n" +
199
+ (if server_cert
200
+ "Server certificate:\n" \
201
+ " subject: #{server_cert.subject}\n" \
202
+ " start date: #{server_cert.not_before}\n" \
203
+ " expire date: #{server_cert.not_after}\n" \
204
+ " issuer: #{server_cert.issuer}\n" \
205
+ " SSL certificate verify ok."
206
+ else
207
+ "SSL certificate verify failed."
208
+ end
209
+ )
210
+ end
211
+ end
212
+
213
+ TLSError = TLS::Error
214
+ end
215
+
216
+ require "httpx/io/tls/ffi"
217
+ require "httpx/io/tls/context"
218
+ require "httpx/io/tls/box"