Class: HTTPX::TLS::Box

Inherits:
Object
  • Object
show all
Defined in:
lib/httpx/io/tls/box.rb

Constant Summary collapse

InstanceLookup =
::Concurrent::Map.new
READ_BUFFER =
2048
SSL_VERIFY_PEER =
0x01
SSL_VERIFY_CLIENT_ONCE =
0x04
VerifyCB =
FFI::Function.new(:int, %i[int pointer]) do |preverify_ok, x509_store|
  x509 = SSL.X509_STORE_CTX_get_current_cert(x509_store)
  ssl = SSL.X509_STORE_CTX_get_ex_data(x509_store, SSL.SSL_get_ex_data_X509_STORE_CTX_idx)

  bio_out = SSL.BIO_new(SSL.BIO_s_mem)
  ret = SSL.PEM_write_bio_X509(bio_out, x509)
  if ret
    len = SSL.BIO_pending(bio_out)
    buffer = FFI::MemoryPointer.new(:char, len, false)
    size = SSL.BIO_read(bio_out, buffer, len)

    # THis is the callback into the ruby class
    cert = buffer.read_string(size)
    SSL.BIO_free(bio_out)
    # InstanceLookup[ssl.address].verify(cert) || preverify_ok.zero? ? 1 : 0
    depth = SSL.X509_STORE_CTX_get_error_depth(ssl)
    box = InstanceLookup[ssl.address]
    if preverify_ok == 1

      hostname_verify = box.verify(cert)
      if hostname_verify
        1
      else
        # SSL.X509_STORE_CTX_set_error(x509_store, SSL::X509_V_ERR_HOSTNAME_MISMATCH)
        0
      end
    else
      1
    end
  else
    SSL.BIO_free(bio_out)
    SSL.X509_STORE_CTX_set_error(x509_store, SSL::X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT)
    0
  end
end
SSL_ERROR_WANT_READ =
2
SSL_ERROR_SSL =
1
SSL_RECEIVED_SHUTDOWN =
2

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(is_server, transport, options = {}) ⇒ Box

Returns a new instance of Box.



70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/httpx/io/tls/box.rb', line 70

def initialize(is_server, transport, options = {})
  @ready = true

  @handshake_completed = false
  @handshake_signaled = false
  @alpn_negotiated = false
  @transport = transport

  @read_buffer = FFI::MemoryPointer.new(:char, READ_BUFFER, false)

  @is_server = is_server
  @context = Context.new(is_server, options)

  @bioRead = SSL.BIO_new(SSL.BIO_s_mem)
  @bioWrite = SSL.BIO_new(SSL.BIO_s_mem)
  @ssl = SSL.SSL_new(@context.ssl_ctx)
  SSL.SSL_set_bio(@ssl, @bioRead, @bioWrite)

  @write_queue = []

  InstanceLookup[@ssl.address] = self

  @verify_peer = options[:verify_peer]
  
  if @verify_peer
    SSL.SSL_set_verify(@ssl, SSL_VERIFY_PEER | SSL_VERIFY_CLIENT_ONCE, VerifyCB)
  end

  # Add Server Name Indication (SNI) for client connections
  if (hostname = options[:hostname])
    if is_server
      @hosts = ::Concurrent::Map.new
      @hosts[hostname.to_s] = @context
      @context.add_server_name_indication
    else
      SSL.SSL_set_tlsext_host_name(@ssl, hostname)
    end
  end

  SSL.SSL_connect(@ssl) unless is_server
end

Instance Attribute Details

#cipherObject (readonly)

Returns the value of attribute cipher.



68
69
70
# File 'lib/httpx/io/tls/box.rb', line 68

def cipher
  @cipher
end

#contextObject (readonly)

Returns the value of attribute context.



68
69
70
# File 'lib/httpx/io/tls/box.rb', line 68

def context
  @context
end

#handshake_completedObject (readonly)

Returns the value of attribute handshake_completed.



68
69
70
# File 'lib/httpx/io/tls/box.rb', line 68

def handshake_completed
  @handshake_completed
end

#hostsObject (readonly)

Returns the value of attribute hosts.



68
69
70
# File 'lib/httpx/io/tls/box.rb', line 68

def hosts
  @hosts
end

#is_serverObject (readonly)

Returns the value of attribute is_server.



68
69
70
# File 'lib/httpx/io/tls/box.rb', line 68

def is_server
  @is_server
end

#ssl_versionObject (readonly)

Returns the value of attribute ssl_version.



68
69
70
# File 'lib/httpx/io/tls/box.rb', line 68

def ssl_version
  @ssl_version
end

#verify_peerObject (readonly)

Returns the value of attribute verify_peer.



