The issue would manifest after performing the following steps on
Python 3.5:
* Create a plain socket without connecting to anything
* Wrap the plain socket in an SSL socket
* Call recv(n) on the SSL socket
This is the only place in the code where we actually assign
self._sslobj, I just modified the code to wrap the sslwrap result in
SSLObject like the standard library SSLSocket.connect does[1] since
Python 3.5[2].
[1] 9ec0aa138b/Lib/ssl.py (L1008)
[2] http://bugs.python.org/issue21965
		
	
		
			
				
	
	
		
			306 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			306 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
import contextlib
 | 
						|
import socket
 | 
						|
import warnings
 | 
						|
 | 
						|
import eventlet
 | 
						|
from eventlet import greenio
 | 
						|
from eventlet.green import socket
 | 
						|
try:
 | 
						|
    from eventlet.green import ssl
 | 
						|
except ImportError:
 | 
						|
    __test__ = False
 | 
						|
from eventlet.support import six
 | 
						|
import tests
 | 
						|
 | 
						|
 | 
						|
def listen_ssl_socket(address=('localhost', 0), **kwargs):
 | 
						|
    sock = ssl.wrap_socket(
 | 
						|
        socket.socket(),
 | 
						|
        tests.private_key_file,
 | 
						|
        tests.certificate_file,
 | 
						|
        server_side=True,
 | 
						|
        **kwargs
 | 
						|
    )
 | 
						|
    sock.bind(address)
 | 
						|
    sock.listen(50)
 | 
						|
    return sock
 | 
						|
 | 
						|
 | 
						|
