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