""" This module contains the class used to communicate with Telegram's servers encrypting every packet, and relies on a valid AuthKey in the used Session. """ import gzip import logging import struct from .. import helpers as utils from ..crypto import AES from ..errors import ( BadMessageError, InvalidChecksumError, BrokenAuthKeyError, rpc_message_to_error ) from ..extensions import BinaryReader from ..tl import TLMessage, MessageContainer, GzipPacked from ..tl.all_tlobjects import tlobjects from ..tl.types import ( MsgsAck, Pong, BadServerSalt, BadMsgNotification, MsgNewDetailedInfo, NewSessionCreated, MsgDetailedInfo ) from ..tl.functions.auth import LogOutRequest __log__ = logging.getLogger(__name__) class MtProtoSender: """MTProto Mobile Protocol sender (https://core.telegram.org/mtproto/description). Note that this class is not thread-safe, and calling send/receive from two or more threads at the same time is undefined behaviour. Rationale: a new connection should be spawned to send/receive requests in parallel, so thread-safety (hence locking) isn't needed. """ def __init__(self, session, connection): """ Initializes a new MTProto sender. :param session: the Session to be used with this sender. Must contain the IP and port of the server, salt, ID, and AuthKey, :param connection: the Connection to be used. """ self.session = session self.connection = connection # Message IDs that need confirmation self._need_confirmation = set() # Requests (as msg_id: Message) sent waiting to be received self._pending_receive = {} def connect(self): """Connects to the server.""" self.connection.connect(self.session.server_address, self.session.port) def is_connected(self): """ Determines whether the sender is connected or not. :return: true if the sender is connected. """ return self.connection.is_connected() def disconnect(self): """Disconnects from the server.""" self.connection.close() self._need_confirmation.clear() self._clear_all_pending() def clone(self): """Creates a copy of this MtProtoSender as a new connection.""" return MtProtoSender(self.session, self.connection.clone()) # region Send and receive def send(self, *requests): """ Sends the specified TLObject(s) (which must be requests), and acknowledging any message which needed confirmation. :param requests: the requests to be sent. """ # Finally send our packed request(s) messages = [TLMessage(self.session, r) for r in requests] self._pending_receive.update({m.msg_id: m for m in messages}) # Pack everything in the same container if we need to send AckRequests if self._need_confirmation: messages.append( TLMessage(self.session, MsgsAck(list(self._need_confirmation))) ) self._need_confirmation.clear() if len(messages) == 1: message = messages[0] else: message = TLMessage(self.session, MessageContainer(messages)) # On bad_msg_salt errors, Telegram will reply with the ID of # the container and not the requests it contains, so in case # this happens we need to know to which container they belong. for m in messages: m.container_msg_id = message.msg_id self._send_message(message) def _send_acknowledge(self, msg_id): """Sends a message acknowledge for the given msg_id.""" self._send_message(TLMessage(self.session, MsgsAck([msg_id]))) def receive(self, update_state): """ Receives a single message from the connected endpoint. This method returns nothing, and will only affect other parts of the MtProtoSender such as the updates callback being fired or a pending request being confirmed. Any unhandled object (likely updates) will be passed to update_state.process(TLObject). :param update_state: the UpdateState that will process all the received Update and Updates objects. """ try: body = self.connection.recv() except (BufferError, InvalidChecksumError): # TODO BufferError, we should spot the cause... # "No more bytes left"; something wrong happened, clear # everything to be on the safe side, or: # # "This packet should be skipped"; since this may have # been a result for a request, invalidate every request # and just re-invoke them to avoid problems __log__.exception('Error while receiving server response. ' '%d pending request(s) will be ignored', len(self._pending_receive)) self._clear_all_pending() return message, remote_msg_id, remote_seq = self._decode_msg(body) with BinaryReader(message) as reader: self._process_msg(remote_msg_id, remote_seq, reader, update_state) # endregion # region Low level processing def _send_message(self, message): """ Sends the given encrypted through the network. :param message: the TLMessage to be sent. """ plain_text = \ struct.pack('