|
33 | 33 | use danog\MadelineProto\VoIP\DiscardReason; |
34 | 34 | use danog\MadelineProto\VoIP\Endpoint; |
35 | 35 | use danog\MadelineProto\VoIP\MessageHandler; |
| 36 | +use danog\MadelineProto\VoIP\SignalingProtocolVersion; |
36 | 37 | use danog\MadelineProto\VoIP\VoIPState; |
37 | 38 | use phpseclib3\Math\BigInteger; |
38 | 39 | use Revolt\EventLoop; |
@@ -62,8 +63,8 @@ final class VoIPController |
62 | 63 | "8.0.0", |
63 | 64 | "9.0.0", |
64 | 65 | "10.0.0", |
65 | | - "11.0.0" |
66 | | - ]*/ |
| 66 | + "11.0.0", |
| 67 | + ],*/ |
67 | 68 | ]; |
68 | 69 | public const NET_TYPE_UNKNOWN = 0; |
69 | 70 | public const NET_TYPE_GPRS = 1; |
@@ -126,6 +127,7 @@ final class VoIPController |
126 | 127 | private CallState $callState; |
127 | 128 |
|
128 | 129 | private array $call; |
| 130 | + private ?SignalingProtocolVersion $tgcallsVersion = null; |
129 | 131 |
|
130 | 132 | /** |
131 | 133 | * @var array<Endpoint> |
@@ -263,6 +265,7 @@ public function confirm(array $params): bool |
263 | 265 | } |
264 | 266 | $this->visualization = $visualization; |
265 | 267 | $this->authKey = $key; |
| 268 | + $this->tgcallsVersion = SignalingProtocolVersion::fromProtocol($params['protocol']); |
266 | 269 | $this->callState = CallState::RUNNING; |
267 | 270 | $this->messageHandler = new MessageHandler( |
268 | 271 | $this, |
@@ -333,6 +336,8 @@ public function complete(array $params): bool |
333 | 336 | if ($this->callState !== CallState::ACCEPTED) { |
334 | 337 | return false; |
335 | 338 | } |
| 339 | + $this->tgcallsVersion = SignalingProtocolVersion::fromProtocol($params['protocol']); |
| 340 | + |
336 | 341 | $this->log(sprintf(Lang::$current_lang['call_completing'], $this->public->otherID), Logger::VERBOSE); |
337 | 342 | $dh_config = $this->API->getDhConfig(); |
338 | 343 | 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 |
424 | 429 |
|
425 | 430 | private const SIGNALING_MIN_SIZE = 21; |
426 | 431 | 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 | + |
427 | 456 | public function onSignaling(string $data): void |
428 | 457 | { |
| 458 | + if ($this->tgcallsVersion === null) { |
| 459 | + throw new Exception('Protocol version is not set!'); |
| 460 | + } |
429 | 461 | 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!'); |
432 | 463 | } |
433 | 464 | $message_key = substr($data, 0, 16); |
434 | 465 | $data = substr($data, 16); |
435 | 466 | [$aes_key, $aes_iv, $x] = Crypt::voipKdf($message_key, $this->authKey, $this->public->outgoing, false); |
436 | 467 | $packet = Crypt::ctrEncrypt($data, $aes_key, $aes_iv); |
437 | 468 |
|
438 | 469 | 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))); |
440 | 482 | return; |
441 | 483 | } |
442 | 484 |
|
443 | 485 | $packet = new BufferedReader(new ReadableBuffer($packet)); |
444 | 486 |
|
445 | | - $packets = []; |
| 487 | + $first = true; |
446 | 488 | while ($packet->isReadable()) { |
447 | 489 | $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; |
450 | 525 | } |
451 | 526 | } |
452 | 527 |
|
453 | | - public static function deserializeRtc(BufferedReader $buffer): array |
| 528 | + private function onSignalingMessageJson(array $message): void |
454 | 529 | { |
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) { |
456 | 543 | case 1: |
457 | 544 | $candidates = []; |
458 | 545 | for ($x = \ord($buffer->readLength(1)); $x > 0; $x--) { |
@@ -503,7 +590,7 @@ public static function deserializeRtc(BufferedReader $buffer): array |
503 | 590 | $isLowDataRequested = (bool) \ord($buffer->readLength(1)); |
504 | 591 | return ['_' => 'remoteNetworkStatus', 'lowCost' => $lowCost, 'isLowDataRequested' => $isLowDataRequested]; |
505 | 592 | } |
506 | | - return ['_' => 'unknown', 'type' => $t]; |
| 593 | + return ['_' => 'unknown', 'type' => $type]; |
507 | 594 | } |
508 | 595 | private static function readString(BufferedReader $buffer): string |
509 | 596 | { |
|
0 commit comments