diff --git a/telethon/client/telegrambaseclient.py b/telethon/client/telegrambaseclient.py index b82d4227..b7275ee0 100644 --- a/telethon/client/telegrambaseclient.py +++ b/telethon/client/telegrambaseclient.py @@ -9,7 +9,7 @@ from datetime import datetime, timezone from .. import version, __name__ as __base_name__ from ..crypto import rsa from ..extensions import markdown -from ..network import MTProtoSender, ConnectionTcpFull, ConnectionTcpMTProxy +from ..network import MTProtoSender, ConnectionTcpFull, TcpMTProxy from ..sessions import Session, SQLiteSession, MemorySession from ..tl import TLObject, functions, types from ..tl.alltlobjects import LAYER @@ -64,7 +64,7 @@ class TelegramBaseClient(abc.ABC): proxy (`tuple` | `list` | `dict`, optional): An iterable consisting of the proxy info. If `connection` is - `ConnectionTcpMTProxy`, then it should contain MTProxy credentials: + one of `MTProxy`, then it should contain MTProxy credentials: ``('hostname', port, 'secret')``. Otherwise, it's meant to store function parameters for PySocks, like ``(type, 'hostname', port)``. See https://github.com/Anorov/PySocks#usage-1 for more. @@ -249,8 +249,8 @@ class TelegramBaseClient(abc.ABC): assert isinstance(connection, type) self._connection = connection - init_proxy = None if connection is not ConnectionTcpMTProxy else \ - types.InputClientProxy(*ConnectionTcpMTProxy.address_info(proxy)) + init_proxy = None if not issubclass(connection, TcpMTProxy) else \ + types.InputClientProxy(*connection.address_info(proxy)) # Used on connection. Capture the variables in a lambda since # exporting clients need to create this InvokeWithLayerRequest. diff --git a/telethon/network/__init__.py b/telethon/network/__init__.py index d8f79d8b..e23070e3 100644 --- a/telethon/network/__init__.py +++ b/telethon/network/__init__.py @@ -7,5 +7,7 @@ from .authenticator import do_authentication from .mtprotosender import MTProtoSender from .connection import ( ConnectionTcpFull, ConnectionTcpIntermediate, ConnectionTcpAbridged, - ConnectionTcpObfuscated, ConnectionTcpMTProxy, ConnectionHttp + ConnectionTcpObfuscated, ConnectionTcpMTProxyAbridged, + ConnectionTcpMTProxyIntermediate, + ConnectionTcpMTProxyRandomizedIntermediate, ConnectionHttp, TcpMTProxy ) diff --git a/telethon/network/connection/__init__.py b/telethon/network/connection/__init__.py index c890da1b..2f3fe15d 100644 --- a/telethon/network/connection/__init__.py +++ b/telethon/network/connection/__init__.py @@ -2,5 +2,10 @@ from .tcpfull import ConnectionTcpFull from .tcpintermediate import ConnectionTcpIntermediate from .tcpabridged import ConnectionTcpAbridged from .tcpobfuscated import ConnectionTcpObfuscated -from .tcpmtproxy import ConnectionTcpMTProxy +from .tcpmtproxy import ( + TcpMTProxy, + ConnectionTcpMTProxyAbridged, + ConnectionTcpMTProxyIntermediate, + ConnectionTcpMTProxyRandomizedIntermediate +) from .http import ConnectionHttp diff --git a/telethon/network/connection/connection.py b/telethon/network/connection/connection.py index ad857d60..5263c5ff 100644 --- a/telethon/network/connection/connection.py +++ b/telethon/network/connection/connection.py @@ -18,6 +18,10 @@ class Connection(abc.ABC): ``ConnectionError``, which will raise when attempting to send if the client is disconnected (includes remote disconnections). """ + # this static attribute should be redefined by `Connection` subclasses and + # should be one of `PacketCodec` implementations + packet_codec = None + def __init__(self, ip, port, dc_id, *, loop, loggers, proxy=None): self._ip = ip self._port = port @@ -30,6 +34,8 @@ class Connection(abc.ABC): self._connected = False self._send_task = None self._recv_task = None + self._codec = None + self._obfuscation = None # TcpObfuscated and MTProxy self._send_queue = asyncio.Queue(1) self._recv_queue = asyncio.Queue(1) @@ -76,6 +82,7 @@ class Connection(abc.ABC): await asyncio.open_connection(sock=s, loop=self._loop) self._connected = True + self._codec = self.packet_codec(self) self._init_conn() await self._writer.drain() @@ -182,27 +189,71 @@ class Connection(abc.ABC): data to Telegram to indicate which connection mode will be used. """ + if self._codec.tag: + self._writer.write(self._codec.tag) - @abc.abstractmethod def _send(self, data): - """ - This method should be implemented differently under each - connection mode and serialize the data into the packet - the way it should be sent through `self._writer`. - """ - raise NotImplementedError + self._writer.write(self._codec.encode_packet(data)) - @abc.abstractmethod async def _recv(self): - """ - This method should be implemented differently under each - connection mode and deserialize the data from the packet - the way it should be read from `self._reader`. - """ - raise NotImplementedError + return await self._codec.read_packet(self._reader) def __str__(self): return '{}:{}/{}'.format( self._ip, self._port, self.__class__.__name__.replace('Connection', '') ) + + +class ObfuscatedConnection(Connection): + """ + Base class for "obfuscated" connections ("obfuscated2", "mtproto proxy") + """ + """ + This attribute should be redefined by subclasses + """ + obfuscated_io = None + + def _init_conn(self): + self._obfuscation = self.obfuscated_io(self) + self._writer.write(self._obfuscation.header) + + def _send(self, data): + self._obfuscation.write(self._codec.encode_packet(data)) + + async def _recv(self): + return await self._codec.read_packet(self._obfuscation) + + +class PacketCodec(abc.ABC): + """ + Base class for packet codecs + """ + + """ + This attribute should be re-defined by subclass to define if some + "magic bytes" should be sent to server right after conection is made to + signal which protocol will be used + """ + tag = None + + def __init__(self, connection): + """ + Codec is created when connection is just made. + """ + pass + + @abc.abstractmethod + def encode_packet(self, data): + """ + Encodes single packet and returns encoded bytes. + """ + raise NotImplementedError + + @abc.abstractmethod + async def read_packet(self, reader): + """ + Reads single packet from `reader` object that should have + `readexactly(n)` method. + """ + raise NotImplementedError diff --git a/telethon/network/connection/http.py b/telethon/network/connection/http.py index bfda941d..253a60b0 100644 --- a/telethon/network/connection/http.py +++ b/telethon/network/connection/http.py @@ -1,34 +1,43 @@ import asyncio -from .connection import Connection +from .connection import Connection, PacketCodec SSL_PORT = 443 -class ConnectionHttp(Connection): - async def connect(self, timeout=None, ssl=None): - await super().connect(timeout=timeout, ssl=self._port == SSL_PORT) +class HttpPacketCodec(PacketCodec): + tag = None + obfuscate_tag = None - def _send(self, message): - self._writer.write( - 'POST /api HTTP/1.1\r\n' - 'Host: {}:{}\r\n' - 'Content-Type: application/x-www-form-urlencoded\r\n' - 'Connection: keep-alive\r\n' - 'Keep-Alive: timeout=100000, max=10000000\r\n' - 'Content-Length: {}\r\n\r\n' - .format(self._ip, self._port, len(message)) - .encode('ascii') + message - ) + def __init__(self, connection): + self._ip = connection._ip + self._port = connection._port - async def _recv(self): + def encode_packet(self, data): + return ('POST /api HTTP/1.1\r\n' + 'Host: {}:{}\r\n' + 'Content-Type: application/x-www-form-urlencoded\r\n' + 'Connection: keep-alive\r\n' + 'Keep-Alive: timeout=100000, max=10000000\r\n' + 'Content-Length: {}\r\n\r\n' + .format(self._ip, self._port, len(data)) + .encode('ascii') + data) + + async def read_packet(self, reader): while True: - line = await self._reader.readline() + line = await reader.readline() if not line or line[-1] != b'\n': raise asyncio.IncompleteReadError(line, None) if line.lower().startswith(b'content-length: '): - await self._reader.readexactly(2) + await reader.readexactly(2) length = int(line[16:-2]) - return await self._reader.readexactly(length) + return await reader.readexactly(length) + + +class ConnectionHttp(Connection): + packet_codec = HttpPacketCodec + + async def connect(self, timeout=None, ssl=None): + await super().connect(timeout=timeout, ssl=self._port == SSL_PORT) diff --git a/telethon/network/connection/tcpabridged.py b/telethon/network/connection/tcpabridged.py index 70cd610d..171b1d8c 100644 --- a/telethon/network/connection/tcpabridged.py +++ b/telethon/network/connection/tcpabridged.py @@ -1,6 +1,27 @@ import struct -from .connection import Connection +from .connection import Connection, PacketCodec + + +class AbridgedPacketCodec(PacketCodec): + tag = b'\xef' + obfuscate_tag = b'\xef\xef\xef\xef' + + def encode_packet(self, data): + length = len(data) >> 2 + if length < 127: + length = struct.pack('B', length) + else: + length = b'\x7f' + int.to_bytes(length, 3, 'little') + return length + data + + async def read_packet(self, reader): + length = struct.unpack('= 127: + length = struct.unpack( + '> 2 - if length < 127: - length = struct.pack('B', length) - else: - length = b'\x7f' + int.to_bytes(length, 3, 'little') - - self._write(length + data) - - async def _recv(self): - length = struct.unpack('= 127: - length = struct.unpack( - ' 0: + return packet_with_padding[:-pad_size] + return packet_with_padding class ConnectionTcpIntermediate(Connection): @@ -8,12 +43,4 @@ class ConnectionTcpIntermediate(Connection): Intermediate mode between `ConnectionTcpFull` and `ConnectionTcpAbridged`. Always sends 4 extra bytes for the packet length. """ - def _init_conn(self): - self._writer.write(b'\xee\xee\xee\xee') - - def _send(self, data): - self._writer.write(struct.pack('