Skip to content

Commit

Permalink
Version 3.3.9
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
zbristow authored and andymccurdy committed Oct 10, 2019
1 parent 29a5259 commit a03c12e
Show file tree
Hide file tree
Showing 4 changed files with 62 additions and 6 deletions.
4 changes: 4 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion redis/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__ = [
Expand Down
50 changes: 50 additions & 0 deletions redis/_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,19 @@
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):
Expand Down Expand Up @@ -60,6 +73,43 @@ def recv(sock, *args, **kwargs):
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
Expand Down
12 changes: 7 additions & 5 deletions redis/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
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,
Expand Down Expand Up @@ -630,7 +631,7 @@ def disconnect(self):
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
Expand Down Expand Up @@ -662,7 +663,7 @@ def send_packed_command(self, command, check_health=True):
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")
Expand Down Expand Up @@ -815,11 +816,12 @@ def _connect(self):
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,
Expand Down

0 comments on commit a03c12e

Please sign in to comment.