Forum: Ruby SSLSocket in non-blocking mode

Posted by Brian Modra (epailty)
on 2013-01-30 09:02
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.
Posted by Brian Modra (epailty)
on 2013-01-30 20:52
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 :)
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.
Posted by Tony Arcieri (Guest)
on 2013-01-30 21:08
(Received via mailing list)
Are you not aware OpenSSL::SSL::SSLSocket has a #read_nonblock method?
Posted by Brian Modra (epailty)
on 2013-01-31 06:05
Not according to this documentation:
http://www.ruby-doc.org/stdlib-1.9.3/libdoc/openss...

... 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__]
Posted by Tony Arcieri (Guest)
on 2013-01-31 06:22
(Received via mailing list)
Weird, they seem to be "documented" here instead. Seems bad:

http://www.ruby-doc.org/gems/docs/o/openssl-nonblo...
Please log in before posting. Registration is free and takes only a minute.
Existing account (Switch to SSL-encrypted connection)
NEW: Do you have a Google/GoogleMail or Yahoo account? No registration required!
Log in with Google account | Log in with Yahoo account
No account? Register here.