Skip to content

Commit 38bd4ba

Browse files
committed
Extract controller
1 parent f524209 commit 38bd4ba

2 files changed

Lines changed: 242 additions & 178 deletions

File tree

src/Tgcalls/Controller.php

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
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

Comments
 (0)