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