class SSLTest(tests.LimitedTestCase):
 | 
						|
    def setUp(self):
 | 
						|
        # disabling socket.ssl warnings because we're testing it here
 | 
						|
        warnings.filterwarnings(
 | 
						|
            action='ignore',
 | 
						|
            message='.*socket.ssl.*',
 | 
						|
            category=DeprecationWarning)
 | 
						|
 | 
						|
        super(SSLTest, self).setUp()
 | 
						|
 | 
						|
    def test_duplex_response(self):
 | 
						|
        def serve(listener):
 | 
						|
            sock, addr = listener.accept()
 | 
						|
            sock.recv(8192)
 | 
						|
            sock.sendall(b'response')
 | 
						|
 | 
						|
        sock = listen_ssl_socket()
 | 
						|
 | 
						|
        server_coro = eventlet.spawn(serve, sock)
 | 
						|
 | 
						|
        client = ssl.wrap_socket(eventlet.connect(sock.getsockname()))
 | 
						|
        client.sendall(b'line 1\r\nline 2\r\n\r\n')
 | 
						|
        self.assertEqual(client.recv(8192), b'response')
 | 
						|
        server_coro.wait()
 | 
						|
 | 
						|
    def test_ssl_close(self):
 | 
						|
        def serve(listener):
 | 
						|
            sock, addr = listener.accept()
 | 
						|
            sock.recv(8192)
 | 
						|
            try:
 | 
						|
                self.assertEqual(b'', sock.recv(8192))
 | 
						|
            except greenio.SSL.ZeroReturnError:
 | 
						|
                pass
 | 
						|
 | 
						|
        sock = listen_ssl_socket()
 | 
						|
 | 
						|
        server_coro = eventlet.spawn(serve, sock)
 | 
						|
 | 
						|
        raw_client = eventlet.connect(sock.getsockname())
 | 
						|
        client = ssl.wrap_socket(raw_client)
 | 
						|
        client.sendall(b'X')
 | 
						|
        greenio.shutdown_safe(client)
 | 
						|
        client.close()
 | 
						|
        server_coro.wait()
 | 
						|
 | 
						|
    def test_ssl_connect(self):
 | 
						|
        def serve(listener):
 | 
						|
            sock, addr = listener.accept()
 | 
						|
            sock.recv(8192)
 | 
						|
        sock = listen_ssl_socket()
 | 
						|
        server_coro = eventlet.spawn(serve, sock)
 | 
						|
 | 
						|
        raw_client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 | 
						|
        ssl_client = ssl.wrap_socket(raw_client)
 | 
						|
        ssl_client.connect(sock.getsockname())
 | 
						|
        ssl_client.sendall(b'abc')
 | 
						|
        greenio.shutdown_safe(ssl_client)
 | 
						|
        ssl_client.close()
 | 
						|
        server_coro.wait()
 | 
						|
 | 
						|
    def test_recv_after_ssl_connect(self):
 | 
						|
        def serve(listener):
 | 
						|
            sock, addr = listener.accept()
 | 
						|
            sock.sendall(b'hjk')
 | 
						|
        sock = listen_ssl_socket()
 | 
						|
        server_coro = eventlet.spawn(serve, sock)
 | 
						|
 | 
						|
        raw_client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 | 
						|
        ssl_client = ssl.wrap_socket(raw_client)
 | 
						|
        # Important: We need to call connect() on an SSL socket, not a plain one.
 | 
						|
        # The bug was affecting that particular combination (create plain socket,
 | 
						|
        # wrap, call connect() on the SSL socket and try to recv) on Python 3.5.
 | 
						|
        ssl_client.connect(sock.getsockname())
 | 
						|
 | 
						|
        # The call to recv used to fail with:
 | 
						|
        # Traceback (most recent call last):
 | 
						|
        #   File "tests/ssl_test.py", line 99, in test_recv_after_ssl_connect
 | 
						|
        #     self.assertEqual(ssl_client.recv(3), b'hjk')
 | 
						|
        #   File "eventlet/green/ssl.py", line 194, in recv
 | 
						|
        #     return self._base_recv(buflen, flags, into=False)
 | 
						|
        #   File "eventlet/green/ssl.py", line 227, in _base_recv
 | 
						|
        #     read = self.read(nbytes)
 | 
						|
        #   File "eventlet/green/ssl.py", line 139, in read
 | 
						|
        #     super(GreenSSLSocket, self).read, *args, **kwargs)
 | 
						|
        #   File "eventlet/green/ssl.py", line 113, in _call_trampolining
 | 
						|
        #     return func(*a, **kw)
 | 
						|
        #   File "PYTHONLIB/python3.5/ssl.py", line 791, in read
 | 
						|
        #     return self._sslobj.read(len, buffer)
 | 
						|
        # TypeError: read() argument 2 must be read-write bytes-like object, not None
 | 
						|
        self.assertEqual(ssl_client.recv(3), b'hjk')
 | 
						|
 | 
						|
        greenio.shutdown_safe(ssl_client)
 | 
						|
        ssl_client.close()
 | 
						|
        server_coro.wait()
 | 
						|
 | 
						|
    def test_ssl_unwrap(self):
 | 
						|
        def serve():
 | 
						|
            sock, addr = listener.accept()
 | 
						|
            self.assertEqual(sock.recv(6), b'before')
 | 
						|
            sock_ssl = ssl.wrap_socket(sock, tests.private_key_file, tests.certificate_file,
 | 
						|
                                       server_side=True)
 | 
						|
            sock_ssl.do_handshake()
 | 
						|
            self.assertEqual(sock_ssl.recv(6), b'during')
 | 
						|
            sock2 = sock_ssl.unwrap()
 | 
						|
            self.assertEqual(sock2.recv(5), b'after')
 | 
						|
            sock2.close()
 | 
						|
 | 
						|
        listener = eventlet.listen(('127.0.0.1', 0))
 | 
						|
        server_coro = eventlet.spawn(serve)
 | 
						|
        client = eventlet.connect(listener.getsockname())
 | 
						|
        client.sendall(b'before')
 | 
						|
        client_ssl = ssl.wrap_socket(client)
 | 
						|
        client_ssl.do_handshake()
 | 
						|
        client_ssl.sendall(b'during')
 | 
						|
        client2 = client_ssl.unwrap()
 | 
						|
        client2.sendall(b'after')
 | 
						|
        server_coro.wait()
 | 
						|
 | 
						|
    def test_sendall_cpu_usage(self):
 | 
						|
        """SSL socket.sendall() busy loop
 | 
						|
 | 
						|
        https://bitbucket.org/eventlet/eventlet/issue/134/greenssl-performance-issues
 | 
						|
 | 
						|
        Idea of this test is to check that GreenSSLSocket.sendall() does not busy loop
 | 
						|
        retrying .send() calls, but instead trampolines until socket is writeable.
 | 
						|
 | 
						|
        BUFFER_SIZE and SENDALL_SIZE are magic numbers inferred through trial and error.
 | 
						|
        """
 | 
						|
        # Time limit resistant to busy loops
 | 
						|
        self.set_alarm(1)
 | 
						|
 | 
						|
        stage_1 = eventlet.event.Event()
 | 
						|
        BUFFER_SIZE = 1000
 | 
						|
        SENDALL_SIZE = 100000
 | 
						|
 | 
						|
        def serve(listener):
 | 
						|
            conn, _ = listener.accept()
 | 
						|
            conn.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, BUFFER_SIZE)
 | 
						|
            self.assertEqual(conn.recv(8), b'request')
 | 
						|
            conn.sendall(b'response')
 | 
						|
 | 
						|
            stage_1.wait()
 | 
						|
            conn.sendall(b'x' * SENDALL_SIZE)
 | 
						|
 | 
						|
        server_sock = listen_ssl_socket()
 | 
						|
        server_coro = eventlet.spawn(serve, server_sock)
 | 
						|
 | 
						|
        client_sock = eventlet.connect(server_sock.getsockname())
 | 
						|
        client_sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, BUFFER_SIZE)
 | 
						|
        client = ssl.wrap_socket(client_sock)
 | 
						|
        client.sendall(b'request')
 | 
						|
        self.assertEqual(client.recv(8), b'response')
 | 
						|
        stage_1.send()
 | 
						|
 | 
						|
        tests.check_idle_cpu_usage(0.2, 0.1)
 | 
						|
        server_coro.kill()
 | 
						|
 | 
						|
    def test_greensslobject(self):
 | 
						|
        def serve(listener):
 | 
						|
            sock, addr = listener.accept()
 | 
						|
            sock.sendall(b'content')
 | 
						|
            greenio.shutdown_safe(sock)
 | 
						|
            sock.close()
 | 
						|
        listener = listen_ssl_socket()
 | 
						|
        eventlet.spawn(serve, listener)
 | 
						|
        client = ssl.wrap_socket(eventlet.connect(listener.getsockname()))
 | 
						|
        self.assertEqual(client.recv(1024), b'content')
 | 
						|
        self.assertEqual(client.recv(1024), b'')
 | 
						|
 | 
						|
    def test_regression_gh_17(self):
 | 
						|
        # https://github.com/eventlet/eventlet/issues/17
 | 
						|
        # ssl wrapped but unconnected socket methods go special code path
 | 
						|
        # test that path at least for syntax/typo errors
 | 
						|
        sock = ssl.wrap_socket(socket.socket())
 | 
						|
        sock.settimeout(0.01)
 | 
						|
        try:
 | 
						|
            sock.sendall(b'')
 | 
						|
        except ssl.SSLError as e:
 | 
						|
            assert 'timed out' in str(e)
 | 
						|
 | 
						|
    def test_no_handshake_block_accept_loop(self):
 | 
						|
        listener = listen_ssl_socket()
 | 
						|
        listener.settimeout(0.3)
 | 
						|
 | 
						|
        def serve(sock):
 | 
						|
            try:
 | 
						|
                name = sock.recv(8)
 | 
						|
                sock.sendall(b'hello ' + name)
 | 
						|
            except Exception:
 | 
						|
                # ignore evil clients
 | 
						|
                pass
 | 
						|
            finally:
 | 
						|
                greenio.shutdown_safe(sock)
 | 
						|
                sock.close()
 | 
						|
 | 
						|
        def accept_loop():
 | 
						|
            while True:
 | 
						|
                try:
 | 
						|
                    sock, _ = listener.accept()
 | 
						|
                except socket.error:
 | 
						|
                    return
 | 
						|
                eventlet.spawn(serve, sock)
 | 
						|
 | 
						|
        loopt = eventlet.spawn(accept_loop)
 | 
						|
 | 
						|
        # evil no handshake
 | 
						|
        evil = eventlet.connect(listener.getsockname())
 | 
						|
        good = ssl.wrap_socket(eventlet.connect(listener.getsockname()))
 | 
						|
        good.sendall(b'good')
 | 
						|
        response = good.recv(16)
 | 
						|
        good.close()
 | 
						|
        assert response == b'hello good'
 | 
						|
        evil.close()
 | 
						|
 | 
						|
        listener.close()
 | 
						|
        loopt.wait()
 | 
						|
        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()
 |