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:
Jakub Stasiak
2016-07-11 17:27:01 +02:00
parent a0f8521eee
commit 8e92d15d26
2 changed files with 97 additions and 10 deletions

View File

@@ -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(),

View File

@@ -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()