mirror of
https://github.com/LonamiWebs/Telethon.git
synced 2025-02-17 03:51:05 +03:00
Initial implementation of MTProxy support (#1107)
This commit is contained in:
parent
45fdd098cc
commit
45d0ba9e2f
|
@ -9,7 +9,7 @@ from datetime import datetime, timezone
|
||||||
from .. import version, __name__ as __base_name__
|
from .. import version, __name__ as __base_name__
|
||||||
from ..crypto import rsa
|
from ..crypto import rsa
|
||||||
from ..extensions import markdown
|
from ..extensions import markdown
|
||||||
from ..network import MTProtoSender, ConnectionTcpFull
|
from ..network import MTProtoSender, ConnectionTcpFull, ConnectionTcpMTProxy
|
||||||
from ..sessions import Session, SQLiteSession, MemorySession
|
from ..sessions import Session, SQLiteSession, MemorySession
|
||||||
from ..tl import TLObject, functions, types
|
from ..tl import TLObject, functions, types
|
||||||
from ..tl.alltlobjects import LAYER
|
from ..tl.alltlobjects import LAYER
|
||||||
|
@ -245,6 +245,8 @@ class TelegramBaseClient(abc.ABC):
|
||||||
|
|
||||||
assert isinstance(connection, type)
|
assert isinstance(connection, type)
|
||||||
self._connection = connection
|
self._connection = connection
|
||||||
|
init_proxy = None if connection is not ConnectionTcpMTProxy else \
|
||||||
|
types.InputClientProxy(*ConnectionTcpMTProxy.address_info(proxy))
|
||||||
|
|
||||||
# Used on connection. Capture the variables in a lambda since
|
# Used on connection. Capture the variables in a lambda since
|
||||||
# exporting clients need to create this InvokeWithLayerRequest.
|
# exporting clients need to create this InvokeWithLayerRequest.
|
||||||
|
@ -258,7 +260,8 @@ class TelegramBaseClient(abc.ABC):
|
||||||
lang_code=lang_code,
|
lang_code=lang_code,
|
||||||
system_lang_code=system_lang_code,
|
system_lang_code=system_lang_code,
|
||||||
lang_pack='', # "langPacks are for official apps only"
|
lang_pack='', # "langPacks are for official apps only"
|
||||||
query=x
|
query=x,
|
||||||
|
proxy=init_proxy
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -345,6 +348,7 @@ class TelegramBaseClient(abc.ABC):
|
||||||
await self._sender.connect(self._connection(
|
await self._sender.connect(self._connection(
|
||||||
self.session.server_address,
|
self.session.server_address,
|
||||||
self.session.port,
|
self.session.port,
|
||||||
|
self.session.dc_id,
|
||||||
loop=self._loop,
|
loop=self._loop,
|
||||||
loggers=self._log,
|
loggers=self._log,
|
||||||
proxy=self._proxy
|
proxy=self._proxy
|
||||||
|
@ -474,6 +478,7 @@ class TelegramBaseClient(abc.ABC):
|
||||||
await sender.connect(self._connection(
|
await sender.connect(self._connection(
|
||||||
dc.ip_address,
|
dc.ip_address,
|
||||||
dc.port,
|
dc.port,
|
||||||
|
dc.id,
|
||||||
loop=self._loop,
|
loop=self._loop,
|
||||||
loggers=self._log,
|
loggers=self._log,
|
||||||
proxy=self._proxy
|
proxy=self._proxy
|
||||||
|
@ -505,6 +510,7 @@ class TelegramBaseClient(abc.ABC):
|
||||||
await sender.connect(self._connection(
|
await sender.connect(self._connection(
|
||||||
dc.ip_address,
|
dc.ip_address,
|
||||||
dc.port,
|
dc.port,
|
||||||
|
dc.id,
|
||||||
loop=self._loop,
|
loop=self._loop,
|
||||||
loggers=self._log,
|
loggers=self._log,
|
||||||
proxy=self._proxy
|
proxy=self._proxy
|
||||||
|
|
|
@ -6,6 +6,6 @@ from .mtprotoplainsender import MTProtoPlainSender
|
||||||
from .authenticator import do_authentication
|
from .authenticator import do_authentication
|
||||||
from .mtprotosender import MTProtoSender
|
from .mtprotosender import MTProtoSender
|
||||||
from .connection import (
|
from .connection import (
|
||||||
ConnectionTcpFull, ConnectionTcpAbridged, ConnectionTcpObfuscated,
|
ConnectionTcpFull, ConnectionTcpIntermediate, ConnectionTcpAbridged,
|
||||||
ConnectionTcpIntermediate, ConnectionHttp
|
ConnectionTcpObfuscated, ConnectionTcpMTProxy, ConnectionHttp
|
||||||
)
|
)
|
||||||
|
|
|
@ -10,7 +10,7 @@ from ..tl.types import (
|
||||||
ResPQ, PQInnerData, ServerDHParamsFail, ServerDHParamsOk,
|
ResPQ, PQInnerData, ServerDHParamsFail, ServerDHParamsOk,
|
||||||
ServerDHInnerData, ClientDHInnerData, DhGenOk, DhGenRetry, DhGenFail
|
ServerDHInnerData, ClientDHInnerData, DhGenOk, DhGenRetry, DhGenFail
|
||||||
)
|
)
|
||||||
from .. import helpers as utils
|
from .. import helpers
|
||||||
from ..crypto import AES, AuthKey, Factorization, rsa
|
from ..crypto import AES, AuthKey, Factorization, rsa
|
||||||
from ..errors import SecurityError
|
from ..errors import SecurityError
|
||||||
from ..extensions import BinaryReader
|
from ..extensions import BinaryReader
|
||||||
|
@ -94,7 +94,7 @@ async def do_authentication(sender):
|
||||||
'Step 2.2 answer was %s' % server_dh_params
|
'Step 2.2 answer was %s' % server_dh_params
|
||||||
|
|
||||||
# Step 3 sending: Complete DH Exchange
|
# Step 3 sending: Complete DH Exchange
|
||||||
key, iv = utils.generate_key_data_from_nonce(
|
key, iv = helpers.generate_key_data_from_nonce(
|
||||||
res_pq.server_nonce, new_nonce
|
res_pq.server_nonce, new_nonce
|
||||||
)
|
)
|
||||||
if len(server_dh_params.encrypted_answer) % 16 != 0:
|
if len(server_dh_params.encrypted_answer) % 16 != 0:
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
from .tcpfull import ConnectionTcpFull
|
from .tcpfull import ConnectionTcpFull
|
||||||
|
from .tcpintermediate import ConnectionTcpIntermediate
|
||||||
from .tcpabridged import ConnectionTcpAbridged
|
from .tcpabridged import ConnectionTcpAbridged
|
||||||
from .tcpobfuscated import ConnectionTcpObfuscated
|
from .tcpobfuscated import ConnectionTcpObfuscated
|
||||||
from .tcpintermediate import ConnectionTcpIntermediate
|
from .tcpmtproxy import ConnectionTcpMTProxy
|
||||||
from .http import ConnectionHttp
|
from .http import ConnectionHttp
|
||||||
|
|
|
@ -18,9 +18,10 @@ class Connection(abc.ABC):
|
||||||
``ConnectionError``, which will raise when attempting to send if
|
``ConnectionError``, which will raise when attempting to send if
|
||||||
the client is disconnected (includes remote disconnections).
|
the client is disconnected (includes remote disconnections).
|
||||||
"""
|
"""
|
||||||
def __init__(self, ip, port, *, loop, loggers, proxy=None):
|
def __init__(self, ip, port, dc_id, *, loop, loggers, proxy=None):
|
||||||
self._ip = ip
|
self._ip = ip
|
||||||
self._port = port
|
self._port = port
|
||||||
|
self._dc_id = dc_id # only for MTProxy, it's an abstraction leak
|
||||||
self._loop = loop
|
self._loop = loop
|
||||||
self._log = loggers[__name__]
|
self._log = loggers[__name__]
|
||||||
self._proxy = proxy
|
self._proxy = proxy
|
||||||
|
@ -94,12 +95,6 @@ class Connection(abc.ABC):
|
||||||
if self._writer:
|
if self._writer:
|
||||||
self._writer.close()
|
self._writer.close()
|
||||||
|
|
||||||
def clone(self):
|
|
||||||
"""
|
|
||||||
Creates a clone of the connection.
|
|
||||||
"""
|
|
||||||
return self.__class__(self._ip, self._port, loop=self._loop)
|
|
||||||
|
|
||||||
def send(self, data):
|
def send(self, data):
|
||||||
"""
|
"""
|
||||||
Sends a packet of data through this connection mode.
|
Sends a packet of data through this connection mode.
|
||||||
|
|
|
@ -10,8 +10,9 @@ class ConnectionTcpFull(Connection):
|
||||||
Default Telegram mode. Sends 12 additional bytes and
|
Default Telegram mode. Sends 12 additional bytes and
|
||||||
needs to calculate the CRC value of the packet itself.
|
needs to calculate the CRC value of the packet itself.
|
||||||
"""
|
"""
|
||||||
def __init__(self, ip, port, *, loop, loggers, proxy=None):
|
def __init__(self, ip, port, dc_id, *, loop, loggers, proxy=None):
|
||||||
super().__init__(ip, port, loop=loop, loggers=loggers, proxy=proxy)
|
super().__init__(
|
||||||
|
ip, port, dc_id, loop=loop, loggers=loggers, proxy=proxy)
|
||||||
self._send_counter = 0
|
self._send_counter = 0
|
||||||
|
|
||||||
async def connect(self, timeout=None, ssl=None):
|
async def connect(self, timeout=None, ssl=None):
|
||||||
|
|
40
telethon/network/connection/tcpmtproxy.py
Normal file
40
telethon/network/connection/tcpmtproxy.py
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
from .tcpobfuscated import ConnectionTcpObfuscated
|
||||||
|
from ...crypto import AESModeCTR
|
||||||
|
|
||||||
|
|
||||||
|
class ConnectionTcpMTProxy(ConnectionTcpObfuscated):
|
||||||
|
"""
|
||||||
|
Wrapper around the "obfuscated2" mode that modifies it a little and allows
|
||||||
|
user to connect to the Telegram proxy servers commonly known as MTProxy.
|
||||||
|
Implemented very ugly due to the leaky abstractions in Telethon networking
|
||||||
|
classes that should be refactored later (TODO).
|
||||||
|
"""
|
||||||
|
@staticmethod
|
||||||
|
def address_info(proxy_info):
|
||||||
|
if proxy_info is None:
|
||||||
|
raise ValueError("No proxy info specified for MTProxy connection")
|
||||||
|
return proxy_info[:2]
|
||||||
|
|
||||||
|
def __init__(self, ip, port, dc_id, *, loop, loggers, proxy=None):
|
||||||
|
proxy_host, proxy_port = self.address_info(proxy)
|
||||||
|
super().__init__(
|
||||||
|
proxy_host, proxy_port, dc_id, loop=loop, loggers=loggers)
|
||||||
|
|
||||||
|
# TODO: Implement the dd-secret secure mode (adds noise to fool DPI)
|
||||||
|
self._secret = bytes.fromhex(proxy[2])
|
||||||
|
if len(self._secret) != 16:
|
||||||
|
raise ValueError(
|
||||||
|
"MTProxy secure mode is not implemented for now"
|
||||||
|
if len(self._secret) == 17 and self._secret[0] == 0xDD else
|
||||||
|
"MTProxy secret must be a hex-string representing 16 bytes"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _compose_key(self, data):
|
||||||
|
return hashlib.sha256(data + self._secret).digest()
|
||||||
|
|
||||||
|
def _compose_tail(self, data):
|
||||||
|
dc_id_bytes = self._dc_id.to_bytes(2, "little", signed=True)
|
||||||
|
tail_bytes = super()._compose_tail(data)
|
||||||
|
return tail_bytes[:4] + dc_id_bytes + tail_bytes[6:]
|
|
@ -7,12 +7,14 @@ from ...crypto import AESModeCTR
|
||||||
|
|
||||||
class ConnectionTcpObfuscated(ConnectionTcpAbridged):
|
class ConnectionTcpObfuscated(ConnectionTcpAbridged):
|
||||||
"""
|
"""
|
||||||
Encodes the packet just like `ConnectionTcpAbridged`, but encrypts
|
Mode that Telegram defines as "obfuscated2". Encodes the packet
|
||||||
every message with a randomly generated key using the
|
just like `ConnectionTcpAbridged`, but encrypts every message with
|
||||||
AES-CTR mode so the packets are harder to discern.
|
a randomly generated key using the AES-CTR mode so the packets are
|
||||||
|
harder to discern.
|
||||||
"""
|
"""
|
||||||
def __init__(self, ip, port, *, loop, loggers, proxy=None):
|
def __init__(self, ip, port, dc_id, *, loop, loggers, proxy=None):
|
||||||
super().__init__(ip, port, loop=loop, loggers=loggers, proxy=proxy)
|
super().__init__(
|
||||||
|
ip, port, dc_id, loop=loop, loggers=loggers, proxy=proxy)
|
||||||
self._aes_encrypt = None
|
self._aes_encrypt = None
|
||||||
self._aes_decrypt = None
|
self._aes_decrypt = None
|
||||||
|
|
||||||
|
@ -40,14 +42,22 @@ class ConnectionTcpObfuscated(ConnectionTcpAbridged):
|
||||||
random_reversed = random[55:7:-1] # Reversed (8, len=48)
|
random_reversed = random[55:7:-1] # Reversed (8, len=48)
|
||||||
|
|
||||||
# Encryption has "continuous buffer" enabled
|
# Encryption has "continuous buffer" enabled
|
||||||
encrypt_key = bytes(random[8:40])
|
encrypt_key = self._compose_key(bytes(random[8:40]))
|
||||||
encrypt_iv = bytes(random[40:56])
|
encrypt_iv = bytes(random[40:56])
|
||||||
decrypt_key = bytes(random_reversed[:32])
|
decrypt_key = self._compose_key(bytes(random_reversed[:32]))
|
||||||
decrypt_iv = bytes(random_reversed[32:48])
|
decrypt_iv = bytes(random_reversed[32:48])
|
||||||
|
|
||||||
self._aes_encrypt = AESModeCTR(encrypt_key, encrypt_iv)
|
self._aes_encrypt = AESModeCTR(encrypt_key, encrypt_iv)
|
||||||
self._aes_decrypt = AESModeCTR(decrypt_key, decrypt_iv)
|
self._aes_decrypt = AESModeCTR(decrypt_key, decrypt_iv)
|
||||||
|
|
||||||
random[56:64] = self._aes_encrypt.encrypt(bytes(random))[56:64]
|
random[56:64] = self._compose_tail(bytes(random))
|
||||||
self._writer.write(random)
|
self._writer.write(random)
|
||||||
await self._writer.drain()
|
await self._writer.drain()
|
||||||
|
|
||||||
|
# Next functions provide the variable parts of the connection handshake.
|
||||||
|
# This is necessary to modify obfuscated2 the way that MTProxy requires.
|
||||||
|
def _compose_key(self, data):
|
||||||
|
return data
|
||||||
|
|
||||||
|
def _compose_tail(self, data):
|
||||||
|
return self._aes_encrypt.encrypt(data)[56:64]
|
||||||
|
|
Loading…
Reference in New Issue
Block a user