ssl: Fix recv_into blocking when reading chunks of data
The comment in the test should explain the issue sufficiently. Turns out that request/urllib3 use recv_into() instad of recv() on Python 3 so: Closes https://github.com/eventlet/eventlet/issues/315. This patch is contributed by Smarkets Limited.
This commit is contained in:
@@ -191,18 +191,48 @@ class GreenSSLSocket(_original_sslsocket):
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
def recv(self, buflen=1024, flags=0):
|
def recv(self, buflen=1024, flags=0):
|
||||||
|
return self._base_recv(buflen, flags, into=False)
|
||||||
|
|
||||||
|
def recv_into(self, buffer, nbytes=None, flags=0):
|
||||||
|
# Copied verbatim from CPython
|
||||||
|
if buffer and nbytes is None:
|
||||||
|
nbytes = len(buffer)
|
||||||
|
elif nbytes is None:
|
||||||
|
nbytes = 1024
|
||||||
|
# end of CPython code
|
||||||
|
|
||||||
|
return self._base_recv(nbytes, flags, into=True, buffer_=buffer)
|
||||||
|
|
||||||
|
def _base_recv(self, nbytes, flags, into, buffer_=None):
|
||||||
|
if into:
|
||||||
|
plain_socket_function = socket.recv_into
|
||||||
|
else:
|
||||||
|
plain_socket_function = socket.recv
|
||||||
|
|
||||||
# *NOTE: gross, copied code from ssl.py becase it's not factored well enough to be used as-is
|
# *NOTE: gross, copied code from ssl.py becase it's not factored well enough to be used as-is
|
||||||
if self._sslobj:
|
if self._sslobj:
|
||||||
if flags != 0:
|
if flags != 0:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"non-zero flags not allowed in calls to recv() on %s" %
|
"non-zero flags not allowed in calls to %s() on %s" %
|
||||||
self.__class__)
|
plain_socket_function.__name__, self.__class__)
|
||||||
read = self.read(buflen)
|
if sys.version_info < (2, 7) and into:
|
||||||
|
# Python 2.6 SSLSocket.read() doesn't support reading into
|
||||||
|
# a given buffer so we need to emulate
|
||||||
|
data = self.read(nbytes)
|
||||||
|
buffer_[:len(data)] = data
|
||||||
|
read = len(data)
|
||||||
|
elif into:
|
||||||
|
read = self.read(nbytes, buffer_)
|
||||||
|
else:
|
||||||
|
read = self.read(nbytes)
|
||||||
return read
|
return read
|
||||||
else:
|
else:
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
return socket.recv(self, buflen, flags)
|
args = [self, nbytes, flags]
|
||||||
|
if into:
|
||||||
|
args.insert(1, buffer_)
|
||||||
|
return plain_socket_function(*args)
|
||||||
except orig_socket.error as e:
|
except orig_socket.error as e:
|
||||||
if self.act_non_blocking:
|
if self.act_non_blocking:
|
||||||
raise
|
raise
|
||||||
@@ -218,12 +248,6 @@ class GreenSSLSocket(_original_sslsocket):
|
|||||||
return b''
|
return b''
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def recv_into(self, buffer, nbytes=None, flags=0):
|
|
||||||
if not self.act_non_blocking:
|
|
||||||
trampoline(self, read=True, timeout=self.gettimeout(),
|
|
||||||
timeout_exc=timeout_exc('timed out'))
|
|
||||||
return super(GreenSSLSocket, self).recv_into(buffer, nbytes, flags)
|
|
||||||
|
|
||||||
def recvfrom(self, addr, buflen=1024, flags=0):
|
def recvfrom(self, addr, buflen=1024, flags=0):
|
||||||
if not self.act_non_blocking:
|
if not self.act_non_blocking:
|
||||||
trampoline(self, read=True, timeout=self.gettimeout(),
|
trampoline(self, read=True, timeout=self.gettimeout(),
|
||||||
|
@@ -1,12 +1,15 @@
|
|||||||
|
import contextlib
|
||||||
import socket
|
import socket
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
import eventlet
|
import eventlet
|
||||||
from eventlet import greenio
|
from eventlet import greenio
|
||||||
|
from eventlet.green import socket
|
||||||
try:
|
try:
|
||||||
from eventlet.green import ssl
|
from eventlet.green import ssl
|
||||||
except ImportError:
|
except ImportError:
|
||||||
__test__ = False
|
__test__ = False
|
||||||
|
from eventlet.support import six
|
||||||
import tests
|
import tests
|
||||||
|
|
||||||
|
|
||||||
@@ -205,3 +208,63 @@ class SSLTest(tests.LimitedTestCase):
|
|||||||
listener.close()
|
listener.close()
|
||||||
loopt.wait()
|
loopt.wait()
|
||||||
eventlet.sleep(0)
|
eventlet.sleep(0)
|
||||||
|
|
||||||
|
def test_receiving_doesnt_block_if_there_is_already_decrypted_buffered_data(self):
|
||||||
|
# Here's what could (and would) happen before the relevant bug was fixed (assuming method
|
||||||
|
# M was trampolining unconditionally before actually reading):
|
||||||
|
# 1. One side sends n bytes, leaves connection open (important)
|
||||||
|
# 2. The other side uses method M to read m (where m < n) bytes, the underlying SSL
|
||||||
|
# implementation reads everything from the underlying socket, decrypts all n bytes,
|
||||||
|
# returns m of them and buffers n-m to be read later.
|
||||||
|
# 3. The other side tries to read the remainder of the data (n-m bytes), this blocks
|
||||||
|
# because M trampolines uncoditionally and trampoline will hang because reading from
|
||||||
|
# the underlying socket would block. It would block because there's no data to be read
|
||||||
|
# and the connection is still open; leaving the connection open /mentioned in 1./ is
|
||||||
|
# important because otherwise trampoline would return immediately and the test would pass
|
||||||
|
# even with the bug still present in the code).
|
||||||
|
#
|
||||||
|
# The solution is to first request data from the underlying SSL implementation and only
|
||||||
|
# trampoline if we actually need to read some data from the underlying socket.
|
||||||
|
#
|
||||||
|
# GreenSSLSocket.recv() wasn't broken but I've added code to test it as well for
|
||||||
|
# completeness.
|
||||||
|
content = b'xy'
|
||||||
|
|
||||||
|
def recv(sock, expected):
|
||||||
|
assert sock.recv(len(expected)) == expected
|
||||||
|
|
||||||
|
def recv_into(sock, expected):
|
||||||
|
buf = bytearray(len(expected))
|
||||||
|
assert sock.recv_into(buf, len(expected)) == len(expected)
|
||||||
|
assert buf == expected
|
||||||
|
|
||||||
|
for read_function in [recv, recv_into]:
|
||||||
|
print('Trying %s...' % (read_function,))
|
||||||
|
listener = listen_ssl_socket()
|
||||||
|
|
||||||
|
def accept(listener):
|
||||||
|
sock, addr = listener.accept()
|
||||||
|
sock.sendall(content)
|
||||||
|
return sock
|
||||||
|
|
||||||
|
accepter = eventlet.spawn(accept, listener)
|
||||||
|
|
||||||
|
client_to_server = None
|
||||||
|
try:
|
||||||
|
client_to_server = ssl.wrap_socket(eventlet.connect(listener.getsockname()))
|
||||||
|
for character in six.iterbytes(content):
|
||||||
|
character = six.int2byte(character)
|
||||||
|
print('We have %d already decrypted bytes pending, expecting: %s' % (
|
||||||
|
client_to_server.pending(), character))
|
||||||
|
read_function(client_to_server, character)
|
||||||
|
finally:
|
||||||
|
if client_to_server is not None:
|
||||||
|
client_to_server.close()
|
||||||
|
server_to_client = accepter.wait()
|
||||||
|
|
||||||
|
# Very important: we only want to close the socket *after* the other side has
|
||||||
|
# read the data it wanted already, otherwise this would defeat the purpose of the
|
||||||
|
# test (see the comment at the top of this test).
|
||||||
|
server_to_client.close()
|
||||||
|
|
||||||
|
listener.close()
|
||||||
|
Reference in New Issue
Block a user