diff --git a/api/settings_example b/api/settings_example index 9705a9a8..32ce02ea 100644 --- a/api/settings_example +++ b/api/settings_example @@ -1,4 +1,4 @@ api_id=12345 api_hash=0123456789abcdef0123456789abcdef -user_phone=34600000000 +user_phone=+34600000000 session_name=anonymous diff --git a/crypto/auth_key.py b/crypto/auth_key.py index 0d548cba..be784f2d 100755 --- a/crypto/auth_key.py +++ b/crypto/auth_key.py @@ -1,6 +1,7 @@ # This file is based on TLSharp # https://github.com/sochix/TLSharp/blob/master/TLSharp.Core/MTProto/Crypto/AuthKey.cs import utils +from errors import * from utils import BinaryWriter, BinaryReader @@ -11,7 +12,7 @@ class AuthKey: elif data: self.key = data else: - raise AssertionError('Either a gab integer or data bytes array must be provided') + raise InvalidParameterError('Either a gab integer or data bytes array must be provided') with BinaryReader(utils.sha1(self.key)) as reader: self.aux_hash = reader.read_long(signed=False) diff --git a/errors.py b/errors.py new file mode 100644 index 00000000..243c2cac --- /dev/null +++ b/errors.py @@ -0,0 +1,82 @@ +class InvalidParameterError(Exception): + """Occurs when an invalid parameter is given, for example, + when either A or B are required but none is given""" + + +class TypeNotFoundError(Exception): + """Occurs when a type is not found, for example, + when trying to read a TLObject with an invalid constructor code""" + def __init__(self, invalid_constructor_id): + super().__init__(self, 'Could not find a matching Constructor ID for the TLObject ' + 'that was supposed to be read with ID {}. Most likely, a TLObject ' + 'was trying to be read when it should not be read.' + .format(hex(invalid_constructor_id))) + + self.invalid_constructor_id = invalid_constructor_id + + +class InvalidDCError(Exception): + def __init__(self, new_dc): + super().__init__(self, 'Your phone number is registered to #{} DC. ' + 'This should have been handled automatically; ' + 'if it has not, please restart the app.') + + self.new_dc = new_dc + + +class InvalidChecksumError(Exception): + def __init__(self, checksum, valid_checksum): + super().__init__(self, 'Invalid checksum ({} when {} was expected). This packet should be skipped.' + .format(checksum, valid_checksum)) + + self.checksum = checksum + self.valid_checksum = valid_checksum + + +class RPCError(Exception): + def __init__(self, message): + super().__init__(self, message) + self.message = message + + +class BadMessageError(Exception): + """Occurs when handling a bad_message_notification""" + ErrorMessages = { + 16: 'msg_id too low (most likely, client time is wrong it would be worthwhile to ' + 'synchronize it using msg_id notifications and re-send the original message ' + 'with the “correct” msg_id or wrap it in a container with a new msg_id if the ' + 'original message had waited too long on the client to be transmitted).', + + 17: 'msg_id too high (similar to the previous case, the client time has to be ' + 'synchronized, and the message re-sent with the correct msg_id).', + + 18: 'Incorrect two lower order msg_id bits (the server expects client message msg_id ' + 'to be divisible by 4).', + + 19: 'Container msg_id is the same as msg_id of a previously received message ' + '(this must never happen).', + + 20: 'Message too old, and it cannot be verified whether the server has received a ' + 'message with this msg_id or not.', + + 32: 'msg_seqno too low (the server has already received a message with a lower ' + 'msg_id but with either a higher or an equal and odd seqno).', + + 33: 'msg_seqno too high (similarly, there is a message with a higher msg_id but with ' + 'either a lower or an equal and odd seqno).', + + 34: 'An even msg_seqno expected (irrelevant message), but odd received.', + + 35: 'Odd msg_seqno expected (relevant message), but even received.', + + 48: 'Incorrect server salt (in this case, the bad_server_salt response is received with ' + 'the correct salt, and the message is to be re-sent with it).', + + 64: 'Invalid container.' + } + + def __init__(self, code): + super().__init__(self, BadMessageError + .ErrorMessages.get(code,'Unknown error code (this should not happen): {}.'.format(code))) + + self.code = code diff --git a/main.py b/main.py index 1aa43364..2446ba96 100755 --- a/main.py +++ b/main.py @@ -16,6 +16,9 @@ if __name__ == '__main__': client.connect() if not client.is_user_authorized(): - phone_code_hash = client.send_code_request(settings['user_phone']) + phone_code_hash = None + while phone_code_hash is None: + phone_code_hash = client.send_code_request(settings['user_phone']) + code = input('Enter the code you just received: ') client.make_auth(settings['user_phone'], phone_code_hash, code) diff --git a/network/mtproto_sender.py b/network/mtproto_sender.py index 069d975d..cbe0b9f7 100755 --- a/network/mtproto_sender.py +++ b/network/mtproto_sender.py @@ -1,7 +1,8 @@ # This file is based on TLSharp # https://github.com/sochix/TLSharp/blob/master/TLSharp.Core/Network/MtProtoSender.cs import re -import zlib +import gzip +from errors import * from time import sleep import utils @@ -64,27 +65,26 @@ class MtProtoSender: """Sends the given packet bytes with the additional information of the original request""" request.msg_id = self.session.get_new_msg_id() - # First calculate the ciphered bit - with BinaryWriter() as writer: - writer.write_long(self.session.salt, signed=False) - writer.write_long(self.session.id, signed=False) - writer.write_long(request.msg_id) - writer.write_int(self.generate_sequence(request.confirmed)) - writer.write_int(len(packet)) - writer.write(packet) + # First calculate plain_text to encrypt it + with BinaryWriter() as plain_writer: + plain_writer.write_long(self.session.salt, signed=False) + plain_writer.write_long(self.session.id, signed=False) + plain_writer.write_long(request.msg_id) + plain_writer.write_int(self.generate_sequence(request.confirmed)) + plain_writer.write_int(len(packet)) + plain_writer.write(packet) - msg_key = utils.calc_msg_key(writer.get_bytes()) + msg_key = utils.calc_msg_key(plain_writer.get_bytes()) key, iv = utils.calc_key(self.session.auth_key.key, msg_key, True) - cipher_text = AES.encrypt_ige(writer.get_bytes(), key, iv) + cipher_text = AES.encrypt_ige(plain_writer.get_bytes(), key, iv) - # And then finally send the packet - with BinaryWriter() as writer: - writer.write_long(self.session.auth_key.key_id, signed=False) - writer.write(msg_key) - writer.write(cipher_text) - - self.transport.send(writer.get_bytes()) + # And then finally send the encrypted packet + with BinaryWriter() as cipher_writer: + cipher_writer.write_long(self.session.auth_key.key_id, signed=False) + cipher_writer.write(msg_key) + cipher_writer.write(cipher_text) + self.transport.send(cipher_writer.get_bytes()) def decode_msg(self, body): """Decodes an received encrypted message body bytes""" @@ -125,7 +125,7 @@ class MtProtoSender: return self.handle_container(msg_id, sequence, reader, request) if code == 0x7abe77ec: # Ping return self.handle_ping(msg_id, sequence, reader) - if code == 0x347773c5: # pong + if code == 0x347773c5: # Pong return self.handle_pong(msg_id, sequence, reader) if code == 0xae500895: # future_salts return self.handle_future_salts(msg_id, sequence, reader) @@ -174,8 +174,13 @@ class MtProtoSender: if not self.process_msg(inner_msg_id, sequence, reader, request): reader.set_position(begin_position + inner_length) - except: - reader.set_position(begin_position + inner_length) + except Exception as error: + if error is InvalidDCError: + print('Re-raise {}'.format(error)) + raise error + else: + print('Error while handling container {}: {}'.format(type(error), error)) + reader.set_position(begin_position + inner_length) return False @@ -216,42 +221,9 @@ class MtProtoSender: code = reader.read_int(signed=False) request_id = reader.read_long(signed=False) request_sequence = reader.read_int() + error_code = reader.read_int() - - if error_code == 16: - raise RuntimeError("msg_id too low (most likely, client time is wrong it would be worthwhile to " - "synchronize it using msg_id notifications and re-send the original message " - "with the “correct” msg_id or wrap it in a container with a new msg_id if the " - "original message had waited too long on the client to be transmitted)") - if error_code == 17: - raise RuntimeError("msg_id too high (similar to the previous case, the client time has to be " - "synchronized, and the message re-sent with the correct msg_id)") - if error_code == 18: - raise RuntimeError("Incorrect two lower order msg_id bits (the server expects client message msg_id " - "to be divisible by 4)") - if error_code == 19: - raise RuntimeError("Container msg_id is the same as msg_id of a previously received message " - "(this must never happen)") - if error_code == 20: - raise RuntimeError("Message too old, and it cannot be verified whether the server has received a " - "message with this msg_id or not") - if error_code == 32: - raise RuntimeError("msg_seqno too low (the server has already received a message with a lower " - "msg_id but with either a higher or an equal and odd seqno)") - if error_code == 33: - raise RuntimeError("msg_seqno too high (similarly, there is a message with a higher msg_id but with " - "either a lower or an equal and odd seqno)") - if error_code == 34: - raise RuntimeError("An even msg_seqno expected (irrelevant message), but odd received") - if error_code == 35: - raise RuntimeError("Odd msg_seqno expected (relevant message), but even received") - if error_code == 48: - raise RuntimeError("Incorrect server salt (in this case, the bad_server_salt response is received with " - "the correct salt, and the message is to be re-sent with it)") - if error_code == 64: - raise RuntimeError("Invalid container") - - raise NotImplementedError('This should never happen!') + raise BadMessageError(error_code) def hangle_msg_detailed_info(self, msg_id, sequence, reader): return False @@ -262,8 +234,6 @@ class MtProtoSender: if request_id == mtproto_request.msg_id: mtproto_request.confirm_received = True - else: - print('We did not get the ID we expected. Got {}, but it should have been {}'.format(request_id, mtproto_request.msg_id)) inner_code = reader.read_int(signed=False) if inner_code == 0x2144ca19: # RPC Error @@ -277,21 +247,14 @@ class MtProtoSender: elif error_msg.startswith('PHONE_MIGRATE_'): dc_index = int(re.search(r'\d+', error_msg).group(0)) - raise ConnectionError('Your phone number is registered to {} DC. Please update settings. ' - 'See https://github.com/sochix/TLSharp#i-get-an-error-migrate_x ' - 'for details.'.format(dc_index)) + raise InvalidDCError(dc_index) else: - raise ValueError(error_msg) + raise RPCError(error_msg) elif inner_code == 0x3072cfa1: # GZip packed - try: - packed_data = reader.tgread_bytes() - unpacked_data = zlib.decompress(packed_data) - - with BinaryReader(unpacked_data) as compressed_reader: - mtproto_request.on_response(compressed_reader) - except: - pass + unpacked_data = gzip.decompress(reader.tgread_bytes()) + with BinaryReader(unpacked_data) as compressed_reader: + mtproto_request.on_response(compressed_reader) else: reader.seek(-4) @@ -300,7 +263,7 @@ class MtProtoSender: def handle_gzip_packed(self, msg_id, sequence, reader, mtproto_request): code = reader.read_int(signed=False) packed_data = reader.tgread_bytes() - unpacked_data = zlib.decompress(packed_data) + unpacked_data = gzip.decompress(packed_data) with BinaryReader(unpacked_data) as compressed_reader: self.process_msg(msg_id, sequence, compressed_reader, mtproto_request) diff --git a/network/tcp_client.py b/network/tcp_client.py index 9172033d..baf040d3 100755 --- a/network/tcp_client.py +++ b/network/tcp_client.py @@ -19,7 +19,7 @@ class TcpClient: def write(self, data): """Writes (sends) the specified bytes to the connected peer""" - self.socket.send(data) + self.socket.sendall(data) def read(self, buffer_size): """Reads (receives) the specified bytes from the connected peer""" diff --git a/network/tcp_message.py b/network/tcp_message.py index 1dd06898..a28c839e 100755 --- a/network/tcp_message.py +++ b/network/tcp_message.py @@ -2,6 +2,7 @@ # https://github.com/sochix/TLSharp/blob/master/TLSharp.Core/Network/TcpMessage.cs from utils import BinaryWriter, BinaryReader from binascii import crc32 +from errors import * class TcpMessage: @@ -11,7 +12,7 @@ class TcpMessage: :param body: Message body byte array """ if body is None: - raise ValueError('body cannot be None') + raise InvalidParameterError('body cannot be None') self.sequence_number = seq_number self.body = body @@ -40,15 +41,15 @@ class TcpMessage: def decode(body): """Returns a TcpMessage from the given encoded bytes, decoding them previously""" if body is None: - raise ValueError('body cannot be None') + raise InvalidParameterError('body cannot be None') if len(body) < 12: - raise ValueError('Wrong size of input packet') + raise InvalidParameterError('Wrong size of input packet') with BinaryReader(body) as reader: packet_len = int.from_bytes(reader.read(4), byteorder='little') if packet_len < 12: - raise ValueError('Invalid packet length: {}'.format(packet_len)) + raise InvalidParameterError('Invalid packet length in body: {}'.format(packet_len)) seq = reader.read_int() packet = reader.read(packet_len - 12) @@ -56,6 +57,6 @@ class TcpMessage: valid_checksum = crc32(body[:packet_len - 4]) if checksum != valid_checksum: - raise ValueError('Invalid checksum, skip') + raise InvalidChecksumError(checksum, valid_checksum) return TcpMessage(seq, packet) diff --git a/network/tcp_transport.py b/network/tcp_transport.py index b605d833..12379896 100755 --- a/network/tcp_transport.py +++ b/network/tcp_transport.py @@ -2,6 +2,7 @@ # https://github.com/sochix/TLSharp/blob/master/TLSharp.Core/Network/TcpTransport.cs from network import TcpMessage, TcpClient from binascii import crc32 +from errors import * class TcpTransport: @@ -42,7 +43,7 @@ class TcpTransport: valid_checksum = crc32(rv) if checksum != valid_checksum: - raise ValueError('Invalid checksum, skip') + raise InvalidChecksumError(checksum, valid_checksum) # If we passed the tests, we can then return a valid TcpMessage return TcpMessage(seq, body) diff --git a/scheme.tl b/scheme.tl index db060f49..4d5a4911 100755 --- a/scheme.tl +++ b/scheme.tl @@ -1,9 +1,5 @@ // Core types (no need to gen) -// We handle some types in a special way -//boolFalse#bc799737 = Bool; -//boolTrue#997275b5 = Bool; -//true#3fedd339 = True; //vector#1cb5c415 {t:Type} # [ t ] = Vector t; /////////////////////////////// @@ -123,6 +119,13 @@ contest.saveDeveloperInfo#9a5f6e95 vk_id:int name:string phone_number:string age ---types--- +//boolFalse#bc799737 = Bool; +//boolTrue#997275b5 = Bool; + +//true#3fedd339 = True; + +//vector#1cb5c415 {t:Type} # [ t ] = Vector t; + error#c4b9f9bb code:int text:string = Error; null#56730bcc = Null; diff --git a/tl/telegram_client.py b/tl/telegram_client.py index 91f425b1..a37c46c3 100644 --- a/tl/telegram_client.py +++ b/tl/telegram_client.py @@ -6,13 +6,13 @@ import platform import utils import network.authenticator from network import MtProtoSender, TcpTransport +from errors import * from tl import Session from tl.types import InputPeerUser from tl.functions import InvokeWithLayerRequest, InitConnectionRequest from tl.functions.help import GetConfigRequest from tl.functions.auth import CheckPhoneRequest, SendCodeRequest, SignInRequest -from tl.functions.contacts import GetContactsRequest from tl.functions.messages import SendMessageRequest @@ -84,6 +84,7 @@ class TelegramClient: return request.result.phone_registered def send_code_request(self, phone_number): + """May return None if an error occured!""" request = SendCodeRequest(phone_number, self.api_id, self.api_hash) completed = False while not completed: @@ -91,14 +92,14 @@ class TelegramClient: self.sender.send(request) self.sender.receive(request) completed = True - except ConnectionError as error: - if str(error).startswith('Your phone number is registered to'): - dc = int(re.search(r'\d+', str(error)).group(0)) - self.reconnect_to_dc(dc) - else: - raise error + except InvalidDCError as error: + self.reconnect_to_dc(error.new_dc) + + if request.result is None: + return None + else: + return request.result.phone_code_hash - return request.result.phone_code_hash def make_auth(self, phone_number, phone_code_hash, code): request = SignInRequest(phone_number, phone_code_hash, code) @@ -111,12 +112,6 @@ class TelegramClient: return self.session.user - def import_contacts(self, phone_code_hash): - request = GetContactsRequest(phone_code_hash) - self.sender.send(request) - self.sender.receive(request) - return request.result.contacts, request.result.users - def send_message(self, user, message): peer = InputPeerUser(user.id, user.access_hash) request = SendMessageRequest(peer, message, utils.generate_random_long()) diff --git a/tl_generator.py b/tl_generator.py index cc74725f..5e8e2a97 100755 --- a/tl_generator.py +++ b/tl_generator.py @@ -101,9 +101,10 @@ def generate_tlobjects(scheme_file): builder.writeln('"""') builder.writeln('super().__init__()') - # Functions have a result object + # Functions have a result object and are confirmed by default if tlobject.is_function: builder.writeln('self.result = None') + builder.writeln('self.confirmed = True # Confirmed by default') # Set the arguments if args: @@ -215,9 +216,13 @@ def write_onsend_code(builder, arg, args, name=None): if name is None: name = 'self.{}'.format(arg.name) - # The argument may be a flag, only write if it's not None! + # The argument may be a flag, only write if it's not None AND if it's not a True type + # True types are not actually sent, but instead only used to determine the flags if arg.is_flag: - builder.writeln('if {} is not None:'.format(name)) + if arg.type == 'true': + return # Exit, since True type is never written + else: + builder.writeln('if {} is not None:'.format(name)) if arg.is_vector: builder.writeln("writer.write_int(0x1cb5c415, signed=False) # Vector's constructor ID") @@ -262,7 +267,7 @@ def write_onsend_code(builder, arg, args, name=None): builder.writeln('writer.tgwrite_bool({})'.format(name)) elif 'true' == arg.type: # Awkwardly enough, Telegram has both bool and "true", used in flags - builder.writeln('writer.write_int(0x3fedd339) # true') + pass # These are actually NOT written! Only used for flags elif 'bytes' == arg.type: builder.writeln('writer.write({})'.format(name)) @@ -297,8 +302,12 @@ def write_onresponse_code(builder, arg, args, name=None): name = 'self.{}'.format(arg.name) # The argument may be a flag, only write that flag was given! + was_flag = False if arg.is_flag: + was_flag = True builder.writeln('if (flags & (1 << {})) != 0:'.format(arg.flag_index)) + # Temporary disable .is_flag not to enter this if again when calling the method recursively + arg.is_flag = False if arg.is_vector: builder.writeln("reader.read_int() # Vector's constructor ID") @@ -338,21 +347,23 @@ def write_onresponse_code(builder, arg, args, name=None): builder.writeln('{} = reader.tgread_bool()'.format(name)) elif 'true' == arg.type: # Awkwardly enough, Telegram has both bool and "true", used in flags - builder.writeln('{} = reader.read_int() == 0x3fedd339 # true'.format(name)) + builder.writeln('{} = True # Arbitrary not-None value, no need to read since it is a flag'.format(name)) elif 'bytes' == arg.type: builder.writeln('{} = reader.read()'.format(name)) else: # Else it may be a custom type - builder.writeln('{} = reader.tgread_object(reader)'.format(name)) + builder.writeln('{} = reader.tgread_object()'.format(name)) # End vector and flag blocks if required (if we opened them before) if arg.is_vector: builder.end_block() - if arg.is_flag: + if was_flag: builder.end_block() + # Restore .is_flag + arg.is_flag = True if __name__ == '__main__': if tlobjects_exist(): diff --git a/utils/binary_reader.py b/utils/binary_reader.py index 4441b180..ea4f57d5 100755 --- a/utils/binary_reader.py +++ b/utils/binary_reader.py @@ -1,6 +1,7 @@ from io import BytesIO, BufferedReader from tl import tlobjects from struct import unpack +from errors import * import inspect import os @@ -16,7 +17,7 @@ class BinaryReader: elif stream: self.stream = stream else: - raise ValueError("Either bytes or a stream must be provided") + raise InvalidParameterError("Either bytes or a stream must be provided") self.reader = BufferedReader(self.stream) @@ -96,11 +97,10 @@ class BinaryReader: def tgread_object(self): """Reads a Telegram object""" - constructor_id = self.read_int() + constructor_id = self.read_int(signed=False) clazz = tlobjects.get(constructor_id, None) if clazz is None: - raise ImportError('Could not find a matching ID for the TLObject that was supposed to be read. ' - 'Found ID: {}'.format(hex(constructor_id))) + raise TypeNotFoundError(constructor_id) # Now we need to determine the number of parameters of the class, so we can # instantiate it with all of them set to None, and still, no need to write