Skip to content

Commit

Permalink
Merge pull request #520 from FosanzDev/asyncore-to-asyncio
Browse files Browse the repository at this point in the history
Python 3.12 asyncore to asyncio (#520)
  • Loading branch information
tomato42 authored Jun 24, 2024
2 parents c2295f1 + 5bceca1 commit 4d2c6b8
Show file tree
Hide file tree
Showing 6 changed files with 310 additions and 6 deletions.
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -568,13 +568,17 @@ handshake() method, doing some sort of server handshake on the connection
argument. If the handshake method returns True, the RequestHandler will be
triggered. See the tests/httpsserver.py example.

10 Using tlslite-ng with asyncore
10 Using tlslite-ng with asyncore (or asyncio - Python 3.12+)
=================================

tlslite-ng can be used with subclasses of asyncore.dispatcher. See the comments
in TLSAsyncDispatcherMixIn.py for details. This is still experimental, and
may not work with all asyncore.dispatcher subclasses.

as said above, asyncore is deprecated in Python 3.12, and asyncio should be used.
Implementation is similar to TLSAsyncDispatcherMixIn.py, but instead, use the class
TLSAsyncioDispatcherMixIn.py.

11 History
===========

Expand Down
8 changes: 8 additions & 0 deletions docs/tlslite.integration.tlsasynciodispatchermixin
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
tlslite.integration.tlsasynciodispatchermixin module
==================================================

.. automodule:: tlslite.integration.tlsasynciodispatchermixin
:members:
:special-members: __init__
:undoc-members:
:show-inheritance:
24 changes: 20 additions & 4 deletions tlslite/api.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
# Author: Trevor Perrin
# Authors:
# Trevor Perrin
# Esteban Sanchez (FosanzDev) - python 3.12 port
#
# See the LICENSE file for legal information regarding use of this file.

__version__ = "0.8.0-beta1"
Expand All @@ -18,12 +21,25 @@

from .integration.httptlsconnection import HTTPTLSConnection
from .integration.tlssocketservermixin import TLSSocketServerMixIn

try:
from .integration.tlsasynciodispatchermixin \
import TLSAsyncioDispatcherMixIn

except ImportError:
# NOTE: asyncio is not available in base python 2, so this try-except
# block is necessary to avoid breaking the import of the
# rest of the module.
pass

try:
from .integration.tlsasyncdispatchermixin import TLSAsyncDispatcherMixIn
except ModuleNotFoundError:
# asyncore was removed in 3.12, I don't use it, so don't know how
# to fix it
except ImportError:
# NOTE: Left this try-except block as is, due to the possibility to use
# both asyncore and asyncio in the same project no matter the python
# version (if the asyncore module is available).
pass

from .integration.pop3_tls import POP3_TLS
from .integration.imap4_tls import IMAP4_TLS
from .integration.smtp_tls import SMTP_TLS
Expand Down
3 changes: 2 additions & 1 deletion tlslite/integration/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@
"smtp_tls",
"xmlrpctransport",
"tlssocketservermixin",
"tlsasyncdispatchermixin"]
"tlsasyncdispatchermixin",
"tlsasynciodispatchermixin"]
169 changes: 169 additions & 0 deletions tlslite/integration/tlsasynciodispatchermixin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
# Authors:
# Esteban Sanchez (FosanzDev) - python 3.12 port
#
# See the LICENSE file for legal information regarding use of this file.

"""TLS Lite + asyncio."""

import asyncio
from tlslite.tlsconnection import TLSConnection
from .asyncstatemachine import AsyncStateMachine


class TLSAsyncioDispatcherMixIn(asyncio.Protocol):
"""
This class can be "mixed in" with an :py:class:`asyncio.Protocol`
to add TLS support.
This class essentially sits between the protocol and the asyncio
event loop, intercepting events and only
calling the protocol when applicable.
In the case of :py:meth:`data_received`, a read operation will be
activated, and when it completes, the bytes will be placed in a
buffer where the protocol can retrieve them by calling :py:meth:`recv`,
and the protocol's :py:meth:`data_received` will be called.
In the case of :py:meth:`send`, the protocol's :py:meth:`send` will
be called, and when it calls :py:meth:`send`, a write operation
will be activated.
To use this class, you must combine it with an asyncio.Protocol, and
pass in a handshake operation with setServerHandshakeOp().
Below is an example of using this class with aiohttp. This class
is mixed in with aiohttp's BaseProtocol to create http_tls_protocol.
Note:
1. the mix-in is listed first in the inheritance list
2. the input buffer size must be at least 16K, otherwise the protocol
might not read all the bytes from the TLS layer, leaving some
bytes in limbo.
3. IE seems to have a problem receiving a whole HTTP response
in a single TLS record, so HTML pages containing
'\\r\\n\\r\\n' won't be displayed on IE.
Add the following text into 'start_aiohttp.py', in the
'HTTP Server' section::
from tlslite import *
s = open("./serverX509Cert.pem").read()
x509 = X509()
x509.parse(s)
cert_chain = X509CertChain([x509])
s = open("./serverX509Key.pem").read()
privateKey = parsePEMKey(s, private=True)
class http_tls_protocol(TLSAsyncioProtocol,
aiohttp.BaseProtocol):
ac_in_buffer_size = 16384
def __init__ (self, server, conn, addr):
aiohttp.BaseProtocol.__init__(self, server, conn, addr)
TLSAsyncioProtocol.__init__(self, conn)
self.tls_connection.ignoreAbruptClose = True
self.setServerHandshakeOp(certChain=cert_chain,
privateKey=privateKey)
hs.protocol_class = http_tls_protocol
If the TLS layer raises an exception, the exception will be caught in
asyncio.Protocol, which will call :py:meth:`close` on this class.
The TLS layer always closes the TLS connection before raising an
exception, so the close operation will complete right away, causing
asyncio.Protocol.close() to be called, which closes the socket and
removes this instance from the asyncio event loop.
"""

