import random
import sys
import warnings

import pytest

import eventlet
from eventlet import greenio
from eventlet.green import socket
try:
    from eventlet.green import ssl
except ImportError:
    __test__ = False
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().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_context(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)

        context = ssl.create_default_context()
        context.verify_mode = ssl.CERT_REQUIRED
        context.check_hostname = True
        context.load_verify_locations(tests.certificate_file)

        client = context.wrap_socket(
            eventlet.connect(sock.getsockname()),
            server_hostname='Test')
        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'')

    @pytest.mark.xfail(sys.platform == "darwin", reason="Fails on macOS for some reason")
    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 OSError:
                    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 iter(content):
                    character = bytes((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()

    def test_context_wrapped_accept(self):
        context = ssl.SSLContext(ssl.PROTOCOL_TLS)
        context.load_cert_chain(tests.certificate_file, tests.private_key_file)
        expected = "success:{}".format(random.random()).encode()

        def client(addr):
            client_tls = ssl.wrap_socket(
                eventlet.connect(addr),
                cert_reqs=ssl.CERT_REQUIRED,
                ca_certs=tests.certificate_file,
            )
            client_tls.send(expected)

        server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        server_sock.bind(('localhost', 0))
        server_sock.listen(1)
        eventlet.spawn(client, server_sock.getsockname())
        server_tls = context.wrap_socket(server_sock, server_side=True)
        peer, _ = server_tls.accept()
        assert peer.recv(64) == expected
        peer.close()

    def test_explicit_keys_accept(self):
        expected = "success:{}".format(random.random()).encode()

        def client(addr):
            client_tls = ssl.wrap_socket(
                eventlet.connect(addr),
                cert_reqs=ssl.CERT_REQUIRED,
                ca_certs=tests.certificate_file,
            )
            client_tls.send(expected)

        server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        server_sock.bind(('localhost', 0))
        server_sock.listen(1)
        eventlet.spawn(client, server_sock.getsockname())
        server_tls = ssl.wrap_socket(
            server_sock, server_side=True,
            keyfile=tests.private_key_file, certfile=tests.certificate_file,
        )
        peer, _ = server_tls.accept()
        assert peer.recv(64) == expected
        peer.close()

    def test_client_check_hostname(self):
        # stdlib API compatibility
        # https://github.com/eventlet/eventlet/issues/567
        def serve(listener):
            sock, addr = listener.accept()
            sock.recv(64)
            sock.sendall(b"response")
            sock.close()

        listener = listen_ssl_socket()
        server_coro = eventlet.spawn(serve, listener)
        ctx = ssl.create_default_context()
        ctx.verify_mode = ssl.CERT_REQUIRED
        ctx.check_hostname = True
        ctx.load_verify_locations(tests.certificate_file)
        ctx.load_cert_chain(tests.certificate_file, tests.private_key_file)
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        client = ctx.wrap_socket(sock, server_hostname="Test")
        client.connect(listener.getsockname())
        client.send(b"check_hostname works")
        client.recv(64)
        server_coro.wait()

    @tests.skip_if(sys.version_info < (3, 7))
    def test_context_version_setters(self):
        tests.run_isolated("ssl_context_version_setters.py")
