From 9402b4a26da22f0646c004dbbd941f9377abf06e Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 29 Sep 2018 10:58:45 +0200 Subject: [PATCH] Create a new layer to lift encryption off the MTProtoSender --- telethon/client/telegrambaseclient.py | 6 +- telethon/network/connection/connection.py | 6 ++ telethon/network/mtprotolayer.py | 104 ++++++++++++++++++++++ telethon/network/mtprotosender.py | 22 ++--- telethon/network/mtprotostate.py | 50 ++++++----- telethon/tl/core/gzippacked.py | 6 +- telethon/tl/core/messagecontainer.py | 7 +- telethon/tl/core/tlmessage.py | 85 +++--------------- 8 files changed, 166 insertions(+), 120 deletions(-) create mode 100644 telethon/network/mtprotolayer.py diff --git a/telethon/client/telegrambaseclient.py b/telethon/client/telegrambaseclient.py index aad04eaa..5b82b625 100644 --- a/telethon/client/telegrambaseclient.py +++ b/telethon/client/telegrambaseclient.py @@ -224,10 +224,9 @@ class TelegramBaseClient(abc.ABC): ) ) - state = MTProtoState(self.session.auth_key) self._connection = connection self._sender = MTProtoSender( - state, self._loop, + self.session.auth_key, self._loop, retries=self._connection_retries, auto_reconnect=self._auto_reconnect, update_callback=self._handle_update, @@ -413,12 +412,11 @@ class TelegramBaseClient(abc.ABC): # Thanks badoualy/kotlogram on /telegram/api/DefaultTelegramClient.kt # for clearly showing how to export the authorization dc = await self._get_dc(dc_id) - state = MTProtoState(None) # Can't reuse self._sender._connection as it has its own seqno. # # If one were to do that, Telegram would reset the connection # with no further clues. - sender = MTProtoSender(state, self._loop) + sender = MTProtoSender(None, self._loop) await sender.connect(self._connection( dc.ip_address, dc.port, loop=self._loop)) __log__.info('Exporting authorization for data center %s', dc) diff --git a/telethon/network/connection/connection.py b/telethon/network/connection/connection.py index 75cd5b37..be609a57 100644 --- a/telethon/network/connection/connection.py +++ b/telethon/network/connection/connection.py @@ -108,3 +108,9 @@ class Connection(abc.ABC): the way it should be read from `self._reader`. """ raise NotImplementedError + + def __str__(self): + return '{}:{}/{}'.format( + self._ip, self._port, + self.__class__.__name__.replace('Connection', '') + ) diff --git a/telethon/network/mtprotolayer.py b/telethon/network/mtprotolayer.py new file mode 100644 index 00000000..754df8b5 --- /dev/null +++ b/telethon/network/mtprotolayer.py @@ -0,0 +1,104 @@ +import io +import struct + +from .mtprotostate import MTProtoState +from ..tl.core.messagecontainer import MessageContainer + + +class MTProtoLayer: + """ + This class is the message encryption layer between the methods defined + in the schema and the response objects. It also holds the necessary state + necessary for this encryption to happen. + + The `connection` parameter is through which these messages will be sent + and received. + + The `auth_key` must be a valid authorization key which will be used to + encrypt these messages. This class is not responsible for generating them. + """ + def __init__(self, connection, auth_key): + self._connection = connection + self._state = MTProtoState(auth_key) + + def connect(self): + """ + Wrapper for ``self._connection.connect()``. + """ + return self._connection.connect() + + def disconnect(self): + """ + Wrapper for ``self._connection.disconnect()``. + """ + self._connection.disconnect() + + async def send(self, data_list): + """ + A list of serialized RPC queries as bytes must be given to be sent. + Nested lists imply an order is required for the messages in them. + Message containers will be used if there is more than one item. + + Returns ``(container_id, msg_ids)``. + """ + data, container_id, msg_ids = self._pack_data_list(data_list) + await self._connection.send(self._state.encrypt_message_data(data)) + return container_id, msg_ids + + async def recv(self): + """ + Reads a single message from the network, decrypts it and returns it. + """ + body = await self._connection.recv() + return self._state.decrypt_message_data(body) + + def _pack_data_list(self, data_list): + """ + A list of serialized RPC queries as bytes must be given to be packed. + Nested lists imply an order is required for the messages in them. + + Returns ``(data, container_id, msg_ids)``. + """ + # TODO write_data_as_message raises on invalid messages, handle it + # TODO This method could be an iterator yielding messages while small + # respecting the ``MessageContainer.MAXIMUM_SIZE`` limit. + # + # Note that the simplest case is writing a single query data into + # a message, and returning the message data and ID. For efficiency + # purposes this method supports more than one message and automatically + # uses containers if deemed necessary. + # + # Technically the message and message container classes could be used + # to store and serialize the data. However, to keep the context local + # and relevant to the only place where such feature is actually used, + # this is not done. + msg_ids = [] + buffer = io.BytesIO() + for data in data_list: + if not isinstance(data, list): + msg_ids.append(self._state.write_data_as_message(buffer, data)) + else: + last_id = None + for d in data: + last_id = self._state.write_data_as_message( + buffer, d, after_id=last_id) + msg_ids.append(last_id) + + if len(msg_ids) == 1: + container_id = None + else: + # Inlined code to pack several messages into a container + # + # TODO This part and encrypting data prepend a few bytes but + # force a potentially large payload to be appended, which + # may be expensive. Can we do better? + data = struct.pack( + ' 512: + # TODO Only content-related requests should be gzipped + if len(data) > 512: gzipped = bytes(GzipPacked(data)) return gzipped if len(gzipped) < len(data) else data else: diff --git a/telethon/tl/core/messagecontainer.py b/telethon/tl/core/messagecontainer.py index de304424..800a31f0 100644 --- a/telethon/tl/core/messagecontainer.py +++ b/telethon/tl/core/messagecontainer.py @@ -27,11 +27,6 @@ class MessageContainer(TLObject): ], } - def __bytes__(self): - return struct.pack( - '