mirror of
https://github.com/LonamiWebs/Telethon.git
synced 2024-11-26 03:13:45 +03:00
Merge pull request #1126 from seriyps/mtproto-proxy-other-protocols
Implement different MTProto proxy protocols
This commit is contained in:
commit
05e5becd78
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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('<B', await reader.readexactly(1))[0]
|
||||
if length >= 127:
|
||||
length = struct.unpack(
|
||||
'<i', await reader.readexactly(3) + b'\0')[0]
|
||||
|
||||
return await reader.readexactly(length << 2)
|
||||
|
||||
|
||||
class ConnectionTcpAbridged(Connection):
|
||||
|
@ -9,34 +30,4 @@ class ConnectionTcpAbridged(Connection):
|
|||
only require 1 byte if the packet length is less than
|
||||
508 bytes (127 << 2, which is very common).
|
||||
"""
|
||||
def _init_conn(self):
|
||||
self._writer.write(b'\xef')
|
||||
|
||||
def _write(self, data):
|
||||
"""
|
||||
Define wrapper write methods for `TcpObfuscated` to override.
|
||||
"""
|
||||
self._writer.write(data)
|
||||
|
||||
async def _read(self, n):
|
||||
"""
|
||||
Define wrapper read methods for `TcpObfuscated` to override.
|
||||
"""
|
||||
return await self._reader.readexactly(n)
|
||||
|
||||
def _send(self, data):
|
||||
length = len(data) >> 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('<B', await self._read(1))[0]
|
||||
if length >= 127:
|
||||
length = struct.unpack(
|
||||
'<i', await self._read(3) + b'\0')[0]
|
||||
|
||||
return await self._read(length << 2)
|
||||
packet_codec = AbridgedPacketCodec
|
||||
|
|
|
@ -1,36 +1,29 @@
|
|||
import struct
|
||||
from zlib import crc32
|
||||
|
||||
from .connection import Connection
|
||||
from .connection import Connection, PacketCodec
|
||||
from ...errors import InvalidChecksumError
|
||||
|
||||
|
||||
class ConnectionTcpFull(Connection):
|
||||
"""
|
||||
Default Telegram mode. Sends 12 additional bytes and
|
||||
needs to calculate the CRC value of the packet itself.
|
||||
"""
|
||||
def __init__(self, ip, port, dc_id, *, loop, loggers, proxy=None):
|
||||
super().__init__(
|
||||
ip, port, dc_id, loop=loop, loggers=loggers, proxy=proxy)
|
||||
self._send_counter = 0
|
||||
class FullPacketCodec(PacketCodec):
|
||||
tag = None
|
||||
|
||||
def _init_conn(self):
|
||||
def __init__(self, _conn):
|
||||
self._send_counter = 0 # Important or Telegram won't reply
|
||||
|
||||
def _send(self, data):
|
||||
def encode_packet(self, data):
|
||||
# https://core.telegram.org/mtproto#tcp-transport
|
||||
# total length, sequence number, packet and checksum (CRC32)
|
||||
length = len(data) + 12
|
||||
data = struct.pack('<ii', length, self._send_counter) + data
|
||||
crc = struct.pack('<I', crc32(data))
|
||||
self._send_counter += 1
|
||||
self._writer.write(data + crc)
|
||||
return data + crc
|
||||
|
||||
async def _recv(self):
|
||||
packet_len_seq = await self._reader.readexactly(8) # 4 and 4
|
||||
async def read_packet(self, reader):
|
||||
packet_len_seq = await reader.readexactly(8) # 4 and 4
|
||||
packet_len, seq = struct.unpack('<ii', packet_len_seq)
|
||||
body = await self._reader.readexactly(packet_len - 8)
|
||||
body = await reader.readexactly(packet_len - 8)
|
||||
checksum = struct.unpack('<I', body[-4:])[0]
|
||||
body = body[:-4]
|
||||
|
||||
|
@ -39,3 +32,11 @@ class ConnectionTcpFull(Connection):
|
|||
raise InvalidChecksumError(checksum, valid_checksum)
|
||||
|
||||
return body
|
||||
|
||||
|
||||
class ConnectionTcpFull(Connection):
|
||||
"""
|
||||
Default Telegram mode. Sends 12 additional bytes and
|
||||
needs to calculate the CRC value of the packet itself.
|
||||
"""
|
||||
packet_codec = FullPacketCodec
|
||||
|
|
|
@ -1,6 +1,41 @@
|
|||
import struct
|
||||
import random
|
||||
import os
|
||||
|
||||
from .connection import Connection
|
||||
from .connection import Connection, PacketCodec
|
||||
|
||||
|
||||
class IntermediatePacketCodec(PacketCodec):
|
||||
tag = b'\xee\xee\xee\xee'
|
||||
obfuscate_tag = tag
|
||||
|
||||
def encode_packet(self, data):
|
||||
return struct.pack('<i', len(data)) + data
|
||||
|
||||
async def read_packet(self, reader):
|
||||
length = struct.unpack('<i', await reader.readexactly(4))[0]
|
||||
return await reader.readexactly(length)
|
||||
|
||||
|
||||
class RandomizedIntermediatePacketCodec(IntermediatePacketCodec):
|
||||
"""
|
||||
Data packets are aligned to 4bytes. This codec adds random bytes of size
|
||||
from 0 to 3 bytes, which are ignored by decoder.
|
||||
"""
|
||||
tag = None
|
||||
obfuscate_tag = b'\xdd\xdd\xdd\xdd'
|
||||
|
||||
def encode_packet(self, data):
|
||||
pad_size = random.randint(0, 3)
|
||||
padding = os.urandom(pad_size)
|
||||
return super().encode_packet(data + padding)
|
||||
|
||||
async def read_packet(self, reader):
|
||||
packet_with_padding = await super().read_packet(reader)
|
||||
pad_size = len(packet_with_padding) % 4
|
||||
if pad_size > 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('<i', len(data)) + data)
|
||||
|
||||
async def _recv(self):
|
||||
return await self._reader.readexactly(
|
||||
struct.unpack('<i', await self._reader.readexactly(4))[0])
|
||||
packet_codec = IntermediatePacketCodec
|
||||
|
|
|
@ -1,20 +1,97 @@
|
|||
import hashlib
|
||||
import os
|
||||
|
||||
from .tcpobfuscated import ConnectionTcpObfuscated
|
||||
from .connection import ObfuscatedConnection
|
||||
from .tcpabridged import AbridgedPacketCodec
|
||||
from .tcpintermediate import (
|
||||
IntermediatePacketCodec,
|
||||
RandomizedIntermediatePacketCodec
|
||||
)
|
||||
|
||||
from ...crypto import AESModeCTR
|
||||
|
||||
|
||||
class ConnectionTcpMTProxy(ConnectionTcpObfuscated):
|
||||
class MTProxyIO:
|
||||
"""
|
||||
Wrapper around the "obfuscated2" mode that modifies it a little and allows
|
||||
user to connect to the Telegram proxy servers commonly known as MTProxy.
|
||||
It's very similar to tcpobfuscated.ObfuscatedIO, but the way
|
||||
encryption keys, protocol tag and dc_id are encoded is different.
|
||||
"""
|
||||
header = None
|
||||
|
||||
def __init__(self, connection):
|
||||
self._reader = connection._reader
|
||||
self._writer = connection._writer
|
||||
|
||||
(self.header,
|
||||
self._encrypt,
|
||||
self._decrypt) = self.init_header(
|
||||
connection._secret, connection._dc_id, connection.packet_codec)
|
||||
|
||||
def init_header(self, secret, dc_id, packet_codec):
|
||||
# Validate
|
||||
is_dd = (len(secret) == 17) and (secret[0] == 0xDD)
|
||||
is_rand_codec = issubclass(
|
||||
packet_codec, RandomizedIntermediatePacketCodec)
|
||||
if is_dd and not is_rand_codec:
|
||||
raise ValueError(
|
||||
"Only RandomizedIntermediate can be used with dd-secrets")
|
||||
secret = secret[:-1] if is_dd else secret
|
||||
if len(secret) != 16:
|
||||
raise ValueError(
|
||||
"MTProxy secret must be a hex-string representing 16 bytes")
|
||||
|
||||
# Obfuscated messages secrets cannot start with any of these
|
||||
keywords = (b'PVrG', b'GET ', b'POST', b'\xee\xee\xee\xee')
|
||||
while True:
|
||||
random = os.urandom(64)
|
||||
if (random[0] != 0xef and
|
||||
random[:4] not in keywords and
|
||||
random[4:4] != b'\0\0\0\0'):
|
||||
break
|
||||
|
||||
random = bytearray(random)
|
||||
random_reversed = random[55:7:-1] # Reversed (8, len=48)
|
||||
|
||||
# Encryption has "continuous buffer" enabled
|
||||
encrypt_key = hashlib.sha256(
|
||||
bytes(random[8:40]) + secret).digest()
|
||||
encrypt_iv = bytes(random[40:56])
|
||||
decrypt_key = hashlib.sha256(
|
||||
bytes(random_reversed[:32]) + secret).digest()
|
||||
decrypt_iv = bytes(random_reversed[32:48])
|
||||
|
||||
encryptor = AESModeCTR(encrypt_key, encrypt_iv)
|
||||
decryptor = AESModeCTR(decrypt_key, decrypt_iv)
|
||||
|
||||
random[56:60] = packet_codec.obfuscate_tag
|
||||
|
||||
dc_id_bytes = dc_id.to_bytes(2, "little", signed=True)
|
||||
random = random[:60] + dc_id_bytes + random[62:]
|
||||
random[56:64] = encryptor.encrypt(bytes(random))[56:64]
|
||||
return (random, encryptor, decryptor)
|
||||
|
||||
async def readexactly(self, n):
|
||||
return self._decrypt.encrypt(await self._reader.readexactly(n))
|
||||
|
||||
def write(self, data):
|
||||
self._writer.write(self._encrypt.encrypt(data))
|
||||
|
||||
|
||||
class TcpMTProxy(ObfuscatedConnection):
|
||||
"""
|
||||
Connector which allows user to connect to the Telegram via proxy servers
|
||||
commonly known as MTProxy.
|
||||
Implemented very ugly due to the leaky abstractions in Telethon networking
|
||||
classes that should be refactored later (TODO).
|
||||
|
||||
.. warning::
|
||||
|
||||
The support for MTProtoProxies class is **EXPERIMENTAL** and prone to
|
||||
The support for TcpMTProxy classes is **EXPERIMENTAL** and prone to
|
||||
be changed. You shouldn't be using this class yet.
|
||||
"""
|
||||
packet_codec = None
|
||||
obfuscated_io = MTProxyIO
|
||||
|
||||
@staticmethod
|
||||
def address_info(proxy_info):
|
||||
if proxy_info is None:
|
||||
|
@ -22,22 +99,29 @@ class ConnectionTcpMTProxy(ConnectionTcpObfuscated):
|
|||
return proxy_info[:2]
|
||||
|
||||
def __init__(self, ip, port, dc_id, *, loop, loggers, proxy=None):
|
||||
# connect to proxy's host and port instead of telegram's ones
|
||||
proxy_host, proxy_port = self.address_info(proxy)
|
||||
self._secret = bytes.fromhex(proxy[2])
|
||||
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()
|
||||
class ConnectionTcpMTProxyAbridged(TcpMTProxy):
|
||||
"""
|
||||
Connect to proxy using abridged protocol
|
||||
"""
|
||||
packet_codec = AbridgedPacketCodec
|
||||
|
||||
def _compose_tail(self, data):
|
||||
dc_id_bytes = self._dc_id.to_bytes(2, "little", signed=True)
|
||||
return super()._compose_tail(data[:60] + dc_id_bytes + data[62:])
|
||||
|
||||
class ConnectionTcpMTProxyIntermediate(TcpMTProxy):
|
||||
"""
|
||||
Connect to proxy using intermediate protocol
|
||||
"""
|
||||
packet_codec = IntermediatePacketCodec
|
||||
|
||||
|
||||
class ConnectionTcpMTProxyRandomizedIntermediate(TcpMTProxy):
|
||||
"""
|
||||
Connect to proxy using randomized intermediate protocol (dd-secrets)
|
||||
"""
|
||||
packet_codec = RandomizedIntermediatePacketCodec
|
||||
|
|
|
@ -1,30 +1,23 @@
|
|||
import os
|
||||
|
||||
from .connection import Connection
|
||||
from .tcpabridged import ConnectionTcpAbridged
|
||||
from .tcpabridged import AbridgedPacketCodec
|
||||
from .connection import ObfuscatedConnection
|
||||
|
||||
from ...crypto import AESModeCTR
|
||||
|
||||
|
||||
class ConnectionTcpObfuscated(ConnectionTcpAbridged):
|
||||
"""
|
||||
Mode that Telegram defines as "obfuscated2". Encodes the packet
|
||||
just like `ConnectionTcpAbridged`, but encrypts every message with
|
||||
a randomly generated key using the AES-CTR mode so the packets are
|
||||
harder to discern.
|
||||
"""
|
||||
def __init__(self, ip, port, dc_id, *, loop, loggers, proxy=None):
|
||||
super().__init__(
|
||||
ip, port, dc_id, loop=loop, loggers=loggers, proxy=proxy)
|
||||
self._aes_encrypt = None
|
||||
self._aes_decrypt = None
|
||||
class ObfuscatedIO:
|
||||
header = None
|
||||
|
||||
def _write(self, data):
|
||||
self._writer.write(self._aes_encrypt.encrypt(data))
|
||||
def __init__(self, connection):
|
||||
self._reader = connection._reader
|
||||
self._writer = connection._writer
|
||||
|
||||
async def _read(self, n):
|
||||
return self._aes_decrypt.encrypt(await self._reader.readexactly(n))
|
||||
(self.header,
|
||||
self._encrypt,
|
||||
self._decrypt) = self.init_header(connection.packet_codec)
|
||||
|
||||
def _init_conn(self):
|
||||
def init_header(self, packet_codec):
|
||||
# Obfuscated messages secrets cannot start with any of these
|
||||
keywords = (b'PVrG', b'GET ', b'POST', b'\xee\xee\xee\xee')
|
||||
while True:
|
||||
|
@ -38,23 +31,31 @@ class ConnectionTcpObfuscated(ConnectionTcpAbridged):
|
|||
random_reversed = random[55:7:-1] # Reversed (8, len=48)
|
||||
|
||||
# Encryption has "continuous buffer" enabled
|
||||
encrypt_key = self._compose_key(bytes(random[8:40]))
|
||||
encrypt_key = bytes(random[8:40])
|
||||
encrypt_iv = bytes(random[40:56])
|
||||
decrypt_key = self._compose_key(bytes(random_reversed[:32]))
|
||||
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)
|
||||
encryptor = AESModeCTR(encrypt_key, encrypt_iv)
|
||||
decryptor = AESModeCTR(decrypt_key, decrypt_iv)
|
||||
|
||||
random[56:60] = b'\xef\xef\xef\xef'
|
||||
random[56:64] = self._compose_tail(bytes(random))
|
||||
random[56:60] = packet_codec.obfuscate_tag
|
||||
random[56:64] = encryptor.encrypt(bytes(random))[56:64]
|
||||
return (random, encryptor, decryptor)
|
||||
|
||||
self._writer.write(random)
|
||||
async def readexactly(self, n):
|
||||
return self._decrypt.encrypt(await self._reader.readexactly(n))
|
||||
|
||||
# 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 write(self, data):
|
||||
self._writer.write(self._encrypt.encrypt(data))
|
||||
|
||||
def _compose_tail(self, data):
|
||||
return self._aes_encrypt.encrypt(data)[56:64]
|
||||
|
||||
class ConnectionTcpObfuscated(ObfuscatedConnection):
|
||||
"""
|
||||
Mode that Telegram defines as "obfuscated2". Encodes the packet
|
||||
just like `ConnectionTcpAbridged`, but encrypts every message with
|
||||
a randomly generated key using the AES-CTR mode so the packets are
|
||||
harder to discern.
|
||||
"""
|
||||
obfuscated_io = ObfuscatedIO
|
||||
packet_codec = AbridgedPacketCodec
|
||||
|
|
Loading…
Reference in New Issue
Block a user