httpx 0.11.3 → 0.12.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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/doc/release_notes/0_11_1.md +5 -1
  4. data/doc/release_notes/0_12_0.md +55 -0
  5. data/lib/httpx.rb +2 -1
  6. data/lib/httpx/adapters/faraday.rb +4 -6
  7. data/lib/httpx/altsvc.rb +1 -0
  8. data/lib/httpx/connection.rb +63 -15
  9. data/lib/httpx/connection/http1.rb +8 -7
  10. data/lib/httpx/connection/http2.rb +32 -25
  11. data/lib/httpx/io.rb +16 -3
  12. data/lib/httpx/io/ssl.rb +7 -9
  13. data/lib/httpx/io/tcp.rb +9 -8
  14. data/lib/httpx/io/tls.rb +218 -0
  15. data/lib/httpx/io/tls/box.rb +365 -0
  16. data/lib/httpx/io/tls/context.rb +199 -0
  17. data/lib/httpx/io/tls/ffi.rb +390 -0
  18. data/lib/httpx/parser/http1.rb +4 -4
  19. data/lib/httpx/plugins/aws_sdk_authentication.rb +81 -0
  20. data/lib/httpx/plugins/aws_sigv4.rb +218 -0
  21. data/lib/httpx/plugins/compression/deflate.rb +2 -5
  22. data/lib/httpx/plugins/internal_telemetry.rb +93 -0
  23. data/lib/httpx/plugins/multipart.rb +2 -0
  24. data/lib/httpx/plugins/multipart/encoder.rb +4 -9
  25. data/lib/httpx/plugins/proxy.rb +1 -1
  26. data/lib/httpx/plugins/proxy/http.rb +1 -1
  27. data/lib/httpx/plugins/proxy/socks4.rb +8 -0
  28. data/lib/httpx/plugins/proxy/socks5.rb +8 -0
  29. data/lib/httpx/plugins/push_promise.rb +3 -2
  30. data/lib/httpx/plugins/retries.rb +1 -1
  31. data/lib/httpx/plugins/stream.rb +3 -5
  32. data/lib/httpx/pool.rb +0 -1
  33. data/lib/httpx/registry.rb +1 -7
  34. data/lib/httpx/request.rb +11 -1
  35. data/lib/httpx/resolver/https.rb +3 -11
  36. data/lib/httpx/response.rb +9 -2
  37. data/lib/httpx/selector.rb +5 -0
  38. data/lib/httpx/session.rb +25 -2
  39. data/lib/httpx/transcoder/body.rb +3 -5
  40. data/lib/httpx/version.rb +1 -1
  41. data/sig/connection/http1.rbs +2 -2
  42. data/sig/connection/http2.rbs +5 -3
  43. data/sig/plugins/aws_sdk_authentication.rbs +17 -0
  44. data/sig/plugins/aws_sigv4.rbs +65 -0
  45. data/sig/plugins/push_promise.rbs +1 -1
  46. metadata +13 -2
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
@@ -59,12 +56,13 @@ module HTTPX
59
56
 
60
57
  unless @io.is_a?(OpenSSL::SSL::SSLSocket)
61
58
  @io = OpenSSL::SSL::SSLSocket.new(@io, @ctx)
62
- @io.hostname = @tls_hostname
59
+ @io.hostname = @sni_hostname
63
60
  @io.sync_close = true
64
61
  end
65
62
  @io.connect_nonblock
66
- @io.post_connection_check(@tls_hostname) if @ctx.verify_mode != OpenSSL::SSL::VERIFY_NONE
63
+ @io.post_connection_check(@sni_hostname) if @ctx.verify_mode != OpenSSL::SSL::VERIFY_NONE
67
64
  transition(:negotiated)
65
+ @interests = :w
68
66
  rescue ::IO::WaitReadable
69
67
  @interests = :r
70
68
  rescue ::IO::WaitWritable
data/lib/httpx/io/tcp.rb CHANGED
@@ -7,7 +7,7 @@ module HTTPX
7
7
  class TCP
8
8
  include Loggable
9
9
 
10
- attr_reader :ip, :port, :addresses, :state
10
+ attr_reader :ip, :port, :addresses, :state, :interests
11
11
 
12
12
  alias_method :host, :ip
13
13
 
@@ -18,6 +18,7 @@ module HTTPX
18
18
  @options = Options.new(options)
19
19
  @fallback_protocol = @options.fallback_protocol
20
20
  @port = origin.port
21
+ @interests = :w
21
22
  if @options.io
22
23
  @io = case @options.io
23
24
  when Hash
