Skip to content

Commit f524209

Browse files
committed
Begin implementing tgcalls
1 parent 5b0d9d3 commit f524209

3 files changed

Lines changed: 145 additions & 38 deletions

File tree

src/VoIP/ProtocolVersion.php

Lines changed: 0 additions & 27 deletions
This file was deleted.
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace danog\MadelineProto\VoIP;
4+
5+
/** @internal */
6+
enum SignalingProtocolVersion
7+
{
8+
case V1;
9+
case V2;
10+
case V3;
11+
12+
case V1_JSON;
13+
case V2_JSON;
14+
15+
public static function fromProtocol(array $protocol): self
16+
{
17+
$v = $protocol['library_versions'] ?? [];
18+
return self::fromLibraryVersion(end($v));
19+
}
20+
21+
public static function fromLibraryVersion(string $version): self
22+
{
23+
return match ($version) {
24+
'7.0.0' => self::V1,
25+
'8.0.0' => self::V2,
26+
'9.0.0' => self::V2,
27+
28+
'10.0.0' => self::V1_JSON,
29+
'11.0.0' => self::V2_JSON,
30+
31+
'12.0.0' => self::V3,
32+
'13.0.0' => self::V3,
33+
34+
default => throw new \InvalidArgumentException("Unknown VoIP signaling protocol version: $version"),
35+
};
36+
}
37+
38+
public function isJson(): bool
39+
{
40+
return true;
41+
// return $this === self::V1_JSON || $this === self::V2_JSON;
42+
}
43+
public function supportsCompression(): bool
44+
{
45+
return $this === self::V3 || $this === self::V2_JSON;
46+
}
47+
}

src/VoIPController.php

Lines changed: 98 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
use danog\MadelineProto\VoIP\DiscardReason;
3434
use danog\MadelineProto\VoIP\Endpoint;
3535
use danog\MadelineProto\VoIP\MessageHandler;
36+
use danog\MadelineProto\VoIP\SignalingProtocolVersion;
3637
use danog\MadelineProto\VoIP\VoIPState;
3738
use phpseclib3\Math\BigInteger;
3839
use Revolt\EventLoop;
@@ -62,8 +63,8 @@ final class VoIPController
6263
"8.0.0",
6364
"9.0.0",
6465
"10.0.0",
65-
"11.0.0"
66-
]*/
66+
"11.0.0",
67+
],*/
6768
];
6869
public const NET_TYPE_UNKNOWN = 0;
6970
public const NET_TYPE_GPRS = 1;
@@ -126,6 +127,7 @@ final class VoIPController
126127
private CallState $callState;
127128

128129
private array $call;
130+
private ?SignalingProtocolVersion $tgcallsVersion = null;
129131

