From a03c12e5869469aeaf4016d75a883a1fceb992fd Mon Sep 17 00:00:00 2001 From: Zac Bristow Date: Fri, 20 Sep 2019 15:20:08 -0700 Subject: Version 3.3.9 Fixes SSL read timeouts in Python 2.7 The ssl module in Python 2.7 raises timeouts as ssl.SSLError instead of socket.timeout. When these timeouts are encountered, the error will be re-raised as socket.timeout so it is handled appropriately by the connection. --- CHANGES | 4 ++++ redis/__init__.py | 2 +- redis/_compat.py | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ redis/connection.py | 12 +++++++----- 4 files changed, 62 insertions(+), 6 deletions(-) diff --git a/CHANGES b/CHANGES index 37b2830..62493fb 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,7 @@ +* 3.3.9 + * Mapped Python 2.7 SSLError to TimeoutError where appropriate. Timeouts + should now consistently raise TimeoutErrors on Python 2.7 for both + unsecured and secured connections. Thanks @zbristow. #1222 * 3.3.8 * Fixed MONITOR parsing to properly parse IPv6 client addresses, unix socket connections and commands issued from Lua. Thanks @kukey. #1201 diff --git a/redis/__init__.py b/redis/__init__.py index 9395147..2835a42 100644 --- a/redis/__init__.py +++ b/redis/__init__.py @@ -29,7 +29,7 @@ def int_or_str(value): return value -__version__ = '3.3.8' +__version__ = '3.3.9' VERSION = tuple(map(int_or_str, __version__.split('.'))) __all__ = [ diff --git a/redis/_compat.py b/redis/_compat.py index d70af2a..39b6619 100644 --- a/redis/_compat.py +++ b/redis/_compat.py @@ -3,6 +3,19 @@ import errno import socket import sys + +def sendall(sock, *args, **kwargs): + return sock.sendall(*args, **kwargs) + + +def shutdown(sock, *args, **kwargs): + return sock.shutdown(*args, **kwargs) + + +def ssl_wrap_socket(context, sock, *args, **kwargs): + return context.wrap_socket(sock, *args, **kwargs) + + # For Python older than 3.5, retry EINTR. if sys.version_info[0] < 3 or (sys.version_info[0] == 3 and sys.version_info[1] < 5): @@ -60,6 +73,43 @@ else: # Python 3.5 and above automatically retry EINTR def recv_into(sock, *args, **kwargs): return sock.recv_into(*args, **kwargs) +if sys.version_info[0] < 3: + # In Python 3, the ssl module raises socket.timeout whereas it raises + # SSLError in Python 2. For compatibility between versions, ensure + # socket.timeout is raised for both. + import functools + + try: + from ssl import SSLError as _SSLError + except ImportError: + class _SSLError(Exception): + """A replacement in case ssl.SSLError is not available.""" + pass + + _EXPECTED_SSL_TIMEOUT_MESSAGES = ( + "The handshake operation timed out", + "The read operation timed out", + "The write operation timed out", + ) + + def _handle_ssl_timeout(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + try: + return func(*args, **kwargs) + except _SSLError as e: + if any(x in e.args[0] for x in _EXPECTED_SSL_TIMEOUT_MESSAGES): + # Raise socket.timeout for compatibility with Python 3. + raise socket.timeout(*e.args) + raise + return wrapper + + recv = _handle_ssl_timeout(recv) + recv_into = _handle_ssl_timeout(recv_into) + sendall = _handle_ssl_timeout(sendall) + shutdown = _handle_ssl_timeout(shutdown) + ssl_wrap_socket = _handle_ssl_timeout(ssl_wrap_socket) + if sys.version_info[0] < 3: from urllib import unquote from urlparse import parse_qs, urlparse diff --git a/redis/connection.py b/redis/connection.py index 333bd75..feea041 100755 --- a/redis/connection.py +++ b/redis/connection.py @@ -13,7 +13,8 @@ import warnings from redis._compat import (xrange, imap, byte_to_chr, unicode, long, nativestr, basestring, iteritems, LifoQueue, Empty, Full, urlparse, parse_qs, - recv, recv_into, unquote, BlockingIOError) + recv, recv_into, unquote, BlockingIOError, + sendall, shutdown, ssl_wrap_socket) from redis.exceptions import ( AuthenticationError, BusyLoadingError, @@ -630,7 +631,7 @@ class Connection(object): return try: if os.getpid() == self.pid: - self._sock.shutdown(socket.SHUT_RDWR) + shutdown(self._sock, socket.SHUT_RDWR) self._sock.close() except socket.error: pass @@ -662,7 +663,7 @@ class Connection(object): if isinstance(command, str): command = [command] for item in command: - self._sock.sendall(item) + sendall(self._sock, item) except socket.timeout: self.disconnect() raise TimeoutError("Timeout writing to socket") @@ -815,11 +816,12 @@ class SSLConnection(Connection): keyfile=self.keyfile) if self.ca_certs: context.load_verify_locations(self.ca_certs) - sock = context.wrap_socket(sock, server_hostname=self.host) + sock = ssl_wrap_socket(context, sock, server_hostname=self.host) else: # In case this code runs in a version which is older than 2.7.9, # we want to fall back to old code - sock = ssl.wrap_socket(sock, + sock = ssl_wrap_socket(ssl, + sock, cert_reqs=self.cert_reqs, keyfile=self.keyfile, certfile=self.certfile, -- cgit v1.2.1