@@ -39,10 +40,6 @@ module HTTPX
39
40
  @io ||= build_socket
40
41
  end
41
42
 
42
- def interests
43
- :w
44
- end
45
-
46
43
  def to_io
47
44
  @io.to_io
48
45
  end
@@ -62,6 +59,8 @@ module HTTPX
62
59
  @io.connect_nonblock(Socket.sockaddr_in(@port, @ip.to_s))
63
60
  rescue Errno::EISCONN
64
61
  end
62
+ @interests = :w
63
+
65
64
  transition(:connected)
66
65
  rescue Errno::EHOSTUNREACH => e
67
66
  raise e if @ip_index <= 0
@@ -69,13 +68,15 @@ module HTTPX
69
68
  @ip_index -= 1
70
69
  retry
71
70
  rescue Errno::ETIMEDOUT => e
72
- raise ConnectTimeoutError, e.message if @ip_index <= 0
71
+ raise ConnectTimeoutError.new(@options.timeout.connect_timeout, e.message) if @ip_index <= 0
73
72
 
74
73
  @ip_index -= 1
75
74
  retry
76
75
  rescue Errno::EINPROGRESS,
77
- Errno::EALREADY,
78
- ::IO::WaitReadable
76
+ Errno::EALREADY
77
+ @interests = :w
78
+ rescue ::IO::WaitReadable
79
+ @interests = :r
79
80
  end
80
81
 
81
82
  if RUBY_VERSION < "2.3"