def __init__(self, sock=None):
"""Initialize the protocol with the given socket."""
super().__init__()
if sock:
self.tls_connection = TLSConnection(sock)
self.sibling_class = self._get_sibling_class()

def _get_sibling_class(self):
"""Get the sibling class that this class is mixed in with."""
for cl in self.__class__.__bases__:
if cl not in (TLSAsyncioDispatcherMixIn, AsyncStateMachine):
return cl
raise AssertionError()

def connection_made(self, transport):
self.transport = transport
# Call the sibling class's connection_made method
if hasattr(self.sibling_class, 'connection_made'):
self.sibling_class.connection_made(transport)

def data_received(self, data):
self.read_buffer = data
if hasattr(self.sibling_class, 'data_received'):
self.sibling_class.data_received(self, data)

def connection_lost(self, exc):
self.sibling_class.connection_lost(self, exc)
if hasattr(self, "tls_connection"):
self.set_close_op()
else:
self.transport.close()

def readable(self):
"""Check if the protocol is ready for reading."""
result = self.wants_read_event()
return result if result is not None \
else self.sibling_class.readable(self)

def writable(self):
"""Check if the protocol is ready for writing."""
result = self.wants_write_event()
return result if result is not None \
else self.sibling_class.writable(self)

def handle_read(self):
"""Handle a read event."""
self.in_read_event()

def handle_write(self):
"""Handle a write event."""
self.in_write_event()

def out_connect_event(self):
"""Handle an outgoing connect event."""
self.sibling_class.handle_connect(self)

def out_close_event(self):
"""Handle an outgoing close event."""
self.transport.close()

def out_read_event(self, read_buffer):
"""Handle an outgoing read event."""
self.read_buffer = read_buffer
self.sibling_class.handle_read(self)

def out_write_event(self):
"""Handle an outgoing write event."""
self.sibling_class.handle_write(self)

def recv(self, buffer_size=16384):
"""Receive data."""
if buffer_size < 16384 or self.read_buffer is None:
raise AssertionError()
return_value = self.read_buffer
self.read_buffer = None
return return_value

def send(self, write_buffer):
self.set_write_op(write_buffer)
self.transport.write(write_buffer)
return len(write_buffer)

def close(self):
"""Close the connection."""
if hasattr(self, "tls_connection"):
self.set_close_op()
else:
self.transport.close()
106 changes: 106 additions & 0 deletions unit_tests/test_tlslite_integration_tlsasynciodispatchermixin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# Author: Esteban Sanchez (FosanzDev)

import sys

# This test case is skipped because it uses asyncio,
# which is not available in Python 2- asyncio is used
# in the implementation of TLSAsyncioDispatcherMixIn
try:
from tlslite.integration.tlsasynciodispatchermixin \
import TLSAsyncioDispatcherMixIn
import asyncio
except ImportError:
pass

try:
import unittest2 as unittest
except ImportError:
import unittest

try:
from unittest.mock import Mock
except ImportError:
from mock import Mock

PY_VER = sys.version_info


@unittest.skipIf(PY_VER < (3,),
"asyncio is not available in Python 2")
class TestTLSAsyncioDispatcherMixIn(unittest.TestCase):
if PY_VER >= (3,):
class MockProtocol(asyncio.Protocol):
def connection_lost(self, exc):
self.in_write_event()

def in_write_event(self):
pass

def readable(self):
return True

def writable(self):
return True

def setUp(self):
self.protocol = TLSAsyncioDispatcherMixIn()
self.protocol.__class__ = type('TestProtocol',
(self.MockProtocol,
TLSAsyncioDispatcherMixIn), {})
self.protocol.transport = Mock()
self.protocol.tls_connection = Mock()

def test_readable(self):
self.protocol.wants_read_event = Mock(return_value=None)
self.protocol._get_sibling_class = Mock(return_value=
Mock(readable=
Mock(return_value=True)))
self.assertTrue(self.protocol.readable())

def test_writable(self):
self.protocol.wants_write_event = Mock(return_value=None)
self.protocol._get_sibling_class = Mock(return_value=
Mock(writable=
Mock(return_value=True)))
self.assertTrue(self.protocol.writable())

def test_data_received(self):
self.protocol.transport = Mock()
self.protocol.sibling_class.data_received = Mock()
self.protocol.data_received(b'test')
self.protocol.sibling_class.data_received.assert_called_once_with(
self.protocol, b'test'
)

def test_connection_lost(self):
self.protocol.in_write_event = Mock()
self.protocol.connection_lost(None)
self.protocol.in_write_event.assert_called_once()

def test_connection_made(self):
self.protocol.transport = Mock()
self.protocol.sibling_class.connection_made = Mock()
self.protocol.connection_made(self.protocol.transport)
self.protocol.sibling_class.connection_made.assert_called_once_with(
self.protocol.transport
)

def test_out_close_event(self):
self.protocol.out_close_event()
self.protocol.transport.close.assert_called_once()

def test_recv(self):
self.protocol.read_buffer = b"test"
self.assertEqual(self.protocol.recv(), b"test")
self.assertIsNone(self.protocol.read_buffer)

def test_send(self):
write_buffer = b"test"
self.protocol.set_write_op = Mock()
self.assertEqual(self.protocol.send(write_buffer), len(write_buffer))
self.protocol.set_write_op.assert_called_once_with(write_buffer)

def test_close(self):
self.protocol.set_close_op = Mock()
self.protocol.close()
self.protocol.set_close_op.assert_called_once()

0 comments on commit 4d2c6b8

Please sign in to comment.