Intro
I have a client that makes numerous SSL connections to a 3rd party service. In certain cases, the 3rd party stops responding during the socket and ssl negotiation process. When this occurs, my current implementation "sits" for hours on end before timing out.
To combat this, I'm trying to implement the following process:
require 'socket'
require 'openssl'
# variables
host = '....'
port = ...
p12 = #OpenSSL::PKCS12 object
# set up socket
addr = Socket.getaddrinfo(host, nil)
sockaddr = Socket.pack_sockaddr_in(port, addr[0][3])
socket = Socket.new(Socket.const_get(addr[0][0]), Socket::SOCK_STREAM, 0)
socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
begin
socket.connect_nonblock(sockaddr)
rescue IO::WaitWritable
if IO.select(nil, [socket], nil, timeout)
begin
socket.connect_nonblock(sockaddr)
rescue Errno::EISCONN
puts "socket connected"
rescue
puts "socket error"
socket.close
raise
end
else
socket.close
raise "Connection timeout"
end
end
# negotiate ssl
context = OpenSSL::SSL::SSLContext.new
context.cert = p12.certificate
context.key = p12.key
ssl_socket = OpenSSL::SSL::SSLSocket.new(socket, context)
ssl_socket.sync_close = true
puts "ssl connecting"
ssl_socket.connect_nonblock
puts "ssl connected"
# cleanup
ssl_socket.close
puts "socket closed"
ssl_socket.connect_nonblock will eventually be wrapped in a similar structure as socket.connect_nonblock is.
The Problem
The issue I'm running into is that ssl_socket.connect_nonblock raises the following when run:
`connect_nonblock': read would block (OpenSSL::SSL::SSLError)
Instead, I'd expect it to raise an IO::WaitWritable as socket.connect_nonblock does.
I've scoured the internet for information on this particular error but can't find anything of particular use. From what I gather, others have had success using this method, so I'm not sure what I'm missing. For the sake of completeness, I've found the same results with both ruby 2.2.0 and 1.9.3.
Any suggestions are greatly appreciated!
Have same problem, I tried below, it seems works right for my situation.
ssl_socket = OpenSSL::SSL::SSLSocket.new socket, context
ssl_socket.sync = true
begin
ssl_socket.connect_nonblock
rescue IO::WaitReadable
if IO.select([ssl_socket], nil, nil, timeout)
retry
else
# timeout
end
rescue IO::WaitWritable
if IO.select(nil, [ssl_socket], nil, timeout)
retry
else
# timeout
end
end
Related
I am fairly new to programming in general and I am using EventMachine on both the client and server side to open a websocket connection between them.
My issue is with the Client side, and when the connection is lost due to network connectivity issues.
def websocket_connection
EM.run do
begin
puts "Connecting"
ws = WebSocket::EventMachine::Client.connect(:uri => "Server Info")
puts "Success WS: #{ws}"
rescue
puts "I've been rescued"
puts "Rescue WS: #{ws}"
ws = nil
sleep 10
websocket_connection
end
ws.onopen do
puts "Connected!"
end
ws.onping do
put "Ping!"
end
ws.onmessage do |msg|
puts msg
end
ws.onclose do |code|
puts "Connection Closed!"
puts "Code: #{code}"
ws = nil
sleep 10
websocket_connection
end
end
end
This connects to the server just fine, but if I pull the network connection and plug it back in I am stuck in an infinite loop of it trying to reconnect with code 1002 (WebSocket protocol error.).
I have tried to call EM.reconnect(server, port, ws) on close, but it crashes and throws this error `connect_server': unable to resolve address: The requested name is valid, but no data of the requested type was found. Which makes sense because it can't contact DNS. Even if wrap the EM.reconnect in a begin rescue it just tries once and never tries again.
I have tried stopping EventMachine and close (EM.stop) but that gets stuck in an infinite loop trying to reconnect.
I am not really sure how to get this client to reconnect to the server after a network lose.
EDIT:
Updated the code above a little bit.
CMD Line:
Success WS: #WebSocket::EventMachine::Client:0x00000002909ac8
Pulled Ethernet Cable
Rescue WS:
Connected Ethernet Cable
Success WS: #WebSocket::EventMachine::Client:0x000000031c42a8
Success WS: #WebSocket::EventMachine::Client:0x000000031a3d50
Success WS: #WebSocket::EventMachine::Client:0x00000003198a90
CTRL + C
block in websocket_connection': undefined methodonopen' for nil:NilClass (NoMethodError)
So it looks like it thinks its connecting, I don't see any connections on the server side.
Well, I couldn't find a way to do a proper reconnect using EventMachine. It looks like weird things happen in EventMachine when you drop your network connection. I ended up relaunching the ruby app under a new Process then killing the current script, not the best way to do this but after a week of trying to get the reconnect working through EventMachine I have just given up. This code works below.
def websocket_restart
exec "ruby script"
exit 0
end
def websocket_connection
EM.run do
begin
puts "Connecting"
ws = WebSocket::EventMachine::Client.connect(:uri => "Server Info")
rescue
websocket_restart
end
ws.onopen do
puts "Connected!"
end
ws.onping do
put "Ping!"
end
ws.onmessage do |msg|
puts msg
end
ws.onclose do |code|
puts "Connection Closed!"
puts "Code: #{code}"
websocket_restart
end
end
end
I've been tasked with modifying an existing Ruby script, but my Ruby knowledge is basic at best...
I need to add a method to check if a server's port is open. if it is, the script should resume doing whatever it's doing. if not, it should exit.
I've applied the following method, taken from Ruby - See if a port is open:
def is_port_open?
#host = "localhost"
#port = "8080"
begin
Timeout::timeout(1) do
begin
s = TCPSocket.new(#host, #port)
s.close
rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
return "port closed :("
end
end
rescue Timeout::Error
end
return "problem with timeout?"
end
This method seems to work well, except when returning "nil" if the port is open. How do i suppress any output (unless there is an error)?
Thanks in Advance!
Whether you only need to check for a condition (port is open):
require 'timeout'
require 'socket'
def is_port_open? host, port
#host = host || "localhost"
#port = port || "8080"
begin
Timeout::timeout(1) do
begin
s = TCPSocket.new(#host, #port)
s.close
return true # success
rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
return false # socket error
end
end
rescue Timeout::Error
end
return false # timeout error
end
is_port_open? 'localhost', 8080
#⇒ true
is_port_open? 'localhost', 11111
#⇒ false
It’s now up to you what to return in case of error etc. Please note, that another option would be to let exceptions propagate to caller. This function would be a way shorter, but you’ll need to handle exceptions in caller.
I am attempting to create a better port scanner by implementing more error handling, and I've run into a bit of trouble.
def pscan(host_name, port)
begin
sock = Socket.new(:INET, :STREAM)
raw = Socket.sockaddr_in(port, host_name)
if sock.connect(raw)
puts "Port #{port} is up!"
rescue (Errno::ECONNREFUSED)
false
rescue (Errno::ETIMEDOUT)
puts "Port #{port} timed out!"
end
end
def run
port = 1
begin
while port <= ARGV[1].to_i do
popen(ARGV[0], port)
port += 1
end
rescue (Errno::EHOSTUNREACH)
puts "No path to host"
rescue(Interrupt)
puts " Process interrupted"
end
end
run
The trouble is that for each port scanned, it will print "No path to host", instead of printing that once and then closing the socket. I'm clearly doing it wrong but everywhere online I find simple code closes a socket connection this way.
I think that you forgot to close the if statement. Also, use 'ensure' to close it.
Try this:
def pscan(host_name, port)
begin
sock = Socket.new(:INET, :STREAM)
raw = Socket.sockaddr_in(port, host_name)
if sock.connect(raw)
puts "Port #{port} is up!"
end
rescue (Errno::ECONNREFUSED)
false
rescue (Errno::ETIMEDOUT)
puts "Port #{port} timed out!"
rescue (Errno::EHOSTUNREACH)
puts "No path to host"
ensure
sock.close
end
end
Like you mentioned in your comments and your code changes, you would have to move the socket closing into the block that's calling your code rather than inside of that code itself, since the block of code will continuously call the method and the socket will never actually be closed.
You'll basically have to do what you posted in your code:
require 'socket'
def pscan(host_name, port)
begin
sock = Socket.new(:INET, :STREAM)
raw = Socket.sockaddr_in(port, host_name)
if sock.connect(raw)
puts "Port #{port} is up!"
end
rescue (Errno::ECONNREFUSED)
puts "Connection refused to port #{port}"
false
rescue (Errno::ETIMEDOUT)
puts "Port #{port} timed out!"
false
end
end
def run
puts "Attempting to scan host #{ARGV[0]}"
port = 1
begin
while port <= ARGV[1].to_i do
pscan(ARGV[0], port)
port += 1
end
rescue (Errno::EHOSTUNREACH)
puts "No path to host #{ARVG[0]}"
rescue(Interrupt)
puts "Process interrupted"
end
end
run
Some thoughts: you may not always get "No path to host" errors when running your code; when I tested it in my network, I never got a "No path to host" error but always got a "Port ... timed out!" error even when I tested it against known unreachable hosts such as 10.0.0.1. You'll have to be wary of that when you run your code in other networks as you might get different results.
$ irb
1.9.3-p448 :001 > require 'socket'
=> true
1.9.3-p448 :002 > TCPSocket.new('www.example.com', 111)
gives
Errno::ETIMEDOUT: Operation timed out - connect(2)
Questions:
How can I define the timeout value for TCPSocket.new?
How can I properly catch the timeout (or, in general, socket) exception(s)?
At least since 2.0 one can simply use Socket::tcp:
Socket.tcp("www.ruby-lang.org", 10567, connect_timeout: 5) {}
Note the block at the end of the expression, which is used to get connection closed in case such is established.
For older versions #falstru answer appears to be best.
Use begin .. rescue Errno::ETIMEDOUT to catch the timeout:
require 'socket'
begin
TCPSocket.new('www.example.com', 111)
rescue Errno::ETIMEDOUT
p 'timeout'
end
To catch any socket exceptions, use SystemCallError instead.
According to the SystemCallError documentation:
SystemCallError is the base class for all low-level platform-dependent errors.
The errors available on the current platform are subclasses of
SystemCallError and are defined in the Errno module.
TCPSocket.new does not support timeout directly.
Use Socket::connect_non_blocking and IO::select to set timeout.
require 'socket'
def connect(host, port, timeout = 5)
# Convert the passed host into structures the non-blocking calls
# can deal with
addr = Socket.getaddrinfo(host, nil)
sockaddr = Socket.pack_sockaddr_in(port, addr[0][4])
Socket.new(Socket.const_get(addr[0][0]), Socket::SOCK_STREAM, 0).tap do |socket|
socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
begin
# Initiate the socket connection in the background. If it doesn't fail
# immediatelyit will raise an IO::WaitWritable (Errno::EINPROGRESS)
# indicating the connection is in progress.
socket.connect_nonblock(sockaddr)
rescue IO::WaitWritable
# IO.select will block until the socket is writable or the timeout
# is exceeded - whichever comes first.
if IO.select(nil, [socket], nil, timeout)
begin
# Verify there is now a good connection
socket.connect_nonblock(sockaddr)
rescue Errno::EISCONN
# Good news everybody, the socket is connected!
rescue
# An unexpected exception was raised - the connection is no good.
socket.close
raise
end
else
# IO.select returns nil when the socket is not ready before timeout
# seconds have elapsed
socket.close
raise "Connection timeout"
end
end
end
end
connect('www.example.com', 111, 2)
The above code comes from "Setting a Socket Connection Timeout in Ruby".
If you like the idea of avoiding the pitfalls of Timeout, but prefer to avoid having to deal with your own implementation of the *_nonblock+select implementation, you can use the tcp_timeout gem.
The tcp_timeout gem monkey-patches TCPSocket#connect, #read, and #write so that they use non-blocking I/O and have timeouts that you can enable.
You to make a timeout you can use ruby's Timeout module:
reqiure 'socket'
reqiure 'timeout'
begin
Timeout.timeout(10) do
begin
TCPSocket.new('www.example.com', 111)
rescue Errno::ENETUNREACH
retry # or do something on network timeout
end
end
rescue Timeout::Error
puts "timed out"
# do something on timeout
end
and you'll get after 10 seconds:
# timed out
# => nil
NOTE: Some people may think that it is dangerous solution, well, this opinion has right to exist, but there were no real investigations proceeded, so, that opinion is just a hypothesis. And currently it is better to use internal ruby's timeout engine in Socket class like the following:
Socket.tcp("www.ruby-lang.org", 80, connect_timeout: 80) do |sock|
...
end
I've got a Ruby TCPSocket client that works great except when I'm trying to close it. When I call the disconnect method in my code below, I get this error:
./smartlinc.rb:70:in `start_listen': stream closed (IOError)
from ./smartlinc.rb:132:in `initialize'
from ./smartlinc.rb:132:in `new'
from ./smartlinc.rb:132:in `start_listen'
from bot.rb:45:in `initialize'
from bot.rb:223:in `new'
from bot.rb:223
Here's the (simplified) code:
class Smartlinc
def initialize
#socket = TCPSocket.new(HOST, PORT)
end
def disconnect
#socket.close
end
def start_listen
# Listen on a background thread
th = Thread.new do
Thread.current.abort_on_exception = true
# Listen for Ctrl-C and disconnect socket gracefully.
Kernel.trap('INT') do
self.disconnect
exit
end
while true
ready = IO.select([#socket])
readable = ready[0]
readable.each do |soc|
if soc == #socket
buf = #socket.recv_nonblock(1024)
if buf.length == 0
puts "The socket connection is dead. Exiting."
exit
else
puts "Received Message"
end
end
end # end each
end # end while
end # end thread
end # end message callback
end
Is there a way I can prevent or catch this error? I'm no expert in socket programming (obviously!), so all help is appreciated.
Your thread is sitting in IO.select() while the trap code happily slams the door in its face with #socket.close, hence you get some complaining.
Don't set abort_on_exception to true, or then handle the exception properly in your code:
Something along these lines...
Kernel.trap('INT') do
#interrupted = true
disconnect
exit
end
...
ready = nil
begin
ready = IO.select(...)
rescue IOError
if #interrupted
puts "Interrupted, we're outta here..."
exit
end
# Else it was a genuine IOError caused by something else, so propagate it up..
raise
end
...