@@ -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"
@@ -0,0 +1,365 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2004-2013 Cotag Media
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is furnished
10
+ # to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in all
13
+ # copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ # THE SOFTWARE.
22
+ #
23
+
24
+ class HTTPX::TLS
25
+ class Box
26
+ InstanceLookup = ::Concurrent::Map.new
27
+
28
+ READ_BUFFER = 2048
29
+ SSL_VERIFY_PEER = 0x01
30
+ SSL_VERIFY_CLIENT_ONCE = 0x04
31
+
32
+ VerifyCB = FFI::Function.new(:int, %i[int pointer]) do |preverify_ok, x509_store|
33
+ x509 = SSL.X509_STORE_CTX_get_current_cert(x509_store)
34
+ ssl = SSL.X509_STORE_CTX_get_ex_data(x509_store, SSL.SSL_get_ex_data_X509_STORE_CTX_idx)
35
+
36
+ bio_out = SSL.BIO_new(SSL.BIO_s_mem)
37
+ ret = SSL.PEM_write_bio_X509(bio_out, x509)
38
+ if ret
39
+ len = SSL.BIO_pending(bio_out)
40
+ buffer = FFI::MemoryPointer.new(:char, len, false)
41
+ size = SSL.BIO_read(bio_out, buffer, len)
42
+
43
+ # THis is the callback into the ruby class
44
+ cert = buffer.read_string(size)
45
+ SSL.BIO_free(bio_out)
46
+ # InstanceLookup[ssl.address].verify(cert) || preverify_ok.zero? ? 1 : 0
47
+ depth = SSL.X509_STORE_CTX_get_error_depth(ssl)
48
+ box = InstanceLookup[ssl.address]
49
+ if preverify_ok == 1
50
+
51
+ hostname_verify = box.verify(cert)
52
+ if hostname_verify
53
+ 1
54
+ else
55
+ # SSL.X509_STORE_CTX_set_error(x509_store, SSL::X509_V_ERR_HOSTNAME_MISMATCH)
56
+ 0
57
+ end
58
+ else
59
+ 1
60
+ end
61
+ else
62
+ SSL.BIO_free(bio_out)
63
+ SSL.X509_STORE_CTX_set_error(x509_store, SSL::X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT)
64
+ 0
65
+ end
66
+ end
67
+
68
+ attr_reader :is_server, :context, :handshake_completed, :hosts, :ssl_version, :cipher, :verify_peer
69
+
70
+ def initialize(is_server, transport, options = {})
71
+ @ready = true
72
+
73
+ @handshake_completed = false
74
+ @handshake_signaled = false
75
+ @alpn_negotiated = false
76
+ @transport = transport
77
+
78
+ @read_buffer = FFI::MemoryPointer.new(:char, READ_BUFFER, false)
79
+
80
+ @is_server = is_server
81
+ @context = Context.new(is_server, options)
82
+
83
+ @bioRead = SSL.BIO_new(SSL.BIO_s_mem)
84
+ @bioWrite = SSL.BIO_new(SSL.BIO_s_mem)
85
+ @ssl = SSL.SSL_new(@context.ssl_ctx)
86
+ SSL.SSL_set_bio(@ssl, @bioRead, @bioWrite)
87
+
88
+ @write_queue = []
89
+
90
+ InstanceLookup[@ssl.address] = self
91
+
92
+ @verify_peer = options[:verify_peer]
93
+
94
+ if @verify_peer
95
+ SSL.SSL_set_verify(@ssl, SSL_VERIFY_PEER | SSL_VERIFY_CLIENT_ONCE, VerifyCB)
96
+ end
97
+
98
+ # Add Server Name Indication (SNI) for client connections
99
+ if (hostname = options[:hostname])
100
+ if is_server
101
+ @hosts = ::Concurrent::Map.new
102
+ @hosts[hostname.to_s] = @context
103
+ @context.add_server_name_indication
104
+ else
105
+ SSL.SSL_set_tlsext_host_name(@ssl, hostname)
106
+ end
107
+ end
108
+
109
+ SSL.SSL_connect(@ssl) unless is_server
110
+ end
111
+
112
+ def add_host(hostname:, **options)
113
+ raise Error, "Server Name Indication (SNI) not configured for default host" unless @hosts
114
+ raise Error, "only valid for server mode context" unless @is_server
115
+
116
+ context = Context.new(true, options)
117
+ @hosts[hostname.to_s] = context
118
+ context.add_server_name_indication
119
+ nil
120
+ end
121
+
122
+ # Careful with this.
123
+ # If you remove all the hosts you'll end up with a segfault
124
+ def remove_host(hostname)
125
+ raise Error, "Server Name Indication (SNI) not configured for default host" unless @hosts
126
+ raise Error, "only valid for server mode context" unless @is_server
127
+
128
+ context = @hosts[hostname.to_s]
129
+ if context
130
+ @hosts.delete(hostname.to_s)
131
+ context.cleanup
132
+ end
133
+ nil
134
+ end
135
+
136
+ def get_peer_cert
137
+ return "" unless @ready
138
+
139
+ SSL.SSL_get_peer_certificate(@ssl)
140
+ end
141
+
142
+ def start
143
+ return unless @ready
144
+
145
+ dispatch_cipher_text
146
+ end
147
+
148
+ def encrypt(data)
149
+ return unless @ready
150
+
151
+ wrote = put_plain_text data
152
+ if wrote < 0
153
+ @transport.close_cb
154
+ else
155
+ dispatch_cipher_text
156
+ end
157
+ end
158
+
159
+ SSL_ERROR_WANT_READ = 2
160
+ SSL_ERROR_SSL = 1
161
+ def decrypt(data)
162
+ return unless @ready
163
+
164
+ put_cipher_text data
165
+
166
+ unless SSL.is_init_finished(@ssl)
167
+ resp = @is_server ? SSL.SSL_accept(@ssl) : SSL.SSL_connect(@ssl)
168
+
169
+ if resp < 0
170
+ err_code = SSL.SSL_get_error(@ssl, resp)
171
+ if err_code != SSL_ERROR_WANT_READ
172
+ if err_code == SSL_ERROR_SSL
173
+ verify_msg = SSL.X509_verify_cert_error_string(SSL.SSL_get_verify_result(@ssl))
174
+ @transport.close_cb(verify_msg)
175
+ end
176
+ return
177
+ end
178
+ end
179
+
180
+ @handshake_completed = true
181
+ @ssl_version = SSL.get_version(@ssl)
182
+ @cipher = SSL.get_current_cipher(@ssl)
183
+ signal_handshake unless @handshake_signaled
184
+ end
185
+
186
+ loop do
187
+ size = get_plain_text(@read_buffer, READ_BUFFER)
188
+ if size > 0
189
+ @transport.dispatch_cb @read_buffer.read_string(size)
190
+ else
191
+ break
192
+ end
193
+ end
194
+
195
+ dispatch_cipher_text
196
+ end
197
+
198
+ def signal_handshake
199
+ @handshake_signaled = true
200
+
201
+ # Check protocol support here
202
+ if @context.alpn_set
203
+ proto = alpn_negotiated_protocol
204
+
205
+ if proto == :failed
206
+ if @alpn_negotiated
207
+ # We should shutdown if this is the case
208
+ # TODO: send back proper error message
209
+ @transport.close_cb
210
+ return
211
+ end
212
+ end
213
+ @transport.alpn_protocol_cb(proto)
214
+ end
215
+
216
+ @transport.handshake_cb
217
+ end
218
+
219
+ def alpn_negotiated!
220
+ @alpn_negotiated = true
221
+ end
222
+
223
+ SSL_RECEIVED_SHUTDOWN = 2
224
+ def cleanup
225
+ return unless @ready
226
+
227
+ @ready = false
228
+
229
+ InstanceLookup.delete @ssl.address
230
+
231
+ if (SSL.SSL_get_shutdown(@ssl) & SSL_RECEIVED_SHUTDOWN) != 0
232
+ SSL.SSL_shutdown @ssl
233
+ else
234
+ SSL.SSL_clear @ssl
235
+ end
236
+
237
+ SSL.SSL_free @ssl
238
+
239
+ if @hosts
240
+ @hosts.each_value(&:cleanup)
241
+ @hosts = nil
242
+ else
243
+ @context.cleanup
244
+ end
245
+ end
246
+
247
+ # Called from class level callback function
248
+ def verify(cert)
249
+ @transport.verify_cb(cert)
250
+ end
251
+
252
+ def close(msg)
253
+ @transport.close_cb(msg)
254
+ end
255
+
256
+ private
257
+
258
+ def alpn_negotiated_protocol
259
+ return nil unless @context.alpn_set
260
+
261
+ proto = FFI::MemoryPointer.new(:pointer, 1, true)
262
+ len = FFI::MemoryPointer.new(:uint, 1, true)
263
+ SSL.SSL_get0_alpn_selected(@ssl, proto, len)
264
+
265
+ resp = proto.get_pointer(0)
266
+
267
+ return :failed if resp.address == 0
268
+
269
+ length = len.get_uint(0)
270
+ resp.read_string(length)
271
+ end
272
+
273
+ def get_plain_text(buffer, ready)
274
+ # Read the buffered clear text
275
+ size = SSL.SSL_read(@ssl, buffer, ready)
276
+ if size >= 0
277
+ size
278
+ else
279
+ SSL.SSL_get_error(@ssl, size) == SSL_ERROR_WANT_READ ? 0 : -1
280
+ end
281
+ end
282
+
283
+ def pending_data(bio)
284
+ SSL.BIO_pending(bio)
285
+ end
286
+
287
+ def get_cipher_text(buffer, length)
288
+ SSL.BIO_read(@bioWrite, buffer, length)
289
+ end
290
+
291
+ def put_cipher_text(data)
292
+ len = data.bytesize
293
+ wrote = SSL.BIO_write(@bioRead, data, len)
294
+ wrote == len
295
+ end
296
+
297
+ SSL_ERROR_WANT_WRITE = 3
298
+ def put_plain_text(data)
299
+ @write_queue.push(data) if data
300
+ return 0 unless SSL.is_init_finished(@ssl)
301
+
302
+ fatal = false
303
+ did_work = false
304
+
305
+ until @write_queue.empty?
306
+ data = @write_queue.pop
307
+ len = data.bytesize
308
+
309
+ wrote = SSL.SSL_write(@ssl, data, len)
310
+
311
+ if wrote > 0
312
+ did_work = true
313
+ else
314
+ err_code = SSL.SSL_get_error(@ssl, wrote)
315
+ if (err_code != SSL_ERROR_WANT_READ) && (err_code != SSL_ERROR_WANT_WRITE)
316
+ fatal = true
317
+ else
318
+ # Not fatal - add back to the queue
319
+ @write_queue.unshift data
320
+ end
321
+
322
+ break
323
+ end
324
+ end
325
+
326
+ if did_work
327
+ 1
328
+ elsif fatal
329
+ -1
330
+ else
331
+ 0
332
+ end
333
+ end
334
+
335
+ CIPHER_DISPATCH_FAILED = "Cipher text dispatch failed"
336
+ def dispatch_cipher_text
337
+ loop do
338
+ did_work = false
339
+
340
+ # Get all the encrypted data and transmit it
341
+ pending = pending_data(@bioWrite)
342
+ if pending > 0
343
+ buffer = FFI::MemoryPointer.new(:char, pending, false)
344
+
345
+ resp = get_cipher_text(buffer, pending)
346
+ raise Error, CIPHER_DISPATCH_FAILED unless resp > 0
347
+
348
+ @transport.transmit_cb(buffer.read_string(resp))
349
+ did_work = true
350
+ end
351
+
352
+ # Send any queued out going data
353
+ unless @write_queue.empty?
354
+ resp = put_plain_text nil
355
+ if resp > 0
356
+ did_work = true
357
+ elsif resp < 0
358
+ @transport.close_cb
359
+ end
360
+ end
361
+ break unless did_work
362
+ end
363
+ end
364
+ end
365
+ end