1
0
mirror of https://github.com/danog/MadelineProto.git synced 2024-11-26 17:24:40 +01:00

Refactor VoIP API

This commit is contained in:
Daniil Gentili 2023-08-12 16:34:46 +02:00
parent a26f906c5b
commit b2af64d84e
Signed by: danog
GPG Key ID: 8C1BE3B34B230CA7
15 changed files with 589 additions and 779 deletions

View File

@ -5,8 +5,7 @@
| Version | Supported |
| ------- | ------------------ |
| 8.x | :white_check_mark: |
| 7.x | :white_check_mark: |
| < 7.x | :x: |
| < 8.x | :x: |
## Reporting a Vulnerability

View File

@ -40,6 +40,7 @@ use danog\MadelineProto\Settings\Database\Mysql;
use danog\MadelineProto\Settings\Database\Postgres;
use danog\MadelineProto\Settings\Database\Redis;
use danog\MadelineProto\SimpleEventHandler;
use danog\MadelineProto\VoIP;
// MadelineProto is already loaded
if (class_exists(API::class)) {
@ -56,6 +57,9 @@ if (class_exists(API::class)) {
/**
* Event handler class.
*
* NOTE: ALL of the following methods are OPTIONAL.
* You can even provide an empty event handler if you want.
*
* All properties returned by __sleep are automatically stored in the database.
*/
class MyEventHandler extends SimpleEventHandler
@ -228,7 +232,7 @@ class MyEventHandler extends SimpleEventHandler
$message->reply($args[0] ?? '');
}
#[FilterRegex('/.*(mt?proto).*/i')]
#[FilterRegex('/.*(mt?proto)[^.]?.*/i')]
public function testRegex(Incoming & Message $message): void
{
$message->reply("Did you mean to write MadelineProto instead of ".$message->matches[1].'?');
@ -288,6 +292,18 @@ class MyEventHandler extends SimpleEventHandler
$reply->reply("Download link: ".$reply->media->getDownloadLink());
}
#[FilterCommand('call')]
public function callVoip(Incoming&Message $message): void
{
$this->requestCall($message->senderId)->play(__DIR__.'/../music.ogg');
}
#[Handler]
public function handleIncomingCall(VoIP $call): void
{
$call->accept()->play(__DIR__.'/../music.ogg');
}
public static function getPluginPaths(): string|array|null
{
return 'plugins/';

View File

@ -5,6 +5,8 @@ namespace danog\MadelineProto\EventHandler\Filter;
use Attribute;
use danog\MadelineProto\EventHandler\AbstractMessage;
use danog\MadelineProto\EventHandler\Update;
use danog\MadelineProto\VoIP;
use danog\MadelineProto\VoIP\CallState;
/**
* Allow only incoming messages.
@ -14,6 +16,8 @@ final class FilterIncoming extends Filter
{
public function apply(Update $update): bool
{
return $update instanceof AbstractMessage && !$update->out;
return ($update instanceof AbstractMessage && !$update->out)
|| ($update instanceof VoIP && $update->getCallState() === CallState::INCOMING)
;
}
}

View File

@ -126,6 +126,7 @@ final class CheckLoop extends Loop
$list = '';
// Don't edit this here pls
foreach ($message_ids as $message_id) {
if (!isset($this->connection->outgoing_messages[$message_id])) continue;
$list .= $this->connection->outgoing_messages[$message_id]->getConstructor().', ';
}
$this->logger->logger("Still missing {$list} on DC {$this->datacenter}, sending state request", Logger::ERROR);

View File

@ -59,6 +59,7 @@ use danog\MadelineProto\TL\TL;
use danog\MadelineProto\TL\TLCallback;
use danog\MadelineProto\TL\TLInterface;
use danog\MadelineProto\TL\Types\LoginQrCode;
use danog\MadelineProto\VoIP\CallState;
use danog\MadelineProto\Wrappers\Ads;
use danog\MadelineProto\Wrappers\Button;
use danog\MadelineProto\Wrappers\DialogHandler;
@ -293,10 +294,6 @@ final class MTProto implements TLCallback, LoggerGetter
* Config loop.
*/
public ?PeriodicLoopInternal $configLoop = null;
/**
* Call checker loop.
*/
private ?PeriodicLoopInternal $callCheckerLoop = null;
/**
* Autoserialization loop.
*/
@ -820,7 +817,6 @@ final class MTProto implements TLCallback, LoggerGetter
*/
private function startLoops(): void
{
$this->callCheckerLoop ??= new PeriodicLoopInternal($this, $this->checkCalls(...), 'call check', 10);
$this->serializeLoop ??= new PeriodicLoopInternal($this, $this->serialize(...), 'serialize', $this->settings->getSerialization()->getInterval());
$this->phoneConfigLoop ??= new PeriodicLoopInternal($this, $this->getPhoneConfig(...), 'phone config', 3600);
$this->configLoop ??= new PeriodicLoopInternal($this, $this->getConfig(...), 'config', 3600);
@ -834,7 +830,6 @@ final class MTProto implements TLCallback, LoggerGetter
$this->logger->logger("Error while starting IPC server: $e", Logger::FATAL_ERROR);
}
}
$this->callCheckerLoop->start();
$this->serializeLoop->start();
$this->phoneConfigLoop->start();
$this->configLoop->start();
@ -852,10 +847,6 @@ final class MTProto implements TLCallback, LoggerGetter
*/
private function stopLoops(): void
{
if ($this->callCheckerLoop) {
$this->callCheckerLoop->stop();
$this->callCheckerLoop = null;
}
if ($this->serializeLoop) {
$this->serializeLoop->stop();
$this->serializeLoop = null;
@ -1006,18 +997,6 @@ final class MTProto implements TLCallback, LoggerGetter
$this->TL->updateCallbacks($callbacks);
// Clean up phone call array
foreach ($this->calls as $id => $controller) {
if (!\is_object($controller)) {
unset($this->calls[$id]);
} elseif ($controller->getCallState() === VoIP::CALL_STATE_ENDED) {
$controller->setMadeline($this);
$controller->discard();
} else {
$controller->setMadeline($this);
}
}
$this->settings->getConnection()->init();
// Setup logger
$this->setupLogger();

View File

@ -58,6 +58,7 @@ use danog\MadelineProto\TL\Types\Button;
use danog\MadelineProto\Tools;
use danog\MadelineProto\UpdateHandlerType;
use danog\MadelineProto\VoIP;
use danog\MadelineProto\VoIP\CallState;
use Revolt\EventLoop;
use Throwable;
use Webmozart\Assert\Assert;
@ -344,6 +345,7 @@ trait UpdateHandler
'updateInlineBotCallbackQuery' => isset($update['game_short_name'])
? new InlineGameQuery($this, $update)
: new InlineButtonQuery($this, $update),
'updatePhoneCall' => $update['phone_call'],
default => null
};
}
@ -749,38 +751,47 @@ trait UpdateHandler
return;
}
if ($update['_'] === 'updatePhoneCall') {
if (!\class_exists('\\danog\\MadelineProto\\VoIP')) {
$this->logger->logger('The php-libtgvoip extension is required to accept and manage calls. See daniil.it/MadelineProto for more info.', Logger::WARNING);
return;
}
switch ($update['phone_call']['_']) {
case 'phoneCallRequested':
return;
/*if (!isset($this->calls[$update['phone_call']['id']])) {
$update['phone_call'] = $this->calls[$update['phone_call']['id']] = new VoIP(
$this,
$update['phone_call'],
);
}
break;*/
case 'phoneCallWaiting':
if (isset($this->calls[$update['phone_call']['id']])) {
return;
}
$controller = new VoIP(false, $update['phone_call']['admin_id'], $this, VoIP::CALL_STATE_INCOMING);
$controller->setCall($update['phone_call']);
$controller->storage = ['g_a_hash' => $update['phone_call']['g_a_hash']];
$controller->storage['video'] = $update['phone_call']['video'] ?? false;
$update['phone_call'] = $this->calls[$update['phone_call']['id']] = $controller;
$update['phone_call'] = $this->calls[$update['phone_call']['id']] = new VoIP(
$this,
$update['phone_call']
);
break;
case 'phoneCallAccepted':
if (!($this->confirmCall($update['phone_call']))) {
if (!isset($this->calls[$update['phone_call']['id']])) {
return;
}
$update['phone_call'] = $this->calls[$update['phone_call']['id']];
$controller = $this->calls[$update['phone_call']['id']];
$controller->confirm($update['phone_call']);
$update['phone_call'] = $controller;
break;
case 'phoneCall':
if (!($this->completeCall($update['phone_call']))) {
if (!isset($this->calls[$update['phone_call']['id']])) {
return;
}
$update['phone_call'] = $this->calls[$update['phone_call']['id']];
$controller = $this->calls[$update['phone_call']['id']];
$controller->complete($update['phone_call']);
$update['phone_call'] = $controller;
break;
case 'phoneCallDiscarded':
if (!isset($this->calls[$update['phone_call']['id']])) {
return;
}
$update['phone_call'] = $this->calls[$update['phone_call']['id']]->discard($update['phone_call']['reason'] ?? ['_' => 'phoneCallDiscardReasonDisconnect'], [], $update['phone_call']['need_debug'] ?? false);
$update['phone_call'] = $controller = $this->calls[$update['phone_call']['id']];
$controller->discard();
break;
}
}

View File

@ -303,11 +303,6 @@ final class Magic
}
}
self::$BIG_ENDIAN = \pack('L', 1) === \pack('N', 1);
if (\class_exists('\\danog\\MadelineProto\\VoIP')) {
if (!\defined('\\danog\\MadelineProto\\VoIP::PHP_LIBTGVOIP_VERSION') || !\in_array(VoIP::PHP_LIBTGVOIP_VERSION, ['1.5.0'], true)) {
throw new Exception(\hex2bin(Lang::$current_lang['v_tgerror']), 0, null, 'MadelineProto', 1);
}
}
self::$hasOpenssl = \extension_loaded('openssl');
self::$emojis = \json_decode(self::JSON_EMOJIS);
self::$zero = new BigInteger(0);

