2017-08-28 22:44:02 +03:00
|
|
|
import os
|
2017-09-27 22:23:59 +03:00
|
|
|
import struct
|
2017-08-28 22:23:31 +03:00
|
|
|
from datetime import timedelta
|
|
|
|
from zlib import crc32
|
2017-09-04 12:24:10 +03:00
|
|
|
from enum import Enum
|
2017-08-28 22:23:31 +03:00
|
|
|
|
2017-09-21 13:37:05 +03:00
|
|
|
import errno
|
|
|
|
|
2017-08-28 22:44:02 +03:00
|
|
|
from ..crypto import AESModeCTR
|
2017-09-27 22:23:59 +03:00
|
|
|
from ..extensions import TcpClient
|
2017-08-28 22:23:31 +03:00
|
|
|
from ..errors import InvalidChecksumError
|
|
|
|
|
|
|
|
|
2017-09-04 12:24:10 +03:00
|
|
|
class ConnectionMode(Enum):
|
|
|
|
"""Represents which mode should be used to stabilise a connection.
|
|
|
|
|
|
|
|
TCP_FULL: Default Telegram mode. Sends 12 additional bytes and
|
|
|
|
needs to calculate the CRC value of the packet itself.
|
|
|
|
|
|
|
|
TCP_INTERMEDIATE: Intermediate mode between TCP_FULL and TCP_ABRIDGED.
|
|
|
|
Always sends 4 extra bytes for the packet length.
|
|
|
|
|
|
|
|
TCP_ABRIDGED: This is the mode with the lowest overhead, as it will
|
|
|
|
only require 1 byte if the packet length is less than
|
|
|
|
508 bytes (127 << 2, which is very common).
|
|
|
|
|
|
|
|
TCP_OBFUSCATED: Encodes the packet just like TCP_ABRIDGED, but encrypts
|
|
|
|
every message with a randomly generated key using the
|
|
|
|
AES-CTR mode so the packets are harder to discern.
|
|
|
|
"""
|
|
|
|
TCP_FULL = 1
|
|
|
|
TCP_INTERMEDIATE = 2
|
|
|
|
TCP_ABRIDGED = 3
|
|
|
|
TCP_OBFUSCATED = 4
|
|
|
|
|
|
|
|
|
2017-08-28 22:23:31 +03:00
|
|
|
class Connection:
|
2017-08-28 22:44:02 +03:00
|
|
|
"""Represents an abstract connection (TCP, TCP abridged...).
|
2017-09-04 12:24:10 +03:00
|
|
|
'mode' must be any of the ConnectionMode enumeration.
|
2017-08-28 22:44:02 +03:00
|
|
|
|
|
|
|
Note that '.send()' and '.recv()' refer to messages, which
|
|
|
|
will be packed accordingly, whereas '.write()' and '.read()'
|
|
|
|
work on plain bytes, with no further additions.
|
|
|
|
"""
|
|
|
|
|
2017-09-04 12:24:10 +03:00
|
|
|
def __init__(self, ip, port, mode=ConnectionMode.TCP_FULL,
|
2017-08-28 22:23:31 +03:00
|
|
|
proxy=None, timeout=timedelta(seconds=5)):
|
|
|
|
self.ip = ip
|
|
|
|
self.port = port
|
|
|
|
self._mode = mode
|
2017-08-28 22:44:02 +03:00
|
|
|
|
2017-08-28 22:23:31 +03:00
|
|
|
self._send_counter = 0
|
2017-08-28 22:44:02 +03:00
|
|
|
self._aes_encrypt, self._aes_decrypt = None, None
|
2017-08-28 22:23:31 +03:00
|
|
|
|
2017-08-28 22:44:02 +03:00
|
|
|
# TODO Rename "TcpClient" as some sort of generic socket?
|
2017-09-02 20:33:42 +03:00
|
|
|
self.conn = TcpClient(proxy=proxy, timeout=timeout)
|
2017-08-28 22:23:31 +03:00
|
|
|
|
2017-08-28 22:44:02 +03:00
|
|
|
# Sending messages
|
2017-09-04 12:24:10 +03:00
|
|
|
if mode == ConnectionMode.TCP_FULL:
|
2017-08-28 22:23:31 +03:00
|
|
|
setattr(self, 'send', self._send_tcp_full)
|
|
|
|
setattr(self, 'recv', self._recv_tcp_full)
|
|
|
|
|
2017-09-04 12:24:10 +03:00
|
|
|
elif mode == ConnectionMode.TCP_INTERMEDIATE:
|
2017-08-29 12:39:44 +03:00
|
|
|
setattr(self, 'send', self._send_intermediate)
|
|
|
|
setattr(self, 'recv', self._recv_intermediate)
|
|
|
|
|
2017-09-04 18:10:04 +03:00
|
|
|
elif mode in (ConnectionMode.TCP_ABRIDGED,
|
|
|
|
ConnectionMode.TCP_OBFUSCATED):
|
2017-08-28 22:23:31 +03:00
|
|
|
setattr(self, 'send', self._send_abridged)
|
|
|
|
setattr(self, 'recv', self._recv_abridged)
|
|
|
|
|
2017-08-28 22:44:02 +03:00
|
|
|
# Writing and reading from the socket
|
2017-09-04 12:24:10 +03:00
|
|
|
if mode == ConnectionMode.TCP_OBFUSCATED:
|
2017-08-28 22:44:02 +03:00
|
|
|
setattr(self, 'write', self._write_obfuscated)
|
|
|
|
setattr(self, 'read', self._read_obfuscated)
|
|
|
|
else:
|
|
|
|
setattr(self, 'write', self._write_plain)
|
|
|
|
setattr(self, 'read', self._read_plain)
|
|
|
|
|
2017-08-28 22:23:31 +03:00
|
|
|
def connect(self):
|
2017-09-21 13:37:05 +03:00
|
|
|
try:
|
|
|
|
self.conn.connect(self.ip, self.port)
|
|
|
|
except OSError as e:
|
|
|
|
if e.errno == errno.EISCONN:
|
|
|
|
return # Already connected, no need to re-set everything up
|
|
|
|
else:
|
|
|
|
raise
|
2017-08-28 22:23:31 +03:00
|
|
|
|
2017-09-21 13:37:05 +03:00
|
|
|
self._send_counter = 0
|
2017-09-04 12:24:10 +03:00
|
|
|
if self._mode == ConnectionMode.TCP_ABRIDGED:
|
2017-08-29 12:39:44 +03:00
|
|
|
self.conn.write(b'\xef')
|
2017-09-04 12:24:10 +03:00
|
|
|
elif self._mode == ConnectionMode.TCP_INTERMEDIATE:
|
2017-08-29 12:39:44 +03:00
|
|
|
self.conn.write(b'\xee\xee\xee\xee')
|
2017-09-04 12:24:10 +03:00
|
|
|
elif self._mode == ConnectionMode.TCP_OBFUSCATED:
|
2017-08-28 22:44:02 +03:00
|
|
|
self._setup_obfuscation()
|
|
|
|
|
2017-09-21 14:43:33 +03:00
|
|
|
def get_timeout(self):
|
|
|
|
return self.conn.timeout
|
|
|
|
|
2017-08-28 22:44:02 +03:00
|
|
|
def _setup_obfuscation(self):
|
|
|
|
# Obfuscated messages secrets cannot start with any of these
|
|
|
|
keywords = (b'PVrG', b'GET ', b'POST', b'\xee' * 4)
|
|
|
|
while True:
|
|
|
|
random = os.urandom(64)
|
|
|
|
if (random[0] != b'\xef' and
|
2017-09-04 18:10:04 +03:00
|
|
|
random[:4] not in keywords and
|
|
|
|
random[4:4] != b'\0\0\0\0'):
|
2017-08-28 22:44:02 +03:00
|
|
|
# Invalid random generated
|
|
|
|
break
|
|
|
|
|
|
|
|
random = list(random)
|
|
|
|
random[56] = random[57] = random[58] = random[59] = 0xef
|
|
|
|
random_reversed = random[55:7:-1] # Reversed (8, len=48)
|
|
|
|
|
|
|
|
# encryption has "continuous buffer" enabled
|
|
|
|
encrypt_key = bytes(random[8:40])
|
|
|
|
encrypt_iv = bytes(random[40:56])
|
|
|
|
decrypt_key = bytes(random_reversed[:32])
|
|
|
|
decrypt_iv = bytes(random_reversed[32:48])
|
|
|
|
|
|
|
|
self._aes_encrypt = AESModeCTR(encrypt_key, encrypt_iv)
|
|
|
|
self._aes_decrypt = AESModeCTR(decrypt_key, decrypt_iv)
|
|
|
|
|
|
|
|
random[56:64] = self._aes_encrypt.encrypt(bytes(random))[56:64]
|
|
|
|
self.conn.write(bytes(random))
|
2017-08-28 22:23:31 +03:00
|
|
|
|
|
|
|
def is_connected(self):
|
|
|
|
return self.conn.connected
|
|
|
|
|
|
|
|
def close(self):
|
|
|
|
self.conn.close()
|
|
|
|
|
2017-08-28 22:44:02 +03:00
|
|
|
# region Receive message implementations
|
2017-08-28 22:23:31 +03:00
|
|
|
|
2017-09-02 20:33:42 +03:00
|
|
|
def recv(self):
|
2017-08-28 22:23:31 +03:00
|
|
|
"""Receives and unpacks a message"""
|
|
|
|
# Default implementation is just an error
|
2017-09-04 12:24:10 +03:00
|
|
|
raise ValueError('Invalid connection mode specified: ' + str(self._mode))
|
2017-08-28 22:23:31 +03:00
|
|
|
|
2017-09-02 20:33:42 +03:00
|
|
|
def _recv_tcp_full(self):
|
2017-08-28 22:44:02 +03:00
|
|
|
packet_length_bytes = self.read(4)
|
2017-08-28 22:23:31 +03:00
|
|
|
packet_length = int.from_bytes(packet_length_bytes, 'little')
|
|
|
|
|
2017-08-28 22:44:02 +03:00
|
|
|
seq_bytes = self.read(4)
|
2017-08-28 22:23:31 +03:00
|
|
|
seq = int.from_bytes(seq_bytes, 'little')
|
|
|
|
|
2017-08-28 22:44:02 +03:00
|
|
|
body = self.read(packet_length - 12)
|
|
|
|
checksum = int.from_bytes(self.read(4), 'little')
|
2017-08-28 22:23:31 +03:00
|
|
|
|
|
|
|
valid_checksum = crc32(packet_length_bytes + seq_bytes + body)
|
|
|
|
if checksum != valid_checksum:
|
|
|
|
raise InvalidChecksumError(checksum, valid_checksum)
|
|
|
|
|
|
|
|
return body
|
|
|
|
|
2017-09-02 20:33:42 +03:00
|
|
|
def _recv_intermediate(self):
|
2017-08-29 12:39:44 +03:00
|
|
|
return self.read(int.from_bytes(self.read(4), 'little'))
|
|
|
|
|
2017-09-02 20:33:42 +03:00
|
|
|
def _recv_abridged(self):
|
2017-08-28 22:44:02 +03:00
|
|
|
length = int.from_bytes(self.read(1), 'little')
|
2017-08-28 22:23:31 +03:00
|
|
|
if length >= 127:
|
2017-08-28 22:44:02 +03:00
|
|
|
length = int.from_bytes(self.read(3) + b'\0', 'little')
|
2017-08-28 22:23:31 +03:00
|
|
|
|
2017-08-28 22:44:02 +03:00
|
|
|
return self.read(length << 2)
|
2017-08-28 22:23:31 +03:00
|
|
|
|
|
|
|
# endregion
|
|
|
|
|
2017-08-28 22:44:02 +03:00
|
|
|
# region Send message implementations
|
2017-08-28 22:23:31 +03:00
|
|
|
|
|
|
|
def send(self, message):
|
|
|
|
"""Encapsulates and sends the given message"""
|
|
|
|
# Default implementation is just an error
|
2017-09-04 12:24:10 +03:00
|
|
|
raise ValueError('Invalid connection mode specified: ' + str(self._mode))
|
2017-08-28 22:23:31 +03:00
|
|
|
|
|
|
|
def _send_tcp_full(self, message):
|
|
|
|
# https://core.telegram.org/mtproto#tcp-transport
|
|
|
|
# total length, sequence number, packet and checksum (CRC32)
|
2017-08-29 21:25:49 +03:00
|
|
|
length = len(message) + 12
|
2017-09-27 22:23:59 +03:00
|
|
|
data = struct.pack('<ii', length, self._send_counter) + message
|
|
|
|
crc = struct.pack('<I', crc32(data))
|
|
|
|
self._send_counter += 1
|
|
|
|
self.write(data + crc)
|
2017-08-28 22:23:31 +03:00
|
|
|
|
2017-08-29 12:39:44 +03:00
|
|
|
def _send_intermediate(self, message):
|
2017-09-27 22:23:59 +03:00
|
|
|
self.write(struct.pack('<i', len(message)) + message)
|
2017-08-29 12:39:44 +03:00
|
|
|
|
2017-08-28 22:23:31 +03:00
|
|
|
def _send_abridged(self, message):
|
2017-09-27 22:23:59 +03:00
|
|
|
length = len(message) >> 2
|
|
|
|
if length < 127:
|
|
|
|
length = struct.pack('B', length)
|
|
|
|
else:
|
|
|
|
length = b'\x7f' + int.to_bytes(length, 3, 'little')
|
|
|
|
|
|
|
|
self.write(length + message)
|
2017-08-28 22:44:02 +03:00
|
|
|
|
|
|
|
# endregion
|
|
|
|
|
|
|
|
# region Read implementations
|
|
|
|
|
|
|
|
def read(self, length):
|
2017-09-04 12:24:10 +03:00
|
|
|
raise ValueError('Invalid connection mode specified: ' + str(self._mode))
|
2017-08-28 22:44:02 +03:00
|
|
|
|
|
|
|
def _read_plain(self, length):
|
2017-09-02 20:33:42 +03:00
|
|
|
return self.conn.read(length)
|
2017-08-28 22:44:02 +03:00
|
|
|
|
|
|
|
def _read_obfuscated(self, length):
|
|
|
|
return self._aes_decrypt.encrypt(
|
2017-09-02 20:33:42 +03:00
|
|
|
self.conn.read(length)
|
2017-08-28 22:44:02 +03:00
|
|
|
)
|
|
|
|
|
|
|
|
# endregion
|
|
|
|
|
|
|
|
# region Write implementations
|
|
|
|
|
|
|
|
def write(self, data):
|
2017-09-04 12:24:10 +03:00
|
|
|
raise ValueError('Invalid connection mode specified: ' + str(self._mode))
|
2017-08-28 22:44:02 +03:00
|
|
|
|
|
|
|
def _write_plain(self, data):
|
|
|
|
self.conn.write(data)
|
|
|
|
|
|
|
|
def _write_obfuscated(self, data):
|
|
|
|
self.conn.write(self._aes_encrypt.encrypt(data))
|
2017-08-28 22:23:31 +03:00
|
|
|
|
|
|
|
# endregion
|