68
69
70
# File 'lib/httpx/io/tls/box.rb', line 68

def verify_peer
  @verify_peer
end

Instance Method Details

#add_host(hostname:, **options) ⇒ Object

Raises:



112
113
114
115
116
117
118
119
120
# File 'lib/httpx/io/tls/box.rb', line 112

def add_host(hostname:, **options)
  raise Error, "Server Name Indication (SNI) not configured for default host" unless @hosts
  raise Error, "only valid for server mode context" unless @is_server

  context = Context.new(true, options)
  @hosts[hostname.to_s] = context
  context.add_server_name_indication
  nil
end

#alpn_negotiated!Object



219
220
221
# File 'lib/httpx/io/tls/box.rb', line 219

def alpn_negotiated!
  @alpn_negotiated = true
end

#cleanupObject



224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
# File 'lib/httpx/io/tls/box.rb', line 224

def cleanup
  return unless @ready

  @ready = false

  InstanceLookup.delete @ssl.address

  if (SSL.SSL_get_shutdown(@ssl) & SSL_RECEIVED_SHUTDOWN) != 0
    SSL.SSL_shutdown @ssl
  else
    SSL.SSL_clear @ssl
  end

  SSL.SSL_free @ssl

  if @hosts
    @hosts.each_value(&:cleanup)
    @hosts = nil
  else
    @context.cleanup
  end
end

#close(msg) ⇒ Object



252
253
254
# File 'lib/httpx/io/tls/box.rb', line 252

def close(msg)
  @transport.close_cb(msg)
end

#decrypt(data) ⇒ Object



161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
# File 'lib/httpx/io/tls/box.rb', line 161

def decrypt(data)
  return unless @ready

  put_cipher_text data

  unless SSL.is_init_finished(@ssl)
    resp = @is_server ? SSL.SSL_accept(@ssl) : SSL.SSL_connect(@ssl)

    if resp < 0
      err_code = SSL.SSL_get_error(@ssl, resp)
      if err_code != SSL_ERROR_WANT_READ
        if err_code == SSL_ERROR_SSL
          verify_msg = SSL.X509_verify_cert_error_string(SSL.SSL_get_verify_result(@ssl))
          @transport.close_cb(verify_msg)
        end
        return
      end
    end

    @handshake_completed = true
    @ssl_version = SSL.get_version(@ssl)
    @cipher = SSL.get_current_cipher(@ssl)
    signal_handshake unless @handshake_signaled
  end

  loop do
    size = get_plain_text(@read_buffer, READ_BUFFER)
    if size > 0
      @transport.dispatch_cb @read_buffer.read_string(size)
    else
      break
    end
  end

  dispatch_cipher_text
end

#encrypt(data) ⇒ Object



148
149
150
151
152
153
154
155
156
157
# File 'lib/httpx/io/tls/box.rb', line 148

def encrypt(data)
  return unless @ready

  wrote = put_plain_text data
  if wrote < 0
    @transport.close_cb
  else
    dispatch_cipher_text
  end
end

#get_peer_certObject



136
137
138
139
140
# File 'lib/httpx/io/tls/box.rb', line 136

def get_peer_cert
  return "" unless @ready

  SSL.SSL_get_peer_certificate(@ssl)
end

#remove_host(hostname) ⇒ Object

Careful with this. If you remove all the hosts you’ll end up with a segfault

Raises:



124
125
126
127
128
129
130
131
132
133
134
# File 'lib/httpx/io/tls/box.rb', line 124

def remove_host(hostname)
  raise Error, "Server Name Indication (SNI) not configured for default host" unless @hosts
  raise Error, "only valid for server mode context" unless @is_server

  context = @hosts[hostname.to_s]
  if context
    @hosts.delete(hostname.to_s)
    context.cleanup
  end
  nil
end

#signal_handshakeObject



198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
# File 'lib/httpx/io/tls/box.rb', line 198

def signal_handshake
  @handshake_signaled = true

  # Check protocol support here
  if @context.alpn_set
    proto = alpn_negotiated_protocol

    if proto == :failed
      if @alpn_negotiated
        # We should shutdown if this is the case
        # TODO: send back proper error message
        @transport.close_cb
        return
      end
    end
    @transport.alpn_protocol_cb(proto)
  end

  @transport.handshake_cb
end

#startObject



142
143
144
145
146
# File 'lib/httpx/io/tls/box.rb', line 142

def start
  return unless @ready

  dispatch_cipher_text
end

#verify(cert) ⇒ Object

Called from class level callback function



248
249
250
# File 'lib/httpx/io/tls/box.rb', line 248

def verify(cert)
  @transport.verify_cb(cert)
end