httpx 0.10.2 → 0.12.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (76) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +13 -5
  3. data/doc/release_notes/0_11_0.md +76 -0
  4. data/doc/release_notes/0_11_1.md +5 -0
  5. data/doc/release_notes/0_11_2.md +5 -0
  6. data/doc/release_notes/0_11_3.md +5 -0
  7. data/doc/release_notes/0_12_0.md +55 -0
  8. data/lib/httpx.rb +2 -1
  9. data/lib/httpx/adapters/datadog.rb +205 -0
  10. data/lib/httpx/adapters/faraday.rb +4 -8
  11. data/lib/httpx/adapters/webmock.rb +123 -0
  12. data/lib/httpx/altsvc.rb +1 -0
  13. data/lib/httpx/chainable.rb +1 -1
  14. data/lib/httpx/connection.rb +63 -15
  15. data/lib/httpx/connection/http1.rb +16 -5
  16. data/lib/httpx/connection/http2.rb +36 -29
  17. data/lib/httpx/domain_name.rb +1 -3
  18. data/lib/httpx/errors.rb +2 -0
  19. data/lib/httpx/headers.rb +1 -0
  20. data/lib/httpx/io.rb +16 -3
  21. data/lib/httpx/io/ssl.rb +7 -13
  22. data/lib/httpx/io/tcp.rb +9 -8
  23. data/lib/httpx/io/tls.rb +218 -0
  24. data/lib/httpx/io/tls/box.rb +365 -0
  25. data/lib/httpx/io/tls/context.rb +199 -0
  26. data/lib/httpx/io/tls/ffi.rb +390 -0
  27. data/lib/httpx/io/udp.rb +4 -3
  28. data/lib/httpx/parser/http1.rb +4 -4
  29. data/lib/httpx/plugins/aws_sdk_authentication.rb +81 -0
  30. data/lib/httpx/plugins/aws_sigv4.rb +218 -0
  31. data/lib/httpx/plugins/compression.rb +1 -1
  32. data/lib/httpx/plugins/compression/deflate.rb +2 -5
  33. data/lib/httpx/plugins/cookies/set_cookie_parser.rb +1 -1
  34. data/lib/httpx/plugins/expect.rb +33 -8
  35. data/lib/httpx/plugins/internal_telemetry.rb +93 -0
  36. data/lib/httpx/plugins/multipart.rb +42 -35
  37. data/lib/httpx/plugins/multipart/encoder.rb +110 -0
  38. data/lib/httpx/plugins/multipart/mime_type_detector.rb +64 -0
  39. data/lib/httpx/plugins/multipart/part.rb +34 -0
  40. data/lib/httpx/plugins/proxy.rb +1 -1
  41. data/lib/httpx/plugins/proxy/http.rb +1 -1
  42. data/lib/httpx/plugins/proxy/socks4.rb +8 -0
  43. data/lib/httpx/plugins/proxy/socks5.rb +11 -2
  44. data/lib/httpx/plugins/push_promise.rb +5 -4
  45. data/lib/httpx/plugins/retries.rb +1 -1
  46. data/lib/httpx/plugins/stream.rb +3 -5
  47. data/lib/httpx/pool.rb +0 -1
  48. data/lib/httpx/registry.rb +1 -7
  49. data/lib/httpx/request.rb +32 -12
  50. data/lib/httpx/resolver.rb +7 -4
  51. data/lib/httpx/resolver/https.rb +7 -13
  52. data/lib/httpx/resolver/native.rb +10 -6
  53. data/lib/httpx/resolver/system.rb +1 -1
  54. data/lib/httpx/response.rb +9 -2
  55. data/lib/httpx/selector.rb +6 -0
  56. data/lib/httpx/session.rb +40 -20
  57. data/lib/httpx/transcoder.rb +6 -4
  58. data/lib/httpx/transcoder/body.rb +3 -5
  59. data/lib/httpx/version.rb +1 -1
  60. data/sig/connection/http1.rbs +2 -2
  61. data/sig/connection/http2.rbs +8 -7
  62. data/sig/headers.rbs +3 -0
  63. data/sig/plugins/aws_sdk_authentication.rbs +17 -0
  64. data/sig/plugins/aws_sigv4.rbs +65 -0
  65. data/sig/plugins/multipart.rbs +27 -4
  66. data/sig/plugins/push_promise.rbs +1 -1
  67. data/sig/request.rbs +1 -1
  68. data/sig/resolver/https.rbs +2 -0
  69. data/sig/response.rbs +1 -1
  70. data/sig/session.rbs +1 -1
  71. data/sig/transcoder.rbs +2 -2
  72. data/sig/transcoder/body.rbs +2 -0
  73. data/sig/transcoder/form.rbs +7 -1
  74. data/sig/transcoder/json.rbs +3 -1
  75. metadata +50 -47
  76. data/sig/missing.rbs +0 -12
@@ -139,10 +139,8 @@ module HTTPX
139
139
  elsif @hostname.end_with?(othername) && @hostname[-othername.size - 1, 1] == DOT
140
140
  # The other is higher
141
141
  -1
142
- elsif othername.end_with?(@hostname) && othername[-@hostname.size - 1, 1] == DOT
143
- # The other is lower
144
- 1
145
142
  else
143
+ # The other is lower
146
144
  1
147
145
  end
148
146
  end
data/lib/httpx/errors.rb CHANGED
@@ -24,6 +24,8 @@ module HTTPX
24
24
 
25
25
  ConnectTimeoutError = Class.new(TimeoutError)
26
26
 
27
+ ResolveTimeoutError = Class.new(TimeoutError)
28
+
27
29
  ResolveError = Class.new(Error)
28
30
 
29
31
  NativeResolveError = Class.new(ResolveError) do
data/lib/httpx/headers.rb CHANGED
@@ -119,6 +119,7 @@ module HTTPX
119
119
  def to_hash
120
120
  Hash[to_a]
121
121
  end
122
+ alias_method :to_h, :to_hash
122
123
 
123
124
  # the headers store in array of pairs format
124
125
  def to_a
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,27 +3,24 @@
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] }
9
11
  else
10
- # :nocov:
11
12
  {}
12
- # :nocov:
13
13
  end
14
14
 
15
15
  def initialize(_, _, options)
16
+ super
16
17
  @ctx = OpenSSL::SSL::SSLContext.new
17
18
  ctx_options = TLS_OPTIONS.merge(options.ssl)
19
+ @sni_hostname = ctx_options.delete(:hostname) || @hostname
18
20
  @ctx.set_params(ctx_options) unless ctx_options.empty?
19
- super
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,19 +56,19 @@ 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 = @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(@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
71
69
  @interests = :w
72
70
  end
73
71
 
74
- # :nocov:
75
72
  if RUBY_VERSION < "2.3"
76
73
  def read(_, buffer)
77
74
  super
@@ -99,14 +96,11 @@ module HTTPX
99
96
  end
100
97
  end
101
98
  end
102
- # :nocov:
103
99
 
104
- # :nocov:
105
100
  def inspect
106
101
  id = @io.closed? ? "closed" : @io.to_io.fileno
107
102
  "#<SSL(fd: #{id}): #{@ip}:#{@port} state: #{@state}>"
108
103
  end
109
- # :nocov:
110
104
 
111
105
  private
112
106
 
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