class RubyTls::SSL::Box

Constants

CIPHER_DISPATCH_FAILED
InstanceLookup
READ_BUFFER
SSL_ERROR_SSL
SSL_ERROR_WANT_READ
SSL_ERROR_WANT_WRITE
SSL_RECEIVED_SHUTDOWN
SSL_VERIFY_CLIENT_ONCE
SSL_VERIFY_PEER
VerifyCB

Attributes

context[R]
handshake_completed[R]
hosts[R]
is_server[R]

Public Class Methods

new(server, transport, options = {}) click to toggle source
# File lib/ruby-tls/ssl.rb, line 548
def initialize(server, transport, options = {})
    @ready = true

    @handshake_completed = false
    @handshake_signaled = false
    @negotiated = false
    @transport = transport

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

    @is_server = server
    @context = Context.new(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

    @alpn_fallback = options[:fallback]
    if options[: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 options[:host_name]
        if server
            @hosts = ::Concurrent::Map.new
            @hosts[options[:host_name].to_s] = @context
            @context.add_server_name_indication
        else
            SSL.SSL_set_tlsext_host_name(@ssl, options[:host_name])
        end
    end

    SSL.SSL_connect(@ssl) unless server
end

Public Instance Methods

add_host(host_name:, **options) click to toggle source
# File lib/ruby-tls/ssl.rb, line 589
def add_host(host_name:, **options)
    raise 'Server Name Indication (SNI) not configured for default host' unless @hosts
    raise 'only valid for server mode context' unless @is_server
    context = Context.new(true, options)
    @hosts[host_name.to_s] = context
    context.add_server_name_indication
    nil
end
cleanup() click to toggle source
# File lib/ruby-tls/ssl.rb, line 718
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 do |context|
            context.cleanup
        end
        @hosts = nil
    else
        @context.cleanup
    end
end
decrypt(data) click to toggle source
# File lib/ruby-tls/ssl.rb, line 657
def decrypt(data)
    return unless @ready

    put_cipher_text data

    if not SSL.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
                @transport.close_cb if err_code == SSL_ERROR_SSL
                return
            end
        end

        @handshake_completed = true
        signal_handshake unless @handshake_signaled
    end

    while true 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) click to toggle source
# File lib/ruby-tls/ssl.rb, line 644
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_cert() click to toggle source
# File lib/ruby-tls/ssl.rb, line 617
def get_peer_cert
    return '' unless @ready
    SSL.SSL_get_peer_certificate(@ssl)
end
negotiated() click to toggle source
# File lib/ruby-tls/ssl.rb, line 713
def negotiated
    @negotiated = true
end
negotiated_protocol() click to toggle source
# File lib/ruby-tls/ssl.rb, line 622
def negotiated_protocol
    return nil unless @context.alpn_set

    proto = FFI::MemoryPointer.new(:pointer, 1, true)
    len = FFI::MemoryPointer.new(:uint, 1, true)
    SSL.SSL_get0_alpn_selected(@ssl, proto, len)

    resp = proto.get_pointer(0)
    if resp.address == 0
        :failed
    else
        length = len.get_uint(0)
        resp.read_string(length).to_sym
    end
end
remove_host(host_name) click to toggle source

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

# File lib/ruby-tls/ssl.rb, line 600
def remove_host(host_name)
    raise 'Server Name Indication (SNI) not configured for default host' unless @hosts
    raise 'only valid for server mode context' unless @is_server
    context = @hosts[host_name.to_s]
    if context
        @hosts.delete(host_name.to_s)
        context.cleanup
    end
    nil
end
signal_handshake() click to toggle source
# File lib/ruby-tls/ssl.rb, line 689
def signal_handshake
    @handshake_signaled = true

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

        if proto == :failed
            if @negotiated
                # We should shutdown if this is the case
                @transport.close_cb
                return
            elsif @alpn_fallback
                # Client or Server with a client that doesn't support ALPN
                proto = @alpn_fallback.to_sym
            end
        end
    else
        proto = nil
    end

    @transport.handshake_cb(proto)
end
start() click to toggle source
# File lib/ruby-tls/ssl.rb, line 638
def start
    return unless @ready

    dispatch_cipher_text
end
verify(cert) click to toggle source

Called from class level callback function

# File lib/ruby-tls/ssl.rb, line 743
def verify(cert)
    @transport.verify_cb(cert) == true ? 1 : 0
end

Private Instance Methods

dispatch_cipher_text() click to toggle source
# File lib/ruby-tls/ssl.rb, line 835
def dispatch_cipher_text
    begin
        did_work = false

        # Get all the encrypted data and transmit it
        pending = pending_data(@bioWrite)
        if pending > 0
            buffer = FFI::MemoryPointer.new(:char, pending, false)

            resp = get_cipher_text(buffer, pending)
            raise CIPHER_DISPATCH_FAILED unless resp > 0

            @transport.transmit_cb(buffer.read_string(resp))
            did_work = true
        end

        # Send any queued out going data
        unless @write_queue.empty?
            resp = put_plain_text nil
            if resp > 0
                did_work = true
            elsif resp < 0
                @transport.close_cb
            end
        end
    end while did_work
end
get_cipher_text(buffer, length) click to toggle source
# File lib/ruby-tls/ssl.rb, line 784
def get_cipher_text(buffer, length)
    SSL.BIO_read(@bioWrite, buffer, length)
end
get_plain_text(buffer, ready) click to toggle source
# File lib/ruby-tls/ssl.rb, line 751
def get_plain_text(buffer, ready)
    # Read the buffered clear text
    size = SSL.SSL_read(@ssl, buffer, ready)
    if size >= 0
        size
    else
        SSL.SSL_get_error(@ssl, size) == SSL_ERROR_WANT_READ ? 0 : -1
    end
end
pending_data(bio) click to toggle source
# File lib/ruby-tls/ssl.rb, line 780
def pending_data(bio)
    SSL.BIO_pending(bio)
end
put_cipher_text(data) click to toggle source
# File lib/ruby-tls/ssl.rb, line 788
def put_cipher_text(data)
    len = data.bytesize
    wrote = SSL.BIO_write(@bioRead, data, len)
    wrote == len
end
put_plain_text(data) click to toggle source
# File lib/ruby-tls/ssl.rb, line 796
def put_plain_text(data)
    @write_queue.push(data) if data
    return 0 unless SSL.SSL_is_init_finished(@ssl)

    fatal = false
    did_work = false

    while !@write_queue.empty? do
        data = @write_queue.pop
        len = data.bytesize

        wrote = SSL.SSL_write(@ssl, data, len)

        if wrote > 0
            did_work = true;
        else
            err_code = SSL.SSL_get_error(@ssl, wrote)
            if (err_code != SSL_ERROR_WANT_READ) && (err_code != SSL_ERROR_WANT_WRITE)
                fatal = true
            else
                # Not fatal - add back to the queue
                @write_queue.unshift data
            end

            break
        end
    end

    if did_work
        1
    elsif fatal
        -1
    else
        0
    end
end