130132
/**
131133
* @var array<Endpoint>
@@ -263,6 +265,7 @@ public function confirm(array $params): bool
263265
}
264266
$this->visualization = $visualization;
265267
$this->authKey = $key;
268+
$this->tgcallsVersion = SignalingProtocolVersion::fromProtocol($params['protocol']);
266269
$this->callState = CallState::RUNNING;
267270
$this->messageHandler = new MessageHandler(
268271
$this,
@@ -333,6 +336,8 @@ public function complete(array $params): bool
333336
if ($this->callState !== CallState::ACCEPTED) {
334337
return false;
335338
}
339+
$this->tgcallsVersion = SignalingProtocolVersion::fromProtocol($params['protocol']);
340+
336341
$this->log(sprintf(Lang::$current_lang['call_completing'], $this->public->otherID), Logger::VERBOSE);
337342
$dh_config = $this->API->getDhConfig();
338343
if (hash('sha256', (string) $params['g_a_or_b'], true) !== (string) $this->call['g_a_hash']) {
@@ -424,35 +429,117 @@ public function discard(DiscardReason $reason = DiscardReason::HANGUP, ?int $rat
424429

425430
private const SIGNALING_MIN_SIZE = 21;
426431
private const SIGNALING_MAX_SIZE = 128 * 1024 * 1024;
432+
433+
private const SINGLE_MESSAGE_PACKET_BIT = 1 << 31;
434+
private const MESSAGE_REQUIRES_ACK_SEQ_BIT = 1 << 30;
435+
436+
private const MAX_ALLOWED_COUNTER = ~self::SINGLE_MESSAGE_PACKET_BIT
437+
& ~self::MESSAGE_REQUIRES_ACK_SEQ_BIT;
438+
439+
public const ACK_ID = 255;
440+
public const EMPTY_ID = 254;
441+
public const CUSTOM_ID = 127;
442+
443+
private static function gunzip(string $data): string
444+
{
445+
if (\strlen($data) < 2) {
446+
return $data;
447+
}
448+
449+
if (($data[0] == \chr(0x1f) && $data[1] == \chr(0x8b)) || ($data[0] == \chr(0x78) && $data[1] == \chr(0x9c))) {
450+
return gzdecode($data);
451+
}
452+
return $data;
453+
454+
}
455+
427456
public function onSignaling(string $data): void
428457
{
458+
if ($this->tgcallsVersion === null) {
459+
throw new Exception('Protocol version is not set!');
460+
}
429461
if (\strlen($data) < self::SIGNALING_MIN_SIZE || \strlen($data) > self::SIGNALING_MAX_SIZE) {
430-
Logger::log('Wrong size in signaling!', Logger::ERROR);
431-
return;
462+
throw new Exception('Invalid signaling size!');
432463
}
433464
$message_key = substr($data, 0, 16);
434465
$data = substr($data, 16);
435466
[$aes_key, $aes_iv, $x] = Crypt::voipKdf($message_key, $this->authKey, $this->public->outgoing, false);
436467
$packet = Crypt::ctrEncrypt($data, $aes_key, $aes_iv);
437468

438469
if ($message_key != substr(hash('sha256', substr($this->authKey, 88 + $x, 32).$packet, true), 8, 16)) {
439-
Logger::log('msg_key mismatch!', Logger::ERROR);
470+
throw new Exception('msg_key mismatch!');
471+
}
472+
if (\strlen($packet) < self::SIGNALING_MIN_SIZE || \strlen($packet) > self::SIGNALING_MAX_SIZE) {
473+
throw new Exception('Invalid signaling size!');
474+
}
475+
476+
if ($this->tgcallsVersion->supportsCompression()) {
477+
$packet = self::gunzip($packet);
478+
479+
$seq = unpack('N', substr($packet, 0, 4))[1];
480+
481+
$this->onSignalingMessage($this->deserializeRtc(null, substr($packet, 4)));
440482
return;
441483
}
442484

443485
$packet = new BufferedReader(new ReadableBuffer($packet));
444486

445-
$packets = [];
487+
$first = true;
446488
while ($packet->isReadable()) {
447489
$seq = unpack('N', $packet->readLength(4))[1];
448-
$length = unpack('N', $packet->readLength(4))[1];
449-
$packets []= self::deserializeRtc($packet);
490+
$messageRequiresAck = (bool) ($seq & self::MESSAGE_REQUIRES_ACK_SEQ_BIT);
491+
$singlePacketFlag = (bool) ($seq & self::SINGLE_MESSAGE_PACKET_BIT);
492+
493+
if (!$first && $singlePacketFlag) {
494+
throw new Exception('Single packet flag can only be set on first message!');
495+
}
496+
497+
$type = \ord($packet->readLength(1));
498+
if ($type === self::EMPTY_ID) {
499+
if (!$first) {
500+
throw new Exception('Empty packet can only be first message!');
501+
}
502+
} elseif ($type === self::ACK_ID) {
503+
// todo ack $seq (contains my seq to be acked)
504+
} else {
505+
$length = unpack('N', $packet->readLength(4))[1];
506+
if ($length > 1024 * 1024) {
507+
throw new Exception('Invalid signaling message length!');
508+
}
509+
$str = $packet->readLength($length);
510+
if (\strlen($str) !== $length) {
511+
throw new Exception('Signaling message is shorter than expected!');
512+
}
513+
514+
$this->onSignalingMessage($this->deserializeRtc($type, $str));
515+
}
516+
$first = false;
517+
}
518+
519+
}
520+
private function onSignalingMessage(array $message): void
521+
{
522+
if ($this->tgcallsVersion->isJson()) {
523+
$this->onSignalingMessageJson($message);
524+
return;
450525
}
451526
}
452527

453-
public static function deserializeRtc(BufferedReader $buffer): array
528+
private function onSignalingMessageJson(array $message): void
454529
{
455-
switch ($t = \ord($buffer->readLength(1))) {
530+
$type = $message['@type'];
531+
if ($type === 'Candidates') {
532+
$sdps = array_column($message['candidates'], 'sdpString');
533+
534+
}
535+
}
536+
private function deserializeRtc(?int $type, string $buffer): array
537+
{
538+
if ($this->tgcallsVersion->isJson()) {
539+
return json_decode($buffer, true, flags: JSON_THROW_ON_ERROR);
540+
}
541+
$buffer = new BufferedReader(new ReadableBuffer($buffer));
542+
switch ($type) {
456543
case 1:
457544
$candidates = [];
458545
for ($x = \ord($buffer->readLength(1)); $x > 0; $x--) {
@@ -503,7 +590,7 @@ public static function deserializeRtc(BufferedReader $buffer): array
503590
$isLowDataRequested = (bool) \ord($buffer->readLength(1));
504591
return ['_' => 'remoteNetworkStatus', 'lowCost' => $lowCost, 'isLowDataRequested' => $isLowDataRequested];
505592
}
506-
return ['_' => 'unknown', 'type' => $t];
593+
return ['_' => 'unknown', 'type' => $type];
507594
}
508595
private static function readString(BufferedReader $buffer): string
509596
{

0 commit comments

Comments
 (0)