1+ <?php declare (strict_types=1 );
2+
3+ /**
4+ * This file is part of MadelineProto.
5+ * MadelineProto is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
6+ * MadelineProto is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
7+ * See the GNU Affero General Public License for more details.
8+ * You should have received a copy of the GNU General Public License along with MadelineProto.
9+ * If not, see <http://www.gnu.org/licenses/>.
10+ *
11+ * @author Daniil Gentili <daniil@daniil.it>
12+ * @copyright 2016-2025 Daniil Gentili <daniil@daniil.it>
13+ * @license https://opensource.org/licenses/AGPL-3.0 AGPLv3
14+ * @link https://docs.madelineproto.xyz MadelineProto documentation
15+ */
16+
17+ namespace danog \MadelineProto ;
18+
19+ use Amp \ByteStream \BufferedReader ;
20+ use Amp \ByteStream \ReadableBuffer ;
21+ use danog \MadelineProto \MTProtoTools \Crypt ;
22+ use danog \MadelineProto \VoIP \SignalingProtocolVersion ;
23+ use Webrtc \ICE \RTCIceCandidate ;
24+ use Webrtc \Webrtc \RTCPeerConnection ;
25+
26+ /** @internal */
27+ final class Controller {
28+
29+ private RTCPeerConnection $ peerConnection ;
30+ public function __construct (
31+ private readonly string $ authKey ,
32+ private readonly bool $ outgoing ,
33+ private readonly SignalingProtocolVersion $ tgcallsVersion ,
34+ private readonly MTProto $ API ,
35+ )
36+ {
37+ $ this ->peerConnection = new RTCPeerConnection ();
38+ }
39+
40+
41+ private const SIGNALING_MIN_SIZE = 21 ;
42+ private const SIGNALING_MAX_SIZE = 128 * 1024 * 1024 ;
43+
44+ private const SINGLE_MESSAGE_PACKET_BIT = 1 << 31 ;
45+ private const MESSAGE_REQUIRES_ACK_SEQ_BIT = 1 << 30 ;
46+
47+ private const MAX_ALLOWED_COUNTER = ~self ::SINGLE_MESSAGE_PACKET_BIT
48+ & ~self ::MESSAGE_REQUIRES_ACK_SEQ_BIT ;
49+
50+ public const ACK_ID = 255 ;
51+ public const EMPTY_ID = 254 ;
52+ public const CUSTOM_ID = 127 ;
53+
54+ private static function gunzip (string $ data ): string
55+ {
56+ if (\strlen ($ data ) < 2 ) {
57+ return $ data ;
58+ }
59+
60+ if (($ data [0 ] == \chr (0x1f ) && $ data [1 ] == \chr (0x8b )) || ($ data [0 ] == \chr (0x78 ) && $ data [1 ] == \chr (0x9c ))) {
61+ return gzdecode ($ data );
62+ }
63+ return $ data ;
64+
65+ }
66+
67+ public function onSignaling (string $ data ): void
68+ {
69+ if ($ this ->tgcallsVersion === null ) {
70+ throw new Exception ('Protocol version is not set! ' );
71+ }
72+ if (\strlen ($ data ) < self ::SIGNALING_MIN_SIZE || \strlen ($ data ) > self ::SIGNALING_MAX_SIZE ) {
73+ throw new Exception ('Invalid signaling size! ' );
74+ }
75+ $ message_key = substr ($ data , 0 , 16 );
76+ $ data = substr ($ data , 16 );
77+ [$ aes_key , $ aes_iv , $ x ] = Crypt::voipKdf ($ message_key , $ this ->authKey , $ this ->outgoing , false );
78+ $ packet = Crypt::ctrEncrypt ($ data , $ aes_key , $ aes_iv );
79+
80+ if ($ message_key != substr (hash ('sha256 ' , substr ($ this ->authKey , 88 + $ x , 32 ).$ packet , true ), 8 , 16 )) {
81+ throw new Exception ('msg_key mismatch! ' );
82+ }
83+ if (\strlen ($ packet ) < self ::SIGNALING_MIN_SIZE || \strlen ($ packet ) > self ::SIGNALING_MAX_SIZE ) {
84+ throw new Exception ('Invalid signaling size! ' );
85+ }
86+
87+ if ($ this ->tgcallsVersion ->supportsCompression ()) {
88+ $ packet = self ::gunzip ($ packet );
89+
90+ $ seq = unpack ('N ' , substr ($ packet , 0 , 4 ))[1 ];
91+
92+ $ this ->onSignalingMessage ($ this ->deserializeRtc (null , substr ($ packet , 4 )));
93+ return ;
94+ }
95+
96+ $ packet = new BufferedReader (new ReadableBuffer ($ packet ));
97+
98+ $ first = true ;
99+ while ($ packet ->isReadable ()) {
100+ $ seq = unpack ('N ' , $ packet ->readLength (4 ))[1 ];
101+ $ messageRequiresAck = (bool ) ($ seq & self ::MESSAGE_REQUIRES_ACK_SEQ_BIT );
102+ $ singlePacketFlag = (bool ) ($ seq & self ::SINGLE_MESSAGE_PACKET_BIT );
103+
104+ if (!$ first && $ singlePacketFlag ) {
105+ throw new Exception ('Single packet flag can only be set on first message! ' );
106+ }
107+
108+ $ type = \ord ($ packet ->readLength (1 ));
109+ if ($ type === self ::EMPTY_ID ) {
110+ if (!$ first ) {
111+ throw new Exception ('Empty packet can only be first message! ' );
112+ }
113+ } elseif ($ type === self ::ACK_ID ) {
114+ // todo ack $seq (contains my seq to be acked)
115+ } else {
116+ $ length = unpack ('N ' , $ packet ->readLength (4 ))[1 ];
117+ if ($ length > 1024 * 1024 ) {
118+ throw new Exception ('Invalid signaling message length! ' );
119+ }
120+ $ str = $ packet ->readLength ($ length );
121+ if (\strlen ($ str ) !== $ length ) {
122+ throw new Exception ('Signaling message is shorter than expected! ' );
123+ }
124+
125+ $ this ->onSignalingMessage ($ this ->deserializeRtc ($ type , $ str ));
126+ }
127+ $ first = false ;
128+ }
129+
130+ }
131+ private function onSignalingMessage (array $ message ): void
132+ {
133+ if ($ this ->tgcallsVersion ->isJson ()) {
134+ $ this ->onSignalingMessageJson ($ message );
135+ return ;
136+ }
137+ }
138+
139+ private function onSignalingMessageJson (array $ message ): void
140+ {
141+ $ type = $ message ['@type ' ];
142+ if ($ type === 'Candidates ' ) {
143+ foreach ($ message ['candidates ' ] as ['sdpString ' => $ sdp ]) {
144+ $ this ->peerConnection ->addIceCandidate (RTCIceCandidate::parseSDP ($ sdp ));
145+ }
146+ return ;
147+ }
148+ var_dump ($ message );
149+ readline ();
150+ }
151+ private function deserializeRtc (?int $ type , string $ buffer ): array
152+ {
153+ if ($ this ->tgcallsVersion ->isJson ()) {
154+ return json_decode ($ buffer , true , flags: JSON_THROW_ON_ERROR );
155+ }
156+ $ buffer = new BufferedReader (new ReadableBuffer ($ buffer ));
157+ switch ($ type ) {
158+ case 1 :
159+ $ candidates = [];
160+ for ($ x = \ord ($ buffer ->readLength (1 )); $ x > 0 ; $ x --) {
161+ $ candidates []= self ::readString ($ buffer );
162+ }
163+ return [
164+ '_ ' => 'candidatesList ' ,
165+ 'ufrag ' => self ::readString ($ buffer ),
166+ 'pwd ' => self ::readString ($ buffer ),
167+ ];
168+ case 2 :
169+ $ formats = [];
170+ for ($ x = \ord ($ buffer ->readLength (1 )); $ x > 0 ; $ x --) {
171+ $ name = self ::readString ($ buffer );
172+ $ parameters = [];
173+ for ($ x = \ord ($ buffer ->readLength (1 )); $ x > 0 ; $ x --) {
174+ $ key = self ::readString ($ buffer );
175+ $ value = self ::readString ($ buffer );
176+ $ parameters [$ key ] = $ value ;
177+ }
178+ $ formats []= [
179+ 'name ' => $ name ,
180+ 'parameters ' => $ parameters ,
181+ ];
182+ }
183+ return [
184+ '_ ' => 'videoFormats ' ,
185+ 'formats ' => $ formats ,
186+ 'encoders ' => \ord ($ buffer ->readLength (1 )),
187+ ];
188+ case 3 :
189+ return ['_ ' => 'requestVideo ' ];
190+ case 4 :
191+ $ state = \ord ($ buffer ->readLength (1 ));
192+ return ['_ ' => 'remoteMediaState ' , 'audio ' => $ state & 0x01 , 'video ' => ($ state >> 1 ) & 0x03 ];
193+ case 5 :
194+ return ['_ ' => 'audioData ' , 'data ' => self ::readBuffer ($ buffer )];
195+ case 6 :
196+ return ['_ ' => 'videoData ' , 'data ' => self ::readBuffer ($ buffer )];
197+ case 7 :
198+ return ['_ ' => 'unstructuredData ' , 'data ' => self ::readBuffer ($ buffer )];
199+ case 8 :
200+ return ['_ ' => 'videoParameters ' , 'aspectRatio ' => unpack ('V ' , $ buffer ->readLength (4 ))[1 ]];
201+ case 9 :
202+ return ['_ ' => 'remoteBatteryLevelIsLow ' , 'isLow ' => (bool ) \ord ($ buffer ->readLength (1 ))];
203+ case 10 :
204+ $ lowCost = (bool ) \ord ($ buffer ->readLength (1 ));
205+ $ isLowDataRequested = (bool ) \ord ($ buffer ->readLength (1 ));
206+ return ['_ ' => 'remoteNetworkStatus ' , 'lowCost ' => $ lowCost , 'isLowDataRequested ' => $ isLowDataRequested ];
207+ }
208+ return ['_ ' => 'unknown ' , 'type ' => $ type ];
209+ }
210+ private static function readString (BufferedReader $ buffer ): string
211+ {
212+ /** @psalm-suppress InvalidArgument */
213+ return $ buffer ->readLength (\ord ($ buffer ->readLength (1 )));
214+ }
215+ private static function readBuffer (BufferedReader $ buffer ): string
216+ {
217+ return $ buffer ->readLength (unpack ('n ' , $ buffer ->readLength (2 ))[1 ]);
218+ }
219+
220+ }
0 commit comments