View File

@ -600,7 +600,7 @@ final class Ogg
$writeTag("MadelineProto ".API::RELEASE.", ".$opus->opus_get_version_string());
$tags .= \pack('V', 2);
$writeTag("ENCODER=MadelineProto ".API::RELEASE." with ".$opus->opus_get_version_string());
$writeTag('See https://docs.madelineproto.xyz/docs/VOIP.html for more info');
$writeTag('See https://docs.madelineproto.xyz/docs/CALLS.html for more info');
$writePage(
0,
0,

View File

@ -15,41 +15,28 @@ If not, see <http://www.gnu.org/licenses/>.
namespace danog\MadelineProto;
use AssertionError;
use danog\MadelineProto\EventHandler\Update;
use danog\MadelineProto\MTProto\PermAuthKey;
use danog\MadelineProto\MTProtoTools\Crypt;
use danog\MadelineProto\Stream\Common\FileBufferedStream;
use danog\MadelineProto\Stream\ConnectionContext;
use danog\MadelineProto\VoIP\AckHandler;
use danog\MadelineProto\VoIP\CallState;
use danog\MadelineProto\VoIP\Endpoint;
use danog\MadelineProto\VoIP\MessageHandler;
use danog\MadelineProto\VoIP\VoIPState;
use phpseclib3\Math\BigInteger;
use Revolt\EventLoop;
use SplQueue;
use Throwable;
use Webmozart\Assert\Assert;
use function Amp\delay;
use function Amp\File\openFile;
if (\extension_loaded('php-libtgvoip')) {
return;
}
final class VoIP
final class VoIP extends Update
{
use MessageHandler;
use AckHandler;
const PHP_LIBTGVOIP_VERSION = '1.5.0';
const STATE_CREATED = 0;
const STATE_WAIT_INIT = 1;
const STATE_WAIT_INIT_ACK = 2;
const STATE_ESTABLISHED = 3;
const STATE_FAILED = 4;
const STATE_RECONNECTING = 5;
const TGVOIP_ERROR_UNKNOWN = 0;
const TGVOIP_ERROR_INCOMPATIBLE = 1;
const TGVOIP_ERROR_TIMEOUT = 2;
const TGVOIP_ERROR_AUDIO_IO = 3;
const NET_TYPE_UNKNOWN = 0;
const NET_TYPE_GPRS = 1;
const NET_TYPE_EDGE = 2;
@ -75,194 +62,276 @@ final class VoIP
const AUDIO_STATE_CONFIGURED = 1;
const AUDIO_STATE_RUNNING = 2;
const CALL_STATE_NONE = -1;
const CALL_STATE_REQUESTED = 0;
const CALL_STATE_INCOMING = 1;
const CALL_STATE_ACCEPTED = 2;
const CALL_STATE_CONFIRMED = 3;
const CALL_STATE_READY = 4;
const CALL_STATE_ENDED = 5;
/** @internal */
const PKT_INIT = 1;
/** @internal */
const PKT_INIT_ACK = 2;
/** @internal */
const PKT_STREAM_STATE = 3;
/** @internal */
const PKT_STREAM_DATA = 4;
/** @internal */
const PKT_UPDATE_STREAMS = 5;
/** @internal */
const PKT_PING = 6;
/** @internal */
const PKT_PONG = 7;
/** @internal */
const PKT_STREAM_DATA_X2 = 8;
/** @internal */
const PKT_STREAM_DATA_X3 = 9;
/** @internal */
const PKT_LAN_ENDPOINT = 10;
/** @internal */
const PKT_NETWORK_CHANGED = 11;
/** @internal */
const PKT_SWITCH_PREF_RELAY = 12;
/** @internal */
const PKT_SWITCH_TO_P2P = 13;
/** @internal */
const PKT_NOP = 14;
/** @internal */
const TLID_DECRYPTED_AUDIO_BLOCK = "\xc1\xdb\xf9\x48";
/** @internal */
const TLID_SIMPLE_AUDIO_BLOCK = "\x0d\x0e\x76\xcc";
/** @internal */
const TLID_REFLECTOR_SELF_INFO = "\xC7\x72\x15\xc0";
/** @internal */
const TLID_REFLECTOR_PEER_INFO = "\x1C\x37\xD9\x27";
/** @internal */
const PROTO_ID = 'GrVP';
/** @internal */
const PROTOCOL_VERSION = 9;
/** @internal */
const MIN_PROTOCOL_VERSION = 9;
/** @internal */
const STREAM_TYPE_AUDIO = 1;
/** @internal */
const STREAM_TYPE_VIDEO = 2;
/** @internal */
const CODEC_OPUS = 'SUPO';
private MTProto $MadelineProto;
public array $received_timestamp_map = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
public array $remote_ack_timestamp_map = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
public int $session_out_seq_no = 0;
public int $session_in_seq_no = 0;
public int $voip_state = 0;
public array $configuration = ['endpoints' => [], 'shared_config' => []];
public array $storage = [];
public array $internalStorage = [];
private $signal = 0;
private ?int $callState = null;
private $callID;
private $creatorID;
private $otherID;
private $protocol;
private $visualization;
private $holdFiles = [];
private $inputFiles = [];
private $outputFile;
private $isPlaying = false;
protected MessageHandler $messageHandler;
protected VoIPState $voipState = VoIPState::CREATED;
protected CallState $callState;
private bool $creator;
protected readonly array $call;
private PermAuthKey $authKey;
private int $peerVersion = 0;
/** @var list<string> */
protected array $holdFiles = [];
/** @var list<string> */
protected array $inputFiles = [];
/**
* @var array<Endpoint>
*/
private array $sockets = [];
protected array $sockets = [];
protected ?Endpoint $bestEndpoint = null;
protected bool $pendingPing = true;
protected ?string $timeoutWatcher = null;
protected ?string $pingWatcher = null;
protected float $lastIncomingTimestamp = 0.0;
protected int $opusTimestamp = 0;
protected SplQueue $packetQueue;
protected array $tempHoldFiles = [];
private ?Endpoint $bestEndpoint = null;
/** Auth key */
protected readonly string $authKey;
/** Protocol call ID */
protected readonly string $protocolCallID;
private bool $pendingPing = true;
/**
* Timeout watcher.
*/
private ?string $timeoutWatcher = null;
/**
* Ping watcher.
*/
private ?string $pingWatcher = null;
/** Phone call ID */
public readonly int $callID;
/** Whether the call is an outgoing call */
public readonly bool $outgoing;
/** ID of the other user in the call */
public readonly int $otherID;
/** ID of the creator of the call */
public readonly int $creatorID;
/** When was the call created */
public readonly int $date;
/** @var ?list{string, string, string, string} */
protected ?array $visualization = null;
/**
* Last incoming timestamp.
* Constructor.
*
* @internal
*/
private float $lastIncomingTimestamp = 0.0;
/**
* The outgoing timestamp.
*
*/
private int $timestamp = 0;
/**
* Packet queue.
*
*/
private SplQueue $packetQueue;
/**
* Temporary holdfile array.
*/
private array $tempHoldFiles = [];
/**
* Sleep function.
*/
public function __sleep(): array
public function __construct(
protected readonly MTProto $API,
array $call
)
{
$vars = \get_object_vars($this);
unset($vars['sockets'], $vars['bestEndpoint'], $vars['timeoutWatcher']);
$call['_'] = 'inputPhoneCall';
$this->packetQueue = new SplQueue;
$this->call = $call;
$this->date = $call['date'];
$this->callID = $call['id'];
if ($call['_'] === 'phoneCallWaiting') {
$this->outgoing = false;
$this->otherID = $call['participant_id'];
$this->creatorID = $call['admin_id'];
$this->callState = CallState::INCOMING;
} else {
$this->outgoing = true;
$this->otherID = $call['admin_id'];
$this->creatorID = $call['participant_id'];
$this->callState = CallState::REQUESTED;
}
}
return \array_keys($vars);
/**
* Confirm requested call.
* @internal
*/
public function confirm(array $params): bool
{
if ($this->callState !== CallState::REQUESTED) {
$this->API->logger->logger(\sprintf(Lang::$current_lang['call_error_2'], $this->callID));
return false;
}
$this->API->logger->logger(\sprintf(Lang::$current_lang['call_confirming'], $this->otherID), Logger::VERBOSE);
$dh_config = $this->API->getDhConfig();
$params['g_b'] = new BigInteger((string) $params['g_b'], 256);
Crypt::checkG($params['g_b'], $dh_config['p']);
$key = \str_pad($params['g_b']->powMod($this->call['a'], $dh_config['p'])->toBytes(), 256, \chr(0), STR_PAD_LEFT);
try {
$res = ($this->API->methodCallAsyncRead('phone.confirmCall', ['key_fingerprint' => \substr(\sha1($key, true), -8), 'peer' => ['id' => $params['id'], 'access_hash' => $params['access_hash'], '_' => 'inputPhoneCall'], 'g_a' => $this->call['g_a'], 'protocol' => ['_' => 'phoneCallProtocol', 'udp_reflector' => true, 'min_layer' => 65, 'max_layer' => 92]]))['phone_call'];
} catch (RPCErrorException $e) {
if ($e->rpc === 'CALL_ALREADY_ACCEPTED') {
$this->API->logger->logger(\sprintf(Lang::$current_lang['call_already_accepted'], $params['id']));
return true;
}
if ($e->rpc === 'CALL_ALREADY_DECLINED') {
$this->API->logger->logger(Lang::$current_lang['call_already_declined']);
$this->discard(['_' => 'phoneCallDiscardReasonHangup']);
return false;
}
throw $e;
}
$visualization = [];
$length = new BigInteger(\count(Magic::$emojis));
foreach (\str_split(\hash('sha256', $key.\str_pad($this->call['g_a'], 256, \chr(0), STR_PAD_LEFT), true), 8) as $number) {
$number[0] = \chr(\ord($number[0]) & 0x7f);
$visualization[] = Magic::$emojis[(int) (new BigInteger($number, 256))->divide($length)[1]->toString()];
}
$this->visualization = $visualization;
$this->authKey = $key;
$this->callState = CallState::RUNNING;
$this->protocolCallID = \substr(\hash('sha256', $key, true), -16);
$this->initialize($res['connections']);
return true;
}
/**
* Accept incoming call.
*/
public function accept(): self
{
if ($this->callState === CallState::RUNNING || $this->callState === CallState::ENDED) {
return $this;
}
Assert::eq($this->callState->name, CallState::INCOMING->name);
$this->API->logger->logger(\sprintf(Lang::$current_lang['accepting_call'], $this->otherID), Logger::VERBOSE);
$dh_config = $this->API->getDhConfig();
$this->API->logger->logger('Generating b...', Logger::VERBOSE);
$b = BigInteger::randomRange(Magic::$two, $dh_config['p']->subtract(Magic::$two));
$g_b = $dh_config['g']->powMod($b, $dh_config['p']);
Crypt::checkG($g_b, $dh_config['p']);
try {
$this->API->methodCallAsyncRead('phone.acceptCall', ['peer' => ['id' => $this->call['id'], 'access_hash' => $this->call['access_hash'], '_' => 'inputPhoneCall'], 'g_b' => $g_b->toBytes(), 'protocol' => ['_' => 'phoneCallProtocol', 'udp_reflector' => true, 'udp_p2p' => true, 'min_layer' => 65, 'max_layer' => 92]]);
} catch (RPCErrorException $e) {
if ($e->rpc === 'CALL_ALREADY_ACCEPTED') {
$this->API->logger->logger(\sprintf(Lang::$current_lang['call_already_accepted'], $this->callID));
return $this;
}
if ($e->rpc === 'CALL_ALREADY_DECLINED') {
$this->API->logger->logger(Lang::$current_lang['call_already_declined']);
$this->discard(['_' => 'phoneCallDiscardReasonHangup']);
return $this;
}
throw $e;
}
$this->call['b'] = $b;
$this->callState = CallState::ACCEPTED;
return $this;
}
/**
* Complete call handshake.
*
* @internal
*/
public function complete(array $params): bool
{
if ($this->callState !== CallState::ACCEPTED) {
$this->API->logger->logger(\sprintf(Lang::$current_lang['call_error_3'], $params['id']));
return false;
}
$this->API->logger->logger(\sprintf(Lang::$current_lang['call_completing'], $this->otherID), Logger::VERBOSE);
$dh_config = $this->API->getDhConfig();
if (\hash('sha256', $params['g_a_or_b'], true) != $this->call['g_a_hash']) {
throw new SecurityException('Invalid g_a!');
}
$params['g_a_or_b'] = new BigInteger((string) $params['g_a_or_b'], 256);
Crypt::checkG($params['g_a_or_b'], $dh_config['p']);
$key = \str_pad($params['g_a_or_b']->powMod($this->call['b'], $dh_config['p'])->toBytes(), 256, \chr(0), STR_PAD_LEFT);
if (\substr(\sha1($key, true), -8) != $params['key_fingerprint']) {
throw new SecurityException(Lang::$current_lang['fingerprint_invalid']);
}
$visualization = [];
$length = new BigInteger(\count(Magic::$emojis));
foreach (\str_split(\hash('sha256', $key.\str_pad($params['g_a_or_b']->toBytes(), 256, \chr(0), STR_PAD_LEFT), true), 8) as $number) {
$number[0] = \chr(\ord($number[0]) & 0x7f);
$visualization[] = Magic::$emojis[(int) (new BigInteger($number, 256))->divide($length)[1]->toString()];
}
$this->visualization = $visualization;
$this->authKey = $key;
$this->callState = CallState::RUNNING;
$this->protocolCallID = \substr(\hash('sha256', $key, true), -16);
$this->initialize($params['connections']);
return true;
}
/**
* Wakeup function.
*/
public function __wakeup(): void
{
if ($this->voip_state === self::STATE_ESTABLISHED) {
$this->voip_state = self::STATE_CREATED;
$this->startTheMagic();
if ($this->callState === CallState::RUNNING) {
$this->startReadLoop();
if ($this->voipState === VoIPState::ESTABLISHED) {
$this->startWriteLoop();
}
}
/**
* Constructor.
*/
public function __construct(bool $creator, int $otherID, MTProto $MadelineProto, int $callState)
{
$this->creator = $creator;
$this->otherID = $otherID;
$this->MadelineProto = $MadelineProto;
$this->callState = $callState;
$this->packetQueue = new SplQueue;
}
/**
* Get max layer.
* Get call emojis (will return null if the call is not inited yet).
*
* @return ?list{string, string, string, string}
*/
public static function getConnectionMaxLayer(): int
{
return 92;
}
/**
* Get debug string.
*/
public function getDebugString(): string
{
return '';
}
/**
* Set call constructor.
*/
public function setCall(array $callID): void
{
$this->protocol = $callID['protocol'];
$this->callID = [
'_' => 'inputPhoneCall',
'id' => $callID['id'],
'access_hash' => $callID['access_hash'],
];
}
/**
* Set emojis.
*/
public function setVisualization(array $visualization): void
{
$this->visualization = $visualization;
}
/**
* Get emojis.
*/
public function getVisualization(): array
public function getVisualization(): ?array
{
return $this->visualization;
}
/**
* Discard call.
*
*/
public function discard(array $reason = ['_' => 'phoneCallDiscardReasonDisconnect'], array $rating = [], bool $debug = false): self
public function discard(array $reason = ['_' => 'phoneCallDiscardReasonDisconnect'], array $rating = []): self
{
if (($this->callState ?? self::CALL_STATE_ENDED) === self::CALL_STATE_ENDED || empty($this->configuration)) {
if ($this->callState === CallState::ENDED) {
return $this;
}
$this->callState = self::CALL_STATE_ENDED;
Logger::log("Now closing $this");
if (isset($this->timeoutWatcher)) {
EventLoop::cancel($this->timeoutWatcher);
@ -274,80 +343,67 @@ final class VoIP
}
Logger::log("Closed all sockets, discarding $this");
$this->MadelineProto->discardCall($this->callID, $reason, $rating, $debug);
return $this;
$this->API->logger->logger(\sprintf(Lang::$current_lang['call_discarding'], $this->callID), Logger::VERBOSE);
try {
$this->API->methodCallAsyncRead('phone.discardCall', ['peer' => $this->call, 'duration' => \time() - $this->calls[$call['id']]->whenCreated(), 'connection_id' => $this->calls[$call['id']]->getPreferredRelayID(), 'reason' => $reason]);
} catch (RPCErrorException $e) {
if (!\in_array($e->rpc, ['CALL_ALREADY_DECLINED', 'CALL_ALREADY_ACCEPTED'], true)) {
throw $e;
}
public function __destruct()
{
EventLoop::queue($this->discard(...), ['_' => 'phoneCallDiscardReasonDisconnect']);
}
/**
* Accept call.
*
*/
public function accept(): self|false
{
if ($this->callState !== self::CALL_STATE_INCOMING) {
return false;
if (!empty($rating)) {
$this->API->logger->logger(\sprintf('Setting rating for call %s...', $this->call), Logger::VERBOSE);
$this->API->methodCallAsyncRead('phone.setCallRating', ['peer' => $this->call, 'rating' => $rating['rating'], 'comment' => $rating['comment']]);
}
$this->callState = self::CALL_STATE_ACCEPTED;
$res = $this->MadelineProto->acceptCall($this->callID);
if (!$res) {
$this->discard(['_' => 'phoneCallDiscardReasonDisconnect']);
}
$this->API->cleanupCall($this->callID);
$this->callState = CallState::ENDED;
return $this;
}
/**
* Start the actual call.
* Connect to the specified endpoints.
*/
public function startTheMagic(): self
private function initialize(array $endpoints): void
{
if ($this->voip_state !== self::STATE_CREATED) {
return $this;
}
$this->voip_state = self::STATE_WAIT_INIT;
$this->timeoutWatcher = EventLoop::repeat(10, function (): void {
if (\microtime(true) - $this->lastIncomingTimestamp > 10) {
$this->discard(['_' => 'phoneCallDiscardReasonDisconnect']);
}
});
EventLoop::queue(function (): void {
$this->authKey = new PermAuthKey();
$this->authKey->setAuthKey($this->configuration['auth_key']);
foreach ($this->configuration['endpoints'] as $endpoint) {
foreach ($endpoints as $endpoint) {
try {
$this->sockets['v6 '.$endpoint['id']] = new Endpoint('['.$endpoint['ipv6'].']', $endpoint['port'], $endpoint['peer_tag'], true, $this);
$this->sockets['v6 '.$endpoint['id']] = new Endpoint(
'['.$endpoint['ipv6'].']',
$endpoint['port'],
$endpoint['peer_tag'],
true,
$this->outgoing,
$this->authKey,
$this->protocolCallID,
$this->messageHandler
);
} catch (Throwable) {
}
try {
$this->sockets['v4 '.$endpoint['id']] = new Endpoint($endpoint['ip'], $endpoint['port'], $endpoint['peer_tag'], true, $this);
$this->sockets['v4 '.$endpoint['id']] = new Endpoint(
$endpoint['ip'],
$endpoint['port'],
$endpoint['peer_tag'],
true,
$this->outgoing,
$this->authKey,
$this->protocolCallID,
$this->messageHandler
);
} catch (Throwable) {
}
}
$this->voipState = VoIPState::WAIT_INIT;
$this->startReadLoop();
foreach ($this->sockets as $socket) {
$socket->write($this->encryptPacket([
$socket->write($this->messageHandler->encryptPacket([
'_' => self::PKT_INIT,
'protocol' => self::PROTOCOL_VERSION,
'min_protocol' => self::MIN_PROTOCOL_VERSION,
'audio_streams' => [self::CODEC_OPUS],
'video_streams' => []
], false));
EventLoop::queue(function () use ($socket): void {
while ($payload = $socket->read()) {
$this->lastIncomingTimestamp = \microtime(true);
EventLoop::queue($this->handlePacket(...), $socket, $payload);
], true));
}
Logger::log("Exiting VoIP read loop in $this!");
});
}
});
return $this;
}
/**
* Handle incoming packet.
@ -356,28 +412,32 @@ final class VoIP
{
switch ($packet['_']) {
case self::PKT_INIT:
//$this->voip_state = self::STATE_WAIT_INIT_ACK;
$this->peerVersion = $packet['protocol'];
$socket->write(
$this->encryptPacket([
//$this->voipState = VoIPState::WAIT_INIT_ACK;
$socket->write($this->messageHandler->encryptPacket([
'_' => self::PKT_INIT_ACK,
'protocol' => self::PROTOCOL_VERSION,
'min_protocol' => self::MIN_PROTOCOL_VERSION,
'all_streams' => [
['id' => 0, 'type' => self::STREAM_TYPE_AUDIO, 'codec' => self::CODEC_OPUS, 'frame_duration' => 60, 'enabled' => 1]
]
])
);
]));
$socket->write($this->messageHandler->encryptPacket([
'_' => self::PKT_INIT,
'protocol' => self::PROTOCOL_VERSION,
'min_protocol' => self::MIN_PROTOCOL_VERSION,
'audio_streams' => [self::CODEC_OPUS],
'video_streams' => []
]));
break;
// no break
case self::PKT_INIT_ACK:
if (!$this->bestEndpoint) {
$this->bestEndpoint = $socket;
$this->pingWatcher = EventLoop::delay(1.0, function (): void {
$this->pendingPing = true;
foreach ($this->sockets as $socket) {
//$socket->udpPing();
$packet = $this->encryptPacket(['_' => self::PKT_PING]);
$socket->udpPing();
$packet = $this->messageHandler->encryptPacket(['_' => self::PKT_PING]);
EventLoop::queue(fn () => $socket->write($packet));
}
});
@ -393,6 +453,9 @@ final class VoIP
case self::PKT_STREAM_DATA_X3:
$cnt = 3;
break;
case self::PKT_PING:
$socket->write($this->messageHandler->encryptPacket(['_' => self::PKT_PONG, 'out_seq_no' => $packet['out_seq_no']]));
break;
case self::PKT_PONG:
if ($this->pendingPing) {
$this->pendingPing = false;
@ -402,16 +465,31 @@ final class VoIP
}
}
break;
default:
\var_dump($packet);
}
}
private function startReadLoop(): void
{
foreach ($this->sockets as $socket) {
EventLoop::queue(function () use ($socket): void {
while ($payload = $socket->read()) {
$this->lastIncomingTimestamp = \microtime(true);
EventLoop::queue($this->handlePacket(...), $socket, $payload);
}
Logger::log("Exiting VoIP read loop in $this!");
});
}
$this->timeoutWatcher = EventLoop::repeat(10, function (): void {
if (\microtime(true) - $this->lastIncomingTimestamp > 10) {
$this->discard(['_' => 'phoneCallDiscardReasonDisconnect']);
}
});
}
/**
* Start write loop.
*/
private function startWriteLoop(): void
{
$this->voip_state = self::STATE_ESTABLISHED;
$this->voipState = VoIPState::ESTABLISHED;
Logger::log("Call established, sending OPUS data!");
$this->tempHoldFiles = [];
@ -432,7 +510,7 @@ final class VoIP
}
$t = \microtime(true) + 0.060;
while (!$this->packetQueue->isEmpty()) {
$packet = $this->encryptPacket(['_' => self::PKT_STREAM_DATA, 'stream_id' => 0, 'data' => $this->packetQueue->dequeue(), 'timestamp' => $this->timestamp]);
$packet = $this->messageHandler->encryptPacket(['_' => self::PKT_STREAM_DATA, 'stream_id' => 0, 'data' => $this->packetQueue->dequeue(), 'timestamp' => $this->opusTimestamp]);
//Logger::log("Writing {$this->timestamp} in $this!");
$diff = $t - \microtime(true);
@ -442,7 +520,7 @@ final class VoIP
$this->bestEndpoint->write($packet);
$t += 0.060;
$this->timestamp += 60;
$this->opusTimestamp += 60;
}
}
}
@ -506,125 +584,19 @@ final class VoIP
return $this;
}
/**
* Set MadelineProto instance.
*/
public function setMadeline(MTProto $MadelineProto): void
{
$this->MadelineProto = $MadelineProto;
}
/**
* Get call protocol.
*/
public function getProtocol(): array
{
return $this->protocol;
}
/**
* Get ID of other user.
*/
public function getOtherID(): int
{
return $this->otherID;
}
/**
* Get call ID.
*
*/
public function getCallID(): string|int
{
return $this->callID;
}
/**
* Get creation date.
*
*/
public function whenCreated(): int|bool
{
return $this->internalStorage['created'] ?? false;
}
/**
* Parse config.
*/
public function parseConfig(): void
{
}
/**
* Get call state.
*/
public function getCallState(): int
public function getCallState(): CallState
{
return $this->callState ?? self::CALL_STATE_ENDED;
return $this->callState;
}
/**
* Get library version.
* Get VoIP state.
*/
public function getVersion(): string
public function getVoIPState(): VoIPState
{
return 'libponyvoip-1.0';
}
/**
* Get preferred relay ID.
*/
public function getPreferredRelayID(): int
{
return 0;
}
/**
* Get last error.
*/
public function getLastError(): string
{
return '';
}
/**
* Get debug log.
*/
public function getDebugLog(): string
{
return '';
}
/**
* Get signal bar count.
*/
public function getSignalBarsCount(): int
{
return $this->signal;
}
/**
* Get the value of creator.
*/
public function isCreator(): bool
{
return $this->creator;
}
/**
* Get the value of authKey.
*/
public function getAuthKey(): PermAuthKey
{
return $this->authKey;
}
/**
* Get the value of peerVersion.
*/
public function getPeerVersion(): int
{
return $this->peerVersion;
return $this->voipState;
}
/**
@ -632,7 +604,6 @@ final class VoIP
*/
public function __toString(): string
{
$id = $this->callID['id'];
return "call {$id} with {$this->otherID}";
return "call {$this->callID} with {$this->otherID}";
}
}

View File

@ -1,80 +0,0 @@
<?php
declare(strict_types=1);
/*
Copyright 2016-2018 Daniil Gentili
(https://daniil.it)
This file is part of MadelineProto.
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.
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.
See the GNU Affero General Public License for more details.
You should have received a copy of the GNU General Public License along with MadelineProto.
If not, see <http://www.gnu.org/licenses/>.
*/
namespace danog\MadelineProto\VoIP;
use danog\MadelineProto\Logger;
/**
* @internal
*/
trait AckHandler
{
private function seqgt(int $s1, int $s2): bool
{
return $s1 > $s2;
}
public function received_packet(int $last_ack_id, int $packet_seq_no, int $ack_mask): bool
{
if ($this->seqgt($packet_seq_no, $this->session_in_seq_no)) {
$diff = $packet_seq_no - $this->session_in_seq_no;
if ($diff > 31) {
$this->received_timestamp_map = \array_fill(0, 32, 0);
} else {
$remaining = 32-$diff;
for ($x = 0; $x < $remaining; $x++) {
$this->received_timestamp_map[$diff+$x] = $this->received_timestamp_map[$x];
}
for ($x = 1; $x < $diff; $x++) {
$this->received_timestamp_map[$x] = 0;
}
$this->received_timestamp_map[0] = \microtime(true);
}
$this->session_in_seq_no = $packet_seq_no;
} elseif (($diff = $this->session_in_seq_no - $packet_seq_no) < 32) {
if (!$this->received_timestamp_map[$diff]) {
Logger::log("Got duplicate $packet_seq_no");
return false;
}
$this->received_timestamp_map[$diff] = \microtime(true);
} else {
Logger::log("Packet $packet_seq_no is out of order and too late");
return false;
}
if ($this->seqgt($last_ack_id, $this->session_out_seq_no)) {
$diff = $last_ack_id - $this->session_out_seq_no;
if ($diff > 31) {
$this->remote_ack_timestamp_map = \array_fill(0, 32, 0);
} else {
$remaining = 32-$diff;
for ($x = 0; $x < $remaining; $x++) {
$this->remote_ack_timestamp_map[$diff+$x] = $this->remote_ack_timestamp_map[$x];
}
for ($x = 1; $x < $diff; $x++) {
$this->remote_ack_timestamp_map[$x] = 0;
}
$this->remote_ack_timestamp_map[0] = \microtime(true);
}
$this->session_out_seq_no = $last_ack_id;
for ($x = 1; $x < 32; $x++) {
if (!$this->remote_ack_timestamp_map[$x] && ($ack_mask >> 32-$x) & 1) {
$this->remote_ack_timestamp_map[$x] = \microtime(true);
}
}
}
return true;
}
}

View File

@ -20,6 +20,7 @@ declare(strict_types=1);
namespace danog\MadelineProto\VoIP;
use Amp\DeferredFuture;
use danog\MadelineProto\Exception;
use danog\MadelineProto\Lang;
use danog\MadelineProto\Logger;
@ -31,6 +32,7 @@ use danog\MadelineProto\RPCErrorException;
use danog\MadelineProto\SecurityException;
use danog\MadelineProto\VoIP;
use phpseclib3\Math\BigInteger;
use Throwable;
use const STR_PAD_LEFT;
@ -44,35 +46,9 @@ use const STR_PAD_LEFT;
*/
trait AuthKeyHandler
{
/** @var array<int, VoIP> */
private array $calls = [];
/**
* Accept call from VoIP instance.
*
* @param VoIP $instance Call instance
* @param array $user User
* @internal
*/
public function acceptCallFrom(VoIP $instance, array $user): ?VoIP
{
if (!$this->acceptCall($user)) {
$instance->discard();
return null;
}
return $instance;
}
/**
* @param VoIP $instance Call instance
* @param array $call Call info
* @param array $reason Discard reason
* @param array $rating Rating
* @param boolean $need_debug Needs debug?
* @internal
*/
public function discardCallFrom(VoIP $instance, array $call, array $reason, array $rating = [], bool $need_debug = true): VoIP
{
$this->discardCall($call, $reason, $rating, $need_debug);
return $instance;
}
private array $pendingCalls = [];
/**
* Request VoIP call.
*
@ -80,228 +56,52 @@ trait AuthKeyHandler
*/
public function requestCall(mixed $user)
{
if (!\class_exists('\\danog\\MadelineProto\\VoIP')) {
throw Exception::extension('libtgvoip');
}
$user = ($this->getInfo($user));
if (!isset($user['InputUser']) || $user['InputUser']['_'] === 'inputUserSelf') {
throw new PeerNotInDbException();
}
$user = $user['InputUser'];
$this->logger->logger(\sprintf('Calling %s...', $user['user_id']), Logger::VERBOSE);
$user = $user['bot_api_id'];
if (isset($this->pendingCalls[$user])) {
return $this->pendingCalls[$user]->await();
}
$deferred = new DeferredFuture;
$this->pendingCalls[$user] = $deferred->getFuture();
try {
$this->logger->logger(\sprintf('Calling %s...', $user), Logger::VERBOSE);
$dh_config = ($this->getDhConfig());
$this->logger->logger('Generating a...', Logger::VERBOSE);
$a = BigInteger::randomRange(Magic::$two, $dh_config['p']->subtract(Magic::$two));
$this->logger->logger('Generating g_a...', Logger::VERBOSE);
$g_a = $dh_config['g']->powMod($a, $dh_config['p']);
Crypt::checkG($g_a, $dh_config['p']);
$controller = new VoIP(true, $user['user_id'], $this, VoIP::CALL_STATE_REQUESTED);
$controller->storage = ['a' => $a, 'g_a' => \str_pad($g_a->toBytes(), 256, \chr(0), STR_PAD_LEFT)];
$res = $this->methodCallAsyncRead('phone.requestCall', ['user_id' => $user, 'g_a_hash' => \hash('sha256', $g_a->toBytes(), true), 'protocol' => ['_' => 'phoneCallProtocol', 'udp_p2p' => true, 'udp_reflector' => true, 'min_layer' => 65, 'max_layer' => VoIP::getConnectionMaxLayer()]]);
$controller->setCall($res['phone_call']);
$this->calls[$res['phone_call']['id']] = $controller;
$this->updaters[UpdateLoop::GENERIC]->resume();
return $controller;
$res = $this->methodCallAsyncRead('phone.requestCall', ['user_id' => $user, 'g_a_hash' => \hash('sha256', $g_a->toBytes(), true), 'protocol' => ['_' => 'phoneCallProtocol', 'udp_p2p' => true, 'udp_reflector' => true, 'min_layer' => 65, 'max_layer' => 92]])['phone_call'];
$res['a'] = $a;
$res['g_a'] = \str_pad($g_a->toBytes(), 256, \chr(0), STR_PAD_LEFT);
$this->calls[$res['id']] = $controller = new VoIP($this, $res);
unset($this->pendingCalls[$user]);
$deferred->complete($controller);
} catch (Throwable $e) {
unset($this->pendingCalls[$user]);
$deferred->error($e);
}
/**
* Accept call.
*
* @param array $call Call
*/
public function acceptCall(array $call): bool
{
if (!\class_exists('\\danog\\MadelineProto\\VoIP')) {
throw new Exception();
}
if ($this->callStatus($call['id']) !== VoIP::CALL_STATE_ACCEPTED) {
$this->logger->logger(\sprintf(Lang::$current_lang['call_error_1'], $call['id']));
return false;
}
$this->logger->logger(\sprintf(Lang::$current_lang['accepting_call'], $this->calls[$call['id']]->getOtherID()), Logger::VERBOSE);
$dh_config = ($this->getDhConfig());
$this->logger->logger('Generating b...', Logger::VERBOSE);
$b = BigInteger::randomRange(Magic::$two, $dh_config['p']->subtract(Magic::$two));
$g_b = $dh_config['g']->powMod($b, $dh_config['p']);
Crypt::checkG($g_b, $dh_config['p']);
try {
$res = $this->methodCallAsyncRead('phone.acceptCall', ['peer' => ['id' => $call['id'], 'access_hash' => $call['access_hash'], '_' => 'inputPhoneCall'], 'g_b' => $g_b->toBytes(), 'protocol' => ['_' => 'phoneCallProtocol', 'udp_reflector' => true, 'udp_p2p' => true, 'min_layer' => 65, 'max_layer' => VoIP::getConnectionMaxLayer()]]);
} catch (RPCErrorException $e) {
if ($e->rpc === 'CALL_ALREADY_ACCEPTED') {
$this->logger->logger(\sprintf(Lang::$current_lang['call_already_accepted'], $call['id']));
return true;
}
if ($e->rpc === 'CALL_ALREADY_DECLINED') {
$this->logger->logger(Lang::$current_lang['call_already_declined']);
$this->discardCall($call['id'], ['_' => 'phoneCallDiscardReasonHangup']);
return false;
}
throw $e;
}
$this->calls[$res['phone_call']['id']]->storage['b'] = $b;
$this->updaters[UpdateLoop::GENERIC]->resume();
return true;
}
/**
* Confirm call.
*
* @param array $params Params
*/
public function confirmCall(array $params)
{
if (!\class_exists('\\danog\\MadelineProto\\VoIP')) {
throw Exception::extension('libtgvoip');
}
if ($this->callStatus($params['id']) !== VoIP::CALL_STATE_REQUESTED) {
$this->logger->logger(\sprintf(Lang::$current_lang['call_error_2'], $params['id']));
return false;
}
$this->logger->logger(\sprintf(Lang::$current_lang['call_confirming'], $this->calls[$params['id']]->getOtherID()), Logger::VERBOSE);
$dh_config = ($this->getDhConfig());
$params['g_b'] = new BigInteger((string) $params['g_b'], 256);
Crypt::checkG($params['g_b'], $dh_config['p']);
$key = \str_pad($params['g_b']->powMod($this->calls[$params['id']]->storage['a'], $dh_config['p'])->toBytes(), 256, \chr(0), STR_PAD_LEFT);
try {
$res = ($this->methodCallAsyncRead('phone.confirmCall', ['key_fingerprint' => \substr(\sha1($key, true), -8), 'peer' => ['id' => $params['id'], 'access_hash' => $params['access_hash'], '_' => 'inputPhoneCall'], 'g_a' => $this->calls[$params['id']]->storage['g_a'], 'protocol' => ['_' => 'phoneCallProtocol', 'udp_reflector' => true, 'min_layer' => 65, 'max_layer' => VoIP::getConnectionMaxLayer()]]))['phone_call'];
} catch (RPCErrorException $e) {
if ($e->rpc === 'CALL_ALREADY_ACCEPTED') {
$this->logger->logger(\sprintf(Lang::$current_lang['call_already_accepted'], $params['id']));
return true;
}
if ($e->rpc === 'CALL_ALREADY_DECLINED') {
$this->logger->logger(Lang::$current_lang['call_already_declined']);
$this->discardCall($params['id'], ['_' => 'phoneCallDiscardReasonHangup']);
return false;
}
throw $e;
}
$this->calls[$params['id']]->setCall($res);
$visualization = [];
$length = new BigInteger(\count(Magic::$emojis));
foreach (\str_split(\hash('sha256', $key.\str_pad($this->calls[$params['id']]->storage['g_a'], 256, \chr(0), STR_PAD_LEFT), true), 8) as $number) {
$number[0] = \chr(\ord($number[0]) & 0x7f);
$visualization[] = Magic::$emojis[(int) (new BigInteger($number, 256))->divide($length)[1]->toString()];
}
$this->calls[$params['id']]->setVisualization($visualization);
$this->calls[$params['id']]->configuration['endpoints'] = \array_merge($res['connections'], $this->calls[$params['id']]->configuration['endpoints']);
$this->calls[$params['id']]->configuration = \array_merge(['recv_timeout' => $this->config['call_receive_timeout_ms'] / 1000, 'init_timeout' => $this->config['call_connect_timeout_ms'] / 1000, 'data_saving' => VoIP::DATA_SAVING_NEVER, 'enable_NS' => true, 'enable_AEC' => true, 'enable_AGC' => true, 'auth_key' => $key, 'auth_key_id' => \substr(\sha1($key, true), -8), 'call_id' => \substr(\hash('sha256', $key, true), -16), 'network_type' => VoIP::NET_TYPE_ETHERNET], $this->calls[$params['id']]->configuration);
$this->calls[$params['id']]->parseConfig();
return $this->calls[$params['id']]->startTheMagic();
}
/**
* Complete call handshake.
*
* @param array $params Params
*/
public function completeCall(array $params)
{
if (!\class_exists('\\danog\\MadelineProto\\VoIP')) {
throw Exception::extension('libtgvoip');
}
if ($this->callStatus($params['id']) !== VoIP::CALL_STATE_ACCEPTED || !isset($this->calls[$params['id']]->storage['b'])) {
$this->logger->logger(\sprintf(Lang::$current_lang['call_error_3'], $params['id']));
return false;
}
$this->logger->logger(\sprintf(Lang::$current_lang['call_completing'], $this->calls[$params['id']]->getOtherID()), Logger::VERBOSE);
$dh_config = ($this->getDhConfig());
if (\hash('sha256', $params['g_a_or_b'], true) != $this->calls[$params['id']]->storage['g_a_hash']) {
throw new SecurityException('Invalid g_a!');
}
$params['g_a_or_b'] = new BigInteger((string) $params['g_a_or_b'], 256);
Crypt::checkG($params['g_a_or_b'], $dh_config['p']);
$key = \str_pad($params['g_a_or_b']->powMod($this->calls[$params['id']]->storage['b'], $dh_config['p'])->toBytes(), 256, \chr(0), STR_PAD_LEFT);
if (\substr(\sha1($key, true), -8) != $params['key_fingerprint']) {
throw new SecurityException(Lang::$current_lang['fingerprint_invalid']);
}
$visualization = [];
$length = new BigInteger(\count(Magic::$emojis));
foreach (\str_split(\hash('sha256', $key.\str_pad($params['g_a_or_b']->toBytes(), 256, \chr(0), STR_PAD_LEFT), true), 8) as $number) {
$number[0] = \chr(\ord($number[0]) & 0x7f);
$visualization[] = Magic::$emojis[(int) (new BigInteger($number, 256))->divide($length)[1]->toString()];
}
$this->calls[$params['id']]->setVisualization($visualization);
$this->calls[$params['id']]->configuration['endpoints'] = \array_merge($params['connections'], $this->calls[$params['id']]->configuration['endpoints']);
$this->calls[$params['id']]->configuration = \array_merge(['recv_timeout' => $this->config['call_receive_timeout_ms'] / 1000, 'init_timeout' => $this->config['call_connect_timeout_ms'] / 1000, 'data_saving' => VoIP::DATA_SAVING_NEVER, 'enable_NS' => true, 'enable_AEC' => true, 'enable_AGC' => true, 'auth_key' => $key, 'auth_key_id' => \substr(\sha1($key, true), -8), 'call_id' => \substr(\hash('sha256', $key, true), -16), 'network_type' => VoIP::NET_TYPE_ETHERNET], $this->calls[$params['id']]->configuration);
$this->calls[$params['id']]->parseConfig();
return $this->calls[$params['id']]->startTheMagic();
return $deferred->getFuture()->await();
}
/**
* Get call status.
*
* @param int $id Call ID
*/
public function callStatus(int $id): int
public function callStatus(int $id): ?CallState
{
if (!\class_exists('\\danog\\MadelineProto\\VoIP')) {
throw Exception::extension('libtgvoip');
}
if (isset($this->calls[$id])) {
return $this->calls[$id]->getCallState();
}
return VoIP::CALL_STATE_NONE;
}
/**
* Get call info.
*
* @param int $call Call ID
*/
public function getCall(int $call): array
{
if (!\class_exists('\\danog\\MadelineProto\\VoIP')) {
throw Exception::extension('libtgvoip');
}
return $this->calls[$call];
}
/**
* Discard call.
*
* @param array $call Call
* @param array $rating Rating
* @param boolean $need_debug Need debug?
*/
public function discardCall(array $call, array $reason, array $rating = [], bool $need_debug = true): ?VoIP
{
if (!\class_exists('\\danog\\MadelineProto\\VoIP')) {
throw Exception::extension('libtgvoip');
}
if (!isset($this->calls[$call['id']])) {
return null;
}
$this->logger->logger(\sprintf(Lang::$current_lang['call_discarding'], $call['id']), Logger::VERBOSE);
try {
$res = $this->methodCallAsyncRead('phone.discardCall', ['peer' => $call, 'duration' => \time() - $this->calls[$call['id']]->whenCreated(), 'connection_id' => $this->calls[$call['id']]->getPreferredRelayID(), 'reason' => $reason]);
} catch (RPCErrorException $e) {
if (!\in_array($e->rpc, ['CALL_ALREADY_DECLINED', 'CALL_ALREADY_ACCEPTED'], true)) {
throw $e;
}
}
if (!empty($rating)) {
$this->logger->logger(\sprintf('Setting rating for call %s...', $call['id']), Logger::VERBOSE);
$this->methodCallAsyncRead('phone.setCallRating', ['peer' => $call, 'rating' => $rating['rating'], 'comment' => $rating['comment']]);
}
if ($need_debug && isset($this->calls[$call['id']])) {
$this->logger->logger(\sprintf('Saving debug data for call %s...', $call['id']), Logger::VERBOSE);
$this->methodCallAsyncRead('phone.saveCallDebug', ['peer' => $call, 'debug' => $this->calls[$call['id']]->getDebugLog()]);
}
if (!isset($this->calls[$call['id']])) {
return null;
}
$c = $this->calls[$call['id']];
unset($this->calls[$call['id']]);
return $c;
}
/**
* Check state of calls.
*
* @internal
*/
public function checkCalls(): void
{
\array_walk($this->calls, function ($controller, $id): void {
if ($controller->getCallState() === VoIP::CALL_STATE_ENDED) {
$this->logger('Discarding ended call...');
$controller->discard();
/** @internal */
public function cleanupCall(int $id): void {
unset($this->calls[$id]);
}
});
}
}

37
src/VoIP/CallState.php Normal file
View File

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
/**
* Call state module.
*
* This file is part of MadelineProto.
* 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.
* 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.
* See the GNU Affero General Public License for more details.
* You should have received a copy of the GNU General Public License along with MadelineProto.
* If not, see <http://www.gnu.org/licenses/>.
*
* @author Daniil Gentili <daniil@daniil.it>
* @copyright 2016-2023 Daniil Gentili <daniil@daniil.it>
* @license https://opensource.org/licenses/AGPL-3.0 AGPLv3
* @link https://docs.madelineproto.xyz MadelineProto documentation
*/
namespace danog\MadelineProto\VoIP;
enum CallState {
/** The call was requested */
case REQUESTED;
/** An incoming call */
case INCOMING;
/** The call was accepted */
case ACCEPTED;
/** The call was confirmed */
case CONFIRMED;
/** The call is ongoing */
case RUNNING;
/** The call has ended */
case ENDED;
}

View File

@ -5,67 +5,48 @@ declare(strict_types=1);
namespace danog\MadelineProto\VoIP;
use Amp\Socket\Socket;
use danog\MadelineProto\Lang;
use danog\MadelineProto\Logger;
use danog\MadelineProto\MTProto\PermAuthKey;
use danog\MadelineProto\MTProtoTools\Crypt;
use danog\MadelineProto\Tools;
use danog\MadelineProto\VoIP;
use Exception;
use function Amp\Socket\connect;
final class Endpoint
{
/**
* IP address.
*/
private string $ip;
/**
* Port.
*/
private int $port;
/**
* Peer tag.
*/
private string $peerTag;
/**
* Whether we're a reflector.
*/
private bool $reflector;
/**
* Call instance.
*/
private VoIP $instance;
/**
* The socket.
*/
private ?Socket $socket = null;
/**
* Whether we're the creator.
*/
private bool $creator;
/**
* The auth key.
*/
private PermAuthKey $authKey;
/**
* Create endpoint.
*/
public function __construct(string $ip, int $port, string $peerTag, bool $reflector, VoIP $instance)
public function __construct(
private readonly string $ip,
private readonly int $port,
private readonly string $peerTag,
private readonly bool $reflector,
private readonly bool $creator,
private readonly string $authKey,
private readonly string $callID,
private readonly MessageHandler $handler
)
{
$this->ip = $ip;
$this->port = $port;
$this->peerTag = $peerTag;
$this->reflector = $reflector;
$this->instance = $instance;
$this->creator = $instance->isCreator();
$this->authKey = $instance->getAuthKey();
$this->socket = connect("udp://{$this->ip}:{$this->port}");
}
public function __wakeup()
{
$this->socket = connect("udp://{$this->ip}:{$this->port}");
}
public function __sleep(): array
{
$vars = get_object_vars($this);
unset($vars['socket']);
return $vars;
}
public function __toString(): string
{
@ -109,21 +90,21 @@ final class Endpoint
*/
public function read(): ?array
{
do {
$packet = $this->socket->read();
if ($packet === null) {
return null;
}
do {
$payload = \fopen('php://memory', 'rw+b');
\fwrite($payload, $packet);
\fseek($payload, 0);
$pos = 0;
if ($this->instance->getPeerVersion() < 9 || $this->reflector) {
/*if (\fread($payload, 16) !== $this->peerTag) {
if ($this->handler->peerVersion < 9 || $this->reflector) {
if (\fread($payload, 16) !== $this->peerTag) {
Logger::log('Received packet has wrong peer tag', Logger::ERROR);
continue;
}*/
}
$pos = 16;
}
if (\fread($payload, 12) === "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF") {
@ -149,11 +130,11 @@ final class Endpoint
} else {
fseek($payload, $pos);
$message_key = \fread($payload, 16);
[$aes_key, $aes_iv] = Crypt::aesCalculate($message_key, $this->authKey->getAuthKey(), !$this->creator);
[$aes_key, $aes_iv] = Crypt::aesCalculate($message_key, $this->authKey, !$this->creator);
$encrypted_data = \stream_get_contents($payload);
$packet = Crypt::igeDecrypt($encrypted_data, $aes_key, $aes_iv);
if ($message_key != \substr(\hash('sha256', \substr($this->authKey->getAuthKey(), 88 + ($this->creator ? 8 : 0), 32).$packet, true), 8, 16)) {
if ($message_key != \substr(\hash('sha256', \substr($this->authKey, 88 + ($this->creator ? 8 : 0), 32).$packet, true), 8, 16)) {
Logger::log('msg_key mismatch!', Logger::ERROR);
continue;
}
@ -177,7 +158,7 @@ final class Endpoint
$flags = \unpack('V', \stream_get_contents($payload, 4))[1];
$result['_'] = $flags >> 24;
if ($flags & 4) {
if (\stream_get_contents($payload, 16) !== $this->instance->configuration['call_id']) {
if (\stream_get_contents($payload, 16) !== $this->callID) {
Logger::log('Call ID mismatch', Logger::ERROR);
continue 2;
}
@ -220,8 +201,8 @@ final class Endpoint
break;
default:
if ($this->instance->getPeerVersion() >= 8 || (!$this->instance->getPeerVersion())) {
\fseek($payload, -4, SEEK_CUR);
if ($this->handler->peerVersion >= 8 || (!$this->handler->peerVersion)) {
\fseek($payload, 0);
$result['_'] = \ord(\stream_get_contents($payload, 1));
$in_seq_no = \unpack('V', \stream_get_contents($payload, 4))[1];
$out_seq_no = \unpack('V', \stream_get_contents($payload, 4))[1];
@ -244,8 +225,8 @@ final class Endpoint
continue 2;
}
}
if (!$this->instance->received_packet($in_seq_no, $out_seq_no, $ack_mask)) {
return $this->read();
if (!$this->handler->shouldSkip($in_seq_no, $out_seq_no, $ack_mask)) {
continue;
}
switch ($result['_']) {
// streamTypeSimple codec:int8 = StreamType;
@ -261,6 +242,7 @@ final class Endpoint
for ($x = 0; $x < $length; $x++) {
$result['audio_streams'][$x] = \stream_get_contents($message, 4);
}
$this->handler->peerVersion = $result['protocol'];
break;
// streamType id:int8 type:int8 codec:int8 frame_duration:int16 enabled:int8 = StreamType;
//
@ -296,10 +278,11 @@ final class Endpoint
$result['timestamp'] = \unpack('V', \stream_get_contents($message, 4))[1];
$result['data'] = \stream_get_contents($message, $length);
break;
/*case \danog\MadelineProto\VoIP::PKT_UPDATE_STREAMS:
break;
case \danog\MadelineProto\VoIP::PKT_UPDATE_STREAMS:
continue 2;
case \danog\MadelineProto\VoIP::PKT_PING:
break;*/
$result['out_seq_no'] = $out_seq_no;
break;
case VoIP::PKT_PONG:
if (\fstat($payload)['size'] - \ftell($payload)) {
$result['out_seq_no'] = \unpack('V', \stream_get_contents($payload, 4))[1];
@ -365,11 +348,11 @@ final class Endpoint
$padding += 16;
}
$plaintext .= Tools::random($padding);
$message_key = \substr(\hash('sha256', \substr($this->authKey->getAuthKey(), 88 + ($this->creator ? 0 : 8), 32).$plaintext, true), 8, 16);
[$aes_key, $aes_iv] = Crypt::aesCalculate($message_key, $this->authKey->getAuthKey(), $this->creator);
$message_key = \substr(\hash('sha256', \substr($this->authKey, 88 + ($this->creator ? 0 : 8), 32).$plaintext, true), 8, 16);
[$aes_key, $aes_iv] = Crypt::aesCalculate($message_key, $this->authKey, $this->creator);
$payload = $message_key.Crypt::igeEncrypt($plaintext, $aes_key, $aes_iv);
if ($this->instance->getPeerVersion() < 9 || $this->reflector) {
if ($this->handler->peerVersion < 9 || $this->reflector) {
$payload = $this->peerTag.$payload;
}
@ -384,11 +367,4 @@ final class Endpoint
$this->socket->write($this->peerTag.Tools::packSignedLong(-1).Tools::packSignedInt(-1).Tools::packSignedInt(-2).Tools::random(8));
return true;
}
/**
* Get peer tag.
*/
public function getPeerTag(): string
{
return $this->peerTag;
}
}

View File

@ -15,6 +15,7 @@ If not, see <http://www.gnu.org/licenses/>.
namespace danog\MadelineProto\VoIP;
use danog\MadelineProto\Logger;
use danog\MadelineProto\Tools;
use danog\MadelineProto\VoIP;
@ -23,8 +24,22 @@ use danog\MadelineProto\VoIP;
*
* @internal
*/
trait MessageHandler
final class MessageHandler
{
private array $received_timestamp_map = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
private array $remote_ack_timestamp_map = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
private int $session_out_seq_no = 0;
private int $session_in_seq_no = 0;
public int $peerVersion = 0;
public function __construct(
private readonly VoIP $instance,
private readonly string $callID
)
{
}
private static function pack_string(string $object): string
{
$l = \strlen($object);
@ -42,7 +57,7 @@ trait MessageHandler
return $concat;
}
private function encryptPacket(array $args, bool $increase_seqno = true): string
public function encryptPacket(array $args, bool $init = false): string
{
$message = '';
switch ($args['_']) {
@ -167,11 +182,11 @@ trait MessageHandler
if ($this->peerVersion >= 8 || (!$this->peerVersion)) {
$payload = \chr($args['_']);
$payload .= Tools::packUnsignedInt($this->session_in_seq_no);
$payload .= Tools::packUnsignedInt($this->session_out_seq_no);
$payload .= Tools::packUnsignedInt($init ? 0 : $this->session_out_seq_no);
$payload .= Tools::packUnsignedInt($ack_mask);
$payload .= \chr(0);
$payload .= $message;
} elseif (\in_array($this->voip_state, [VoIP::STATE_WAIT_INIT, VoIP::STATE_WAIT_INIT_ACK], true)) {
} elseif (\in_array($this->instance->getVoIPState(), [VoIPState::WAIT_INIT, VoIPState::WAIT_INIT_ACK], true)) {
$payload = VoIP::TLID_DECRYPTED_AUDIO_BLOCK;
$payload .= Tools::random(8);
$payload .= \chr(7);
@ -185,9 +200,9 @@ trait MessageHandler
$flags = \strlen($message) ? $flags | 1 : $flags & ~1; // raw_data
$flags = $flags | ($args['_'] << 24);
$payload .= Tools::packUnsignedInt($flags);
$payload .= $this->configuration['call_id'];
$payload .= $this->callID;
$payload .= Tools::packUnsignedInt($this->session_in_seq_no);
$payload .= Tools::packUnsignedInt($this->session_out_seq_no);
$payload .= Tools::packUnsignedInt($init ? 0 : $this->session_out_seq_no);
$payload .= Tools::packUnsignedInt($ack_mask);
$payload .= VoIP::PROTO_ID;
if ($flags & 2) {
@ -201,14 +216,67 @@ trait MessageHandler
$payload .= Tools::random(8);
$payload .= \chr(7);
$payload .= Tools::random(7);
$message = \chr($args['_']).Tools::packUnsignedInt($this->session_in_seq_no).Tools::packUnsignedInt($this->session_out_seq_no).Tools::packUnsignedInt($ack_mask).$message;
$message = \chr($args['_']).Tools::packUnsignedInt($this->session_in_seq_no).Tools::packUnsignedInt($init ? 0 : $this->session_out_seq_no).Tools::packUnsignedInt($ack_mask).$message;
$payload .= $this->pack_string($message);
}
if ($increase_seqno) {
if (!$init) {
$this->session_out_seq_no++;
}
return $payload;
}
public function shouldSkip(int $last_ack_id, int $packet_seq_no, int $ack_mask): bool
{
if ($packet_seq_no > $this->session_in_seq_no) {
$diff = $packet_seq_no - $this->session_in_seq_no;
if ($diff > 31) {
$this->received_timestamp_map = \array_fill(0, 32, 0);
} else {
$remaining = 32-$diff;
for ($x = 0; $x < $remaining; $x++) {
$this->received_timestamp_map[$diff+$x] = $this->received_timestamp_map[$x];
}
for ($x = 1; $x < $diff; $x++) {
$this->received_timestamp_map[$x] = 0;
}
$this->received_timestamp_map[0] = \microtime(true);
}
$this->session_in_seq_no = $packet_seq_no;
} elseif (($diff = $this->session_in_seq_no - $packet_seq_no) < 32) {
if (!$this->received_timestamp_map[$diff]) {
Logger::log("Got duplicate $packet_seq_no");
return false;
}
$this->received_timestamp_map[$diff] = \microtime(true);
} else {
Logger::log("Packet $packet_seq_no is out of order and too late");
return false;
}
if ($last_ack_id > $this->session_out_seq_no) {
$diff = $last_ack_id - $this->session_out_seq_no;
if ($diff > 31) {
$this->remote_ack_timestamp_map = \array_fill(0, 32, 0);
} else {
$remaining = 32-$diff;
for ($x = 0; $x < $remaining; $x++) {
$this->remote_ack_timestamp_map[$diff+$x] = $this->remote_ack_timestamp_map[$x];
}
for ($x = 1; $x < $diff; $x++) {
$this->remote_ack_timestamp_map[$x] = 0;
}
$this->remote_ack_timestamp_map[0] = \microtime(true);
}
$this->session_out_seq_no = $last_ack_id;
for ($x = 1; $x < 32; $x++) {
if (!$this->remote_ack_timestamp_map[$x] && ($ack_mask >> 32-$x) & 1) {
$this->remote_ack_timestamp_map[$x] = \microtime(true);
}
}
}
return true;
}
}

33
src/VoIP/VoIPState.php Normal file
View File

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
/**
* Call state module.
*
* This file is part of MadelineProto.
* 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.
* 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.
* See the GNU Affero General Public License for more details.
* You should have received a copy of the GNU General Public License along with MadelineProto.
* If not, see <http://www.gnu.org/licenses/>.
*
* @author Daniil Gentili <daniil@daniil.it>
* @copyright 2016-2023 Daniil Gentili <daniil@daniil.it>
* @license https://opensource.org/licenses/AGPL-3.0 AGPLv3
* @link https://docs.madelineproto.xyz MadelineProto documentation
*/
namespace danog\MadelineProto\VoIP;
/**
* VoIP protcol state.
*/
enum VoIPState {
case CREATED;
case WAIT_INIT;
case WAIT_INIT_ACK;
case ESTABLISHED;
case FAILED;
case RECONNECTING;
}