Skip to content

Commit 2cf80fe

Browse files
committed
ssl: support SSLSocket#connect and #accept with timeout
Add a keyword argument timeout to specify the total time allowed for the TLS handshake to complete. Inspired by Addrinfo#connect(timeout:) and TCPSocket.open(connect_timeout:).
1 parent 34fdd01 commit 2cf80fe

2 files changed

Lines changed: 78 additions & 14 deletions

File tree

lib/openssl/ssl.rb

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -377,19 +377,39 @@ def sysclose
377377
end
378378
end
379379

380+
private def with_timeout(timeout)
381+
# IO#timeout= was added in Ruby 3.2
382+
timeout ||= self.timeout if IO.method_defined?(:timeout)
383+
if timeout.nil?
384+
while true
385+
yield
386+
end
387+
else
388+
remaining = timeout
389+
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
390+
while remaining >= 0
391+
yield remaining
392+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
393+
remaining = timeout - (now - start)
394+
end
395+
end
396+
raise IO_TimeoutError, "user specified timeout for SSL handshake"
397+
end
398+
380399
# :call-seq:
381-
# ssl.connect -> self
400+
# ssl.connect(timeout: nil) -> self
382401
#
383402
# Initiates an SSL/TLS handshake with a server.
384-
def connect
385-
while true
403+
#
404+
# If _timeout_ is specified, and if the handshake does not complete
405+
# within _timeout_ seconds, IO::TimeoutError is raised.
406+
def connect(timeout: nil)
407+
with_timeout(timeout) do |remaining|
386408
case ret = ssl_connect
387409
when :wait_readable
388-
wait_readable or
389-
raise IO_TimeoutError, "Timed out while waiting to become readable!"
410+
wait_readable(remaining)
390411
when :wait_writable
391-
wait_writable or
392-
raise IO_TimeoutError, "Timed out while waiting to become writable!"
412+
wait_writable(remaining)
393413
else
394414
return ret
395415
end
@@ -424,18 +444,19 @@ def connect_nonblock(exception: true)
424444
end
425445

426446
# :call-seq:
427-
# ssl.accept -> self
447+
# ssl.accept(timeout: nil) -> self
428448
#
429449
# Waits for a SSL/TLS client to initiate a handshake.
430-
def accept
431-
while true
450+
#
451+
# If _timeout_ is specified, and if the handshake does not complete
452+
# within _timeout_ seconds, IO::TimeoutError is raised.
453+
def accept(timeout: nil)
454+
with_timeout(timeout) do |remaining|
432455
case ret = ssl_accept
433456
when :wait_readable
434-
wait_readable or
435-
raise IO_TimeoutError, "Timed out while waiting to become readable!"
457+
wait_readable(remaining)
436458
when :wait_writable
437-
wait_writable or
438-
raise IO_TimeoutError, "Timed out while waiting to become writable!"
459+
wait_writable(remaining)
439460
else
440461
return ret
441462
end

test/openssl/test_ssl.rb

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,49 @@ def test_connect_accept_nonblock
186186
end
187187
end
188188

189+
def test_connect_timeout
190+
timeout_error = defined?(IO::TimeoutError) ? IO::TimeoutError : IOError
191+
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
192+
server_proc = proc do |sock|
193+
# TLS 1.2 handshake takes 2 RTTs
194+
ctx = make_server_context
195+
ctx.max_version = OpenSSL::SSL::TLS1_2_VERSION
196+
ssl = OpenSSL::SSL::SSLSocket.new(sock, ctx)
197+
198+
case (sleep 0.1; ssl.accept_nonblock(exception: false))
199+
when :wait_readable then ssl.wait_readable
200+
when :wait_writable then ssl.wait_writable
201+
else break
202+
end while true
203+
readwrite_loop(ssl)
204+
rescue OpenSSL::SSL::SSLError, SystemCallError
205+
end
206+
start_server_proc(server_proc) do |port|
207+
th = []
208+
th << Thread.new do
209+
sock = TCPSocket.new("127.0.0.1", port)
210+
sock.setsockopt(:TCP, :NODELAY, 1)
211+
ssl = OpenSSL::SSL::SSLSocket.new(sock)
212+
assert_raise(timeout_error) { ssl.connect(timeout: 0.05) }
213+
taken = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
214+
assert_operator(taken, :<, 0.2)
215+
ensure
216+
sock.close
217+
end
218+
th << Thread.new do
219+
sock = TCPSocket.new("127.0.0.1", port)
220+
sock.setsockopt(:TCP, :NODELAY, 1)
221+
ssl = OpenSSL::SSL::SSLSocket.new(sock)
222+
ssl.connect(timeout: 1)
223+
taken = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
224+
assert_operator(taken, :>=, 0.2)
225+
ensure
226+
sock.close
227+
end
228+
assert_join_threads(th)
229+
end
230+
end
231+
189232
def test_low_level_socket
190233
start_server do |port|
191234
sock = Socket.tcp("127.0.0.1", port)

0 commit comments

Comments
 (0)