Skip to content

Commit 14bc19d

Browse files
committed
Added MtProxy Support
1 parent e311c9e commit 14bc19d

6 files changed

Lines changed: 579 additions & 5 deletions

File tree

pyrogram/connection/connection.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222

2323
from pyrogram import utils
2424

25-
from .transport import TCP, TCPAbridged
25+
from .transport import TCP, TCPAbridged, TCPMTProxyAbridged, TCPMTProxyIntermediate, TCPMTProxyRandomizedIntermediate
2626

2727
log = logging.getLogger(__name__)
2828

@@ -51,6 +51,12 @@ def __init__(
5151
self.media = media
5252
self.protocol_factory = protocol_factory
5353
self.crypto_executor_workers = crypto_executor_workers
54+
self.is_mtproxy = False
55+
56+
# if isinstance(proxy, str) and proxy.lower().startswith("mtproxy://"):
57+
# self.is_mtproxy = True
58+
if isinstance(proxy, dict) and proxy.get("scheme", "").lower() == "mtproxy":
59+
self.is_mtproxy = True
5460

5561
self.protocol: Optional[TCP] = None
5662

@@ -61,8 +67,10 @@ def __init__(
6167

6268
async def connect(self) -> None:
6369
for i in range(Connection.MAX_CONNECTION_ATTEMPTS):
64-
self.protocol = self.protocol_factory(ipv6=self.ipv6, proxy=self.proxy, crypto_executor_workers=self.crypto_executor_workers, loop=self.loop)
65-
70+
if self.is_mtproxy and self.protocol_factory not in (TCPMTProxyAbridged, TCPMTProxyIntermediate, TCPMTProxyRandomizedIntermediate):
71+
self.protocol = TCPMTProxyAbridged(dc_id=self.dc_id, ipv6=self.ipv6, proxy=self.proxy, crypto_executor_workers=self.crypto_executor_workers, loop=self.loop)
72+
else:
73+
self.protocol = self.protocol_factory(ipv6=self.ipv6, proxy=self.proxy, crypto_executor_workers=self.crypto_executor_workers, loop=self.loop)
6674
try:
6775
log.info("Connecting...")
6876
await self.protocol.connect((self.server_address, self.port))

pyrogram/connection/transport/tcp/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,4 @@
2222
from .tcp_full import TCPFull
2323
from .tcp_intermediate import TCPIntermediate
2424
from .tcp_intermediate_o import TCPIntermediateO
25+
from .tcp_mtproxy import TCPMTProxyAbridged, TCPMTProxyIntermediate, TCPMTProxyRandomizedIntermediate

pyrogram/connection/transport/tcp/tcp.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ class ProxyDict(TypedDict):
3636
scheme: str
3737
hostname: str
3838
port: int
39+
secret: Optional[str]
3940
username: Optional[str]
4041
password: Optional[str]
4142

@@ -154,7 +155,7 @@ async def _connect_via_direct(self, destination: Tuple[str, int]) -> None:
154155
log.info("Connection established")
155156

156157
async def _connect(self, destination: Tuple[str, int]) -> None:
157-
if self.proxy:
158+
if self.proxy and isinstance(self.proxy, dict) and self.proxy.get("scheme", "").lower() != "mtproxy":
158159
await self._connect_via_proxy(destination)
159160
else:
160161
await self._connect_via_direct(destination)
@@ -245,4 +246,4 @@ async def recv(self, length: int = 0) -> Optional[bytes]:
245246
return None
246247

247248
log.debug("Recv complete: %d bytes", len(data))
248-
return data
249+
return data
Lines changed: 334 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,334 @@
1+
# Pyrogram - Telegram MTProto API Client Library for Python
2+
# Copyright (C) 2017-present Dan <https://github.com/delivrance>
3+
#
4+
# This file is part of Pyrogram.
5+
#
6+
# Pyrogram is free software: you can redistribute it and/or modify
7+
# it under the terms of the GNU Lesser General Public License as published
8+
# by the Free Software Foundation, either version 3 of the License, or
9+
# (at your option) any later version.
10+
#
11+
# Pyrogram is distributed in the hope that it will be useful,
12+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
# GNU Lesser General Public License for more details.
15+
#
16+
# You should have received a copy of the GNU Lesser General Public License
17+
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
18+
19+
20+
import os
21+
import time
22+
import hmac
23+
import random
24+
import struct
25+
import asyncio
26+
import hashlib
27+
import logging
28+
29+
from typing import Optional, Tuple, Union
30+
31+
from pyrogram.crypto import aes, faketls
32+
33+
from .tcp import TCP, ProxyDict
34+
35+
36+
log = logging.getLogger(__name__)
37+
38+
39+
# Packet codec classes – each defines an obfuscation tag.
40+
41+
class AbridgedPacketCodec:
42+
obfuscate_tag = b"\xef\xef\xef\xef"
43+
44+
@staticmethod
45+
def encode(data: bytes) -> bytes:
46+
length = len(data) // 4
47+
if length <= 126:
48+
return bytes([length]) + data
49+
else:
50+
return b"\x7f" + length.to_bytes(3, "little") + data
51+
52+
@staticmethod
53+
def decode(reader) -> bytes:
54+
raise NotImplementedError
55+
56+
57+
class IntermediatePacketCodec:
58+
obfuscate_tag = b"\xee\xee\xee\xee"
59+
60+
@staticmethod
61+
def encode(data: bytes) -> bytes:
62+
return len(data).to_bytes(4, "little") + data
63+
64+
@staticmethod
65+
def decode(reader) -> bytes:
66+
raise NotImplementedError
67+
68+
69+
class RandomizedIntermediatePacketCodec:
70+
obfuscate_tag = b"\xdd\xdd\xdd\xdd"
71+
72+
@staticmethod
73+
def encode(data: bytes) -> bytes:
74+
padding = os.urandom(os.urandom(1)[0] % 4)
75+
return (len(data) + len(padding)).to_bytes(4, "little") + data + padding
76+
77+
@staticmethod
78+
def decode(reader) -> bytes:
79+
raise NotImplementedError
80+
81+
82+
# Main MTProxy transport class with subclasses for each codec.
83+
84+
class TCPMTProxy(TCP):
85+
packet_codec = AbridgedPacketCodec # default
86+
RESERVED = (b"HEAD", b"POST", b"GET ", b"OPTI", b"\xee" * 4)
87+
88+
def __init__(
89+
self,
90+
dc_id: int,
91+
ipv6: bool = False,
92+
proxy: Optional[Union[str, ProxyDict]] = None,
93+
crypto_executor_workers: int = 1,
94+
loop: Optional[asyncio.AbstractEventLoop] = None,
95+
) -> None:
96+
super().__init__(ipv6, proxy, crypto_executor_workers, loop)
97+
98+
self.dc_id = dc_id
99+
self.secret = None
100+
self.secret_mode = "ef" # "ef", "ee", or "dd" – used only for parsing
101+
self.hostname = None
102+
self.port = None
103+
104+
self._faketls = False
105+
self._sni_hostname = "www.google.com"
106+
107+
self._recv_buf = bytearray()
108+
109+
if isinstance(proxy, dict) and proxy.get("scheme", "").lower() == "mtproxy":
110+
self.hostname = proxy.get("hostname")
111+
self.port = proxy.get("port")
112+
raw = proxy.get("secret", "")
113+
114+
if isinstance(raw, str):
115+
prefix = raw[:2].lower()
116+
if prefix == "ee":
117+
payload_bytes = bytes.fromhex(raw[2:])
118+
self.secret = payload_bytes[:16]
119+
domain_bytes = payload_bytes[16:]
120+
self.secret_mode = "ee"
121+
self._sni_hostname = domain_bytes.decode("utf-8", errors="ignore") or "www.google.com"
122+
self._faketls = True
123+
elif prefix == "dd":
124+
self.secret = bytes.fromhex(raw[2:])
125+
self.secret_mode = "dd"
126+
else:
127+
self.secret = bytes.fromhex(raw)
128+
else:
129+
self.secret = raw
130+
131+
self.is_mtproxy = bool(self.hostname and self.port and self.secret)
132+
133+
log.info(
134+
f"TCPMTProxy initialized (dc={dc_id}, mode={self.secret_mode}, "
135+
f"faketls={self._faketls}, host={self.hostname}:{self.port}, "
136+
f"codec={self.packet_codec.__name__})"
137+
)
138+
139+
async def connect(self, address: Tuple[str, int]) -> None:
140+
self.marker_event.clear()
141+
142+
while True:
143+
nonce = bytearray(os.urandom(64))
144+
if (
145+
nonce[0] not in (0xEF, 0xDD)
146+
and nonce[:4] not in self.RESERVED
147+
and nonce[4:8] != b"\x00" * 4
148+
):
149+
break
150+
151+
temp = bytearray(nonce[55:7:-1])
152+
encrypt_key = hashlib.sha256(bytes(nonce[8:40]) + self.secret).digest()
153+
decrypt_key = hashlib.sha256(bytes(temp[:32]) + self.secret).digest()
154+
155+
self.encrypt = (encrypt_key, bytes(nonce[40:56]), bytearray(1))
156+
self.decrypt = (decrypt_key, bytes(temp[32:48]), bytearray(1))
157+
158+
nonce[56:60] = self.packet_codec.obfuscate_tag
159+
160+
nonce[60:62] = self.dc_id.to_bytes(2, "little", signed=True)
161+
nonce[62:64] = b"\x00\x00"
162+
163+
encrypted = aes.ctr256_encrypt(bytes(nonce), *self.encrypt)
164+
nonce[56:64] = encrypted[56:64]
165+
166+
await super().connect((self.hostname, self.port))
167+
168+
if self._faketls:
169+
await self._send_faketls_handshake(nonce)
170+
else:
171+
await super().send(nonce, wait_for_marker=False)
172+
173+
self.marker_event.set()
174+
175+
async def _send_faketls_handshake(self, nonce: bytearray) -> None:
176+
client_hello = faketls.build_fake_tls_client_hello(self._sni_hostname)
177+
178+
h = hmac.new(self.secret, client_hello, hashlib.sha256).digest()
179+
timestamp = struct.pack("<I", int(time.time()))
180+
hmac_result = h[:28] + bytes(a ^ b for a, b in zip(h[28:32], timestamp))
181+
182+
client_hello = client_hello[:11] + hmac_result + client_hello[43:]
183+
await super().send(client_hello, wait_for_marker=False)
184+
185+
sh = await super().recv(5)
186+
if not sh or len(sh) < 5 or sh[0] != 0x16:
187+
raise ConnectionError(f"FakeTLS: expected ServerHello (0x16), got {sh.hex() if sh else 'None'}")
188+
await super().recv(int.from_bytes(sh[3:5], "big"))
189+
190+
ccs = await super().recv(5)
191+
if not ccs or len(ccs) < 5:
192+
raise ConnectionError("FakeTLS: short record after ServerHello")
193+
if ccs[0] != 0x14:
194+
raise ConnectionError(
195+
f"FakeTLS: expected ChangeCipherSpec (0x14), got 0x{ccs[0]:02x} "
196+
"(camouflage fallback - secret/timestamp rejected or not a FakeTLS proxy)"
197+
)
198+
await super().recv(int.from_bytes(ccs[3:5], "big"))
199+
200+
app = await super().recv(5)
201+
if not app or len(app) < 5 or app[0] != 0x17:
202+
raise ConnectionError(f"FakeTLS: expected ApplicationData (0x17), got {app.hex() if app else 'None'}")
203+
app_len = int.from_bytes(app[3:5], "big")
204+
if app_len:
205+
await super().recv(app_len)
206+
207+
await super().send(
208+
b"\x14\x03\x03\x00\x01\x01" + self._tls_record(0x17, bytes(nonce)),
209+
wait_for_marker=False,
210+
)
211+
212+
def _tls_record(self, record_type: int, data: bytes) -> bytes:
213+
return struct.pack("!BHH", record_type, 0x0303, len(data)) + data
214+
215+
async def send(self, data: bytes, *args) -> None:
216+
if issubclass(self.packet_codec, (IntermediatePacketCodec, RandomizedIntermediatePacketCodec)): # Padded intermediate transport
217+
padding = os.urandom(random.randint(0, 3))
218+
payload = struct.pack("<I", len(data) + len(padding)) + data + padding
219+
else: # Abridged transport (default)
220+
length = len(data) // 4
221+
payload = (
222+
bytes([length])
223+
if length <= 126
224+
else b"\x7f" + length.to_bytes(3, "little")
225+
) + data
226+
227+
encrypted = await self.loop.run_in_executor(
228+
self.crypto_executor,
229+
aes.ctr256_encrypt,
230+
payload,
231+
*self.encrypt,
232+
)
233+
234+
if self._faketls:
235+
encrypted = self._tls_record(0x17, encrypted)
236+
237+
await super().send(encrypted)
238+
239+
async def recv(self, length: int = 0) -> Optional[bytes]:
240+
if issubclass(self.packet_codec, (IntermediatePacketCodec, RandomizedIntermediatePacketCodec)):
241+
return await self._recv_padded()
242+
else:
243+
return await self._recv_abridged()
244+
245+
async def _recv_tls(self, length: int) -> Optional[bytes]:
246+
while len(self._recv_buf) < length:
247+
if self._faketls:
248+
header = await super().recv(5)
249+
if not header:
250+
return None
251+
record_type, _, record_len = struct.unpack("!BHH", header)
252+
if record_type != 0x17:
253+
await super().recv(record_len)
254+
continue
255+
body = await super().recv(record_len)
256+
if not body:
257+
return None
258+
self._recv_buf.extend(body)
259+
else:
260+
chunk = await super().recv(length - len(self._recv_buf))
261+
if not chunk:
262+
return None
263+
self._recv_buf.extend(chunk)
264+
265+
res = bytes(self._recv_buf[:length])
266+
self._recv_buf = self._recv_buf[length:]
267+
return res
268+
269+
async def _recv_padded(self) -> Optional[bytes]:
270+
raw_len = await self._recv_tls(4)
271+
if raw_len is None:
272+
return None
273+
274+
raw_len = aes.ctr256_decrypt(raw_len, *self.decrypt)
275+
packet_length = int.from_bytes(raw_len, "little")
276+
277+
data = await self._recv_tls(packet_length)
278+
if data is None:
279+
return None
280+
281+
decrypted = await self.loop.run_in_executor(
282+
self.crypto_executor,
283+
aes.ctr256_decrypt,
284+
data,
285+
*self.decrypt,
286+
)
287+
288+
if len(decrypted) >= 20 and decrypted[:8] == b"\x00" * 8:
289+
real = 20 + int.from_bytes(decrypted[16:20], "little")
290+
elif len(decrypted) >= 24:
291+
real = 24 + ((len(decrypted) - 24) // 16) * 16
292+
else:
293+
real = len(decrypted)
294+
295+
return decrypted[:real]
296+
297+
async def _recv_abridged(self) -> Optional[bytes]:
298+
length = await self._recv_tls(1)
299+
if length is None:
300+
return None
301+
302+
length = aes.ctr256_decrypt(length, *self.decrypt)
303+
304+
if length == b"\x7f":
305+
length = await self._recv_tls(3)
306+
if length is None:
307+
return None
308+
length = aes.ctr256_decrypt(length, *self.decrypt)
309+
310+
packet_length = int.from_bytes(length, "little") * 4
311+
data = await self._recv_tls(packet_length)
312+
if data is None:
313+
return None
314+
315+
return await self.loop.run_in_executor(
316+
self.crypto_executor,
317+
aes.ctr256_decrypt,
318+
data,
319+
*self.decrypt,
320+
)
321+
322+
323+
# Subclasses for each packet codec
324+
325+
class TCPMTProxyAbridged(TCPMTProxy):
326+
packet_codec = AbridgedPacketCodec
327+
328+
329+
class TCPMTProxyIntermediate(TCPMTProxy):
330+
packet_codec = IntermediatePacketCodec
331+
332+
333+
class TCPMTProxyRandomizedIntermediate(TCPMTProxy):
334+
packet_codec = RandomizedIntermediatePacketCodec

0 commit comments

Comments
 (0)