Skip to content

Commit a03c12e

Browse files
zbristowandymccurdy
authored andcommitted
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.
1 parent 29a5259 commit a03c12e

File tree

4 files changed

+62
-6
lines changed

4 files changed

+62
-6
lines changed

CHANGES

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
* 3.3.9
2+
* Mapped Python 2.7 SSLError to TimeoutError where appropriate. Timeouts
3+
should now consistently raise TimeoutErrors on Python 2.7 for both
4+
unsecured and secured connections. Thanks @zbristow. #1222
15
* 3.3.8
26
* Fixed MONITOR parsing to properly parse IPv6 client addresses, unix
37
socket connections and commands issued from Lua. Thanks @kukey. #1201

redis/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ def int_or_str(value):
2929
return value
3030

3131

32-
__version__ = '3.3.8'
32+
__version__ = '3.3.9'
3333
VERSION = tuple(map(int_or_str, __version__.split('.')))
3434

3535
__all__ = [

redis/_compat.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,19 @@
33
import socket
44
import sys
55

6+
7+
def sendall(sock, *args, **kwargs):
8+
return sock.sendall(*args, **kwargs)
9+
10+
11+
def shutdown(sock, *args, **kwargs):
12+
return sock.shutdown(*args, **kwargs)
13+
14+
15+
def ssl_wrap_socket(context, sock, *args, **kwargs):
16+
return context.wrap_socket(sock, *args, **kwargs)
17+
18+
619
# For Python older than 3.5, retry EINTR.
720
if sys.version_info[0] < 3 or (sys.version_info[0] == 3 and
821
sys.version_info[1] < 5):
@@ -60,6 +73,43 @@ def recv(sock, *args, **kwargs):
6073
def recv_into(sock, *args, **kwargs):
6174
return sock.recv_into(*args, **kwargs)
6275

76+
if sys.version_info[0] < 3:
77+
# In Python 3, the ssl module raises socket.timeout whereas it raises
78+
# SSLError in Python 2. For compatibility between versions, ensure
79+
# socket.timeout is raised for both.
80+
import functools
81+
82+
try:
83+
from ssl import SSLError as _SSLError
84+
except ImportError:
85+
class _SSLError(Exception):
86+
"""A replacement in case ssl.SSLError is not available."""
87+
pass
88+
89+
_EXPECTED_SSL_TIMEOUT_MESSAGES = (
90+
"The handshake operation timed out",
91+
"The read operation timed out",
92+
"The write operation timed out",
93+
)
94+
95+
def _handle_ssl_timeout(func):
96+
@functools.wraps(func)
97+
def wrapper(*args, **kwargs):
98+
try:
99+
return func(*args, **kwargs)
100+
except _SSLError as e:
101+
if any(x in e.args[0] for x in _EXPECTED_SSL_TIMEOUT_MESSAGES):
102+
# Raise socket.timeout for compatibility with Python 3.
103+
raise socket.timeout(*e.args)
104+
raise
105+
return wrapper
106+
107+
recv = _handle_ssl_timeout(recv)
108+
recv_into = _handle_ssl_timeout(recv_into)
109+
sendall = _handle_ssl_timeout(sendall)
110+
shutdown = _handle_ssl_timeout(shutdown)
111+
ssl_wrap_socket = _handle_ssl_timeout(ssl_wrap_socket)
112+
63113
if sys.version_info[0] < 3:
64114
from urllib import unquote
65115
from urlparse import parse_qs, urlparse

redis/connection.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
from redis._compat import (xrange, imap, byte_to_chr, unicode, long,
1414
nativestr, basestring, iteritems,
1515
LifoQueue, Empty, Full, urlparse, parse_qs,
16-
recv, recv_into, unquote, BlockingIOError)
16+
recv, recv_into, unquote, BlockingIOError,
17+
sendall, shutdown, ssl_wrap_socket)
1718
from redis.exceptions import (
1819
AuthenticationError,
1920
BusyLoadingError,
@@ -630,7 +631,7 @@ def disconnect(self):
630631
return
631632
try:
632633
if os.getpid() == self.pid:
633-
self._sock.shutdown(socket.SHUT_RDWR)
634+
shutdown(self._sock, socket.SHUT_RDWR)
634635
self._sock.close()
635636
except socket.error:
636637
pass
@@ -662,7 +663,7 @@ def send_packed_command(self, command, check_health=True):
662663
if isinstance(command, str):
663664
command = [command]
664665
for item in command:
665-
self._sock.sendall(item)
666+
sendall(self._sock, item)
666667
except socket.timeout:
667668
self.disconnect()
668669
raise TimeoutError("Timeout writing to socket")
@@ -815,11 +816,12 @@ def _connect(self):
815816
keyfile=self.keyfile)
816817
if self.ca_certs:
817818
context.load_verify_locations(self.ca_certs)
818-
sock = context.wrap_socket(sock, server_hostname=self.host)
819+
sock = ssl_wrap_socket(context, sock, server_hostname=self.host)
819820
else:
820821
# In case this code runs in a version which is older than 2.7.9,
821822
# we want to fall back to old code
822-
sock = ssl.wrap_socket(sock,
823+
sock = ssl_wrap_socket(ssl,
824+
sock,
823825
cert_reqs=self.cert_reqs,
824826
keyfile=self.keyfile,
825827
certfile=self.certfile,

0 commit comments

Comments
 (0)