You should set timeout on a socket and not on a context.
You should consider two possible cases:
- TCP connection hangs
- SSL connection hangs
For scenario #1 you can just set timeout on a TCP socket. When you set a timeout on a socket then it becomes non-blocking. This means that whenever OpenSSL requires more data instead of blocking it will throw an error indicating that operation should be retried. So OpenSSL timeout will be number of retries * delay between retries.
Example client showing how this should be implemented (this is python3 code and to run it locally you need to install pyopenssl and retry):
from socket import socket
from OpenSSL import SSL
from retry import retry
def connect(host, port):
tcp_socket = get_tcp_socket()
ssl_socket = get_ssl_socket(tcp_socket)
ssl_socket.connect((host, port))
do_hanshake(ssl_socket)
print("Successfully connected to: ", host, port)
def get_tcp_socket():
tcp_socket = socket()
# by setting timeout to 2 seconds we
# make the socket non-blocking
tcp_socket.settimeout(2)
return tcp_socket
def get_ssl_socket(tcp_socket):
ssl_context = SSL.Context(SSL.TLSv1_2_METHOD)
# since we are using non-blocking TCP socket
# this makes SSL socket also non-blocking
ssl_socket = SSL.Connection(ssl_context, tcp_socket)
# indicate that socket should operate in client mode
ssl_socket.set_connect_state()
return ssl_socket
@retry((SSL.WantReadError), tries=25, delay=0.1)
def do_hanshake(ssl_socket):
"""
If the SSL socket is non-blocking we have to do retries
since SSL.WantReadError indicates that OpenSSL requires
more data to complete the operation.
"""
print("Retry")
ssl_socket.do_handshake()
"""
Connection will succeed.
SSL.WantReadError might be thrown couple of times.
"""
connect("google.com", 443)
"""
We will get a TCP level timeout since port 81 is not open.
Traceback (most recent call last):
File "tmp.py", line 52, in <module>
connect_but_fail_with_timeout()
File "tmp.py", line 19, in connect_but_fail_with_timeout
ssl_socket.connect(host_and_port)
File ~/.virtualenvs/tmp/lib/python3.8/site-packages/OpenSSL/SSL.py", line 1859, in connect
return self._socket.connect(addr)
socket.timeout: timed out
"""
connect("google.com", 81)
"""
TCP connection is accepted but SSL data exchange is very slow
Retry
....
Retry
Traceback (most recent call last):
File "client.py", line 65, in <module>
connect("localhost", 4430)
File "client.py", line 10, in connect
do_hanshake(ssl_socket)
File "~/.virtualenvs/tmp/lib/python3.8/site-packages/decorator.py", line 232, in fun
return caller(func, *(extras + args), **kw)
File "~/.virtualenvs/tmp/lib/python3.8/site-packages/retry/api.py", line 73, in retry_decorator
return __retry_internal(partial(f, *args, **kwargs), exceptions, tries, delay, max_delay, backoff, jitter,
File "~/.virtualenvs/tmp/lib/python3.8/site-packages/retry/api.py", line 33, in __retry_internal
return f()
File "client.py", line 39, in do_hanshake
ssl_socket.do_handshake()
File "~/.virtualenvs/tmp/lib/python3.8/site-packages/OpenSSL/SSL.py", line 1828, in do_handshake
self._raise_ssl_error(self._ssl, result)
File "~/.virtualenvs/tmp/lib/python3.8/site-packages/OpenSSL/SSL.py", line 1541, in _raise_ssl_error
raise WantReadError()
OpenSSL.SSL.WantReadError
"""
connect("localhost", 4430)
Last example uses TCP server that is slow to exchange data:
import socket
import time
# create a TCP/IP socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# set server port
server_address = ("", 4430)
sock.bind(server_address)
print("Starting server on %s port %s" % sock.getsockname())
# configure server to accept single connection
sock.listen(1)
while True:
print("Waiting for a connection")
tcp_socket, client_address = sock.accept()
try:
print("Client connected:", client_address)
# slowly send irelevant data
while True:
# wait for 2 second
time.sleep(2)
client_data = tcp_socket.recv(5)
print("received:", client_data)
if client_data:
tcp_socket.sendall(b"abc")
else:
break
finally:
tcp_socket.close()