SSLSocket in non-blocking mode

Hi,
I’ve had a little trouble getting SSLSocket to behave as I want it to.
It’s #pending method returns 0 until I call #sysread(1)… which throws
EOFError if there is nothing to read, or 1 byte after some bytes have
become available.

I suspect that the implementation does not fill its internal buffer with
raw bytes to decrypt to its output buffer until #sysread is called on an
empty output buffer. Given that the SSLSocket class almost certainly
would not be implementing its own thread to do this automatically, that
seems reasonable.

But my solution seems a bit “hackish”, and I would appreciate if someone
knows a better way to do this?

(Maybe IO:select on the tcp socket to see if it has bytes ready, then
call #sysread)

Following is a simplified code snippet that shows essentially what I’m
doing:

class SslSocketServer
def initialize(port, hostname, cert_fname, key_fname)
@socket_server = Socket.new(AF_INET, SOCK_STREAM, 0)
sockaddr = Socket.sockaddr_in(port, hostname)
@socket_server.bind(sockaddr)
@socket_server.listen(5)
@ssl_context = OpenSSL::SSL::SSLContext.new
cert_file = File.open(cert_fname)
key_file = File.open(key_fname)
@ssl_context.cert = OpenSSL::X509::Certificate.new(cert_file)
@ssl_context.key = OpenSSL::PKey::RSA.new(key_file)
end

def accept
ssl = nil
begin
sock, addr = @socket_server.accept
begin
sslsock = OpenSSL::SSL::SSLSocket.new(sock, @ssl_context)
sslsock.sync_close = true
sslsock.accept
ssl = SslConnection.new(sock, sslsock, addr)
rescue SSLError => ex
sock.close
raise ex
end
rescue IO::WaitReadable
rescue Errno::EINTR => e
STDERR.puts("#{e.class}: #{e.to_s}")
STDERR.puts(e.backtrace.join("\n"))
end
ssl
end
end

class SslConnection
def initialize(sock, sslsock, addr)
@sock = sock
@sslsock = sslsock
@addr = addr
end

def recvfrom_nonblock(size)
available = @sslsock.pending
if available == 0
begin
byte = @sslsock.sysread(1)
[byte, @addr]
rescue EOFError
raise SslNotEnoughPending, “EOFError”
end
else
size = available if available < size
[@sslsock.sysread(size), @addr]
end
end

def close
@sslsock.close
@sock.close
end

def write(bytes)
@sslsock.syswrite(bytes)
end
end

Then I read from the SslConnection as if it were a socket returned from
Socket#accept, using the rcvfrom_nonblock and write methods only.

I’ve done some more work on this since posting, and tested out an idea I
hinted at in my previous post:
I check the underlying tcpsocket (@sock in my code) using IO.select.
(Actually I call select passing it an array of the currently connected
tcp sockets, and if there are any bytes waiting, then if I call sysread
on the SSLSocket, it will fill its input buffer from the tcpsocket,
decrypt them, and return them.
So I guess I answered my own question.

Also, it seems logical to me now that #pending will actually only tell
me how many bytes are remaining in the output buffer of the SLSocket,
because the SSLSocket won’t read from the tcpsocket unless its buffer is
-empty-. I.e. when #pending is returning zero :slight_smile:
So I have to be optimistic, read one byte (only when I know, from the
call to IO.select, that there are bytes ready for it to process)…
which will trigger it to read from the TCPSocket, and decrypt it, and
fill its output buffer, and then #pending will be non zero.

I’m making assumptions here, and would be grateful if someone can
provide an authoritative answer.

Are you not aware OpenSSL::SSL::SSLSocket has a #read_nonblock method?

Not according to this documentation:

… but according to irb, yes! Thanks

irb(main):015:0> sslsock.methods
=> [:io, :context, :hostname, :hostname=, :sync_close, :sync_close=,
:to_io, :connect, :connect_nonblock, :accept, :accept_nonblock,
:sysread, :syswrite, :sysclose, :cert, :peer_cert, :peer_cert_chain,
:cipher, :state, :pending, :session_reused?, :session=, :verify_result,
:client_ca, :post_connection_check, :session, :addr, :peeraddr,
:setsockopt, :getsockopt, :fcntl, :closed?, :do_not_reverse_lookup=,
:sync, :sync=, :read, :readpartial, :read_nonblock, :gets, :each,
:each_line, :readlines, :readline, :getc, :each_byte, :readchar,
:ungetc, :eof?, :eof, :write, :write_nonblock, :<<, :puts, :print,
:printf, :flush, :close, :to_a, :entries, :sort, :sort_by, :grep,
:count, :find, :detect, :find_index, :find_all, :select, :reject,
:collect, :map, :flat_map, :collect_concat, :inject, :reduce,
:partition, :group_by, :first, :all?, :any?, :one?, :none?, :min, :max,
:minmax, :min_by, :max_by, :minmax_by, :member?, :include?,
:each_with_index, :reverse_each, :each_entry, :each_slice, :each_cons,
:each_with_object, :zip, :take, :take_while, :drop, :drop_while, :cycle,
:chunk, :slice_before, :nil?, :===, :=~, :!~, :eql?, :hash, :<=>,
:class, :singleton_class, :clone, :dup, :initialize_dup,
:initialize_clone, :taint, :tainted?, :untaint, :untrust, :untrusted?,
:trust, :freeze, :frozen?, :to_s, :inspect, :methods,
:singleton_methods, :protected_methods, :private_methods,
:public_methods, :instance_variables, :instance_variable_get,
:instance_variable_set, :instance_variable_defined?, :instance_of?,
:kind_of?, :is_a?, :tap, :send, :public_send, :respond_to?,
:respond_to_missing?, :extend, :display, :method, :public_method,
:define_singleton_method, :object_id, :to_enum, :enum_for, :==, :equal?,
:!, :!=, :instance_eval, :instance_exec, :send, :id]

Weird, they seem to be “documented” here instead. Seems bad:

http://www.ruby-doc.org/gems/docs/o/openssl-nonblock-0.2.0/OpenSSL/SSL/SSLSocket.html#method-i-read_nonblock