Implement secret chat feeder loop

This commit is contained in:
Daniil Gentili 2020-10-18 18:08:39 +02:00
parent f03705e4aa
commit 73384f0bb4
Signed by: danog
GPG Key ID: 8C1BE3B34B230CA7
9 changed files with 254 additions and 74 deletions

View File

@ -0,0 +1,113 @@
<?php
/**
* Update feeder loop.
*
* 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-2020 Daniil Gentili <daniil@daniil.it>
* @license https://opensource.org/licenses/AGPL-3.0 AGPLv3
*
* @link https://docs.madelineproto.xyz MadelineProto documentation
*/
namespace danog\MadelineProto\Loop\Update;
use danog\Loop\ResumableSignalLoop;
use danog\MadelineProto\Loop\InternalLoop;
use danog\MadelineProto\MTProto;
use danog\MadelineProto\SecurityException;
/**
* Secret feed loop.
*
* @author Daniil Gentili <daniil@daniil.it>
*/
class SecretFeedLoop extends ResumableSignalLoop
{
use InternalLoop {
__construct as private init;
}
/**
* Incoming secret updates array.
*/
private array $incomingUpdates = [];
/**
* Secret chat ID.
*/
private int $secretId;
/**
* Constructor.
*
* @param MTProto $API API instance
* @param integer $secretId Secret chat ID
*/
public function __construct(MTProto $API, int $secretId)
{
$this->init($API);
$this->secretId = $secretId;
}
/**
* Main loop.
*
* @return \Generator
*/
public function loop(): \Generator
{
$API = $this->API;
while (!$API->hasAllAuth()) {
if (yield $this->waitSignal($this->pause())) {
return;
}
}
while (true) {
while (!$API->hasAllAuth()) {
if (yield $this->waitSignal($this->pause())) {
return;
}
}
if (yield $this->waitSignal($this->pause())) {
return;
}
$API->logger->logger("Resumed {$this}");
while ($this->incomingUpdates) {
$updates = $this->incomingUpdates;
$this->incomingUpdates = [];
foreach ($updates as $update) {
try {
if (!yield from $API->handleEncryptedUpdate($update)) {
$API->logger->logger("Secret chat deleted, exiting $this...");
unset($API->secretFeeders[$this->secretId]);
return;
}
} catch (SecurityException $e) {
$API->logger->logger("Secret chat deleted, exiting $this...");
unset($API->secretFeeders[$this->secretId]);
throw $e;
}
}
$updates = null;
}
}
}
/**
* Feed incoming update to loop.
*
* @param array $update
* @return void
*/
public function feed(array $update): void
{
$this->incomingUpdates []= $update;
}
public function __toString(): string
{
return "secret chat feed loop {$this->secretId}";
}
}

View File

@ -34,6 +34,7 @@ use danog\MadelineProto\Db\MemoryArray;
use danog\MadelineProto\Ipc\Server;
use danog\MadelineProto\Loop\Generic\PeriodicLoopInternal;
use danog\MadelineProto\Loop\Update\FeedLoop;
use danog\MadelineProto\Loop\Update\SecretFeedLoop;
use danog\MadelineProto\Loop\Update\SeqLoop;
use danog\MadelineProto\Loop\Update\UpdateLoop;
use danog\MadelineProto\MTProtoTools\CombinedUpdatesState;
@ -394,6 +395,12 @@ class MTProto extends AsyncConstruct implements TLCallback
* @var array<\danog\MadelineProto\Loop\Update\FeedLoop>
*/
public $feeders = [];
/**
* Secret chat feeder loops.
*
* @var array<\danog\MadelineProto\Loop\Update\SecretFeedLoop>
*/
public $secretFeeders = [];
/**
* Updater loops.
*
@ -1563,6 +1570,14 @@ class MTProto extends AsyncConstruct implements TLCallback
return;
}
$this->logger("Starting update system");
foreach ($this->secret_chats as $id => $chat) {
if (!isset($this->secretFeeders[$id])) {
$this->secretFeeders[$id] = new SecretFeedLoop($this, $id);
}
if ($this->secretFeeders[$id]->start() && isset($this->secretFeeders[$id])) {
$this->secretFeeders[$id]->resume();
}
}
if (!isset($this->seqUpdater)) {
$this->seqUpdater = new SeqLoop($this);
}

View File

@ -382,7 +382,7 @@ trait ResponseHandler
$seconds = \preg_replace('/[^0-9]+/', '', $response['error_message']);
$limit = $request->getFloodWaitLimit() ?? $this->API->settings->getRPC()->getFloodTimeout();
if (\is_numeric($seconds) && $seconds < $limit) {
$this->logger->logger("Flood, waiting '.$seconds.' seconds before repeating async call of $request...", Logger::NOTICE);
$this->logger->logger("Flood, waiting $seconds seconds before repeating async call of $request...", Logger::NOTICE);
$request->setSent(($request->getSent() ?? \time()) + $seconds);
Loop::delay($seconds * 1000, [$this, 'methodRecall'], ['message_id' => $request->getMsgId()]);
return null;

View File

@ -365,7 +365,12 @@ trait UpdateHandler
$this->logger->logger('Applying qts: '.$update['qts'].' over current qts '.$cur_state->qts().', chat id: '.$update['message']['chat_id'], \danog\MadelineProto\Logger::VERBOSE);
yield from $this->methodCallAsyncRead('messages.receivedQueue', ['max_qts' => $cur_state->qts($update['qts'])], $this->settings->getDefaultDcParams());
}
yield from $this->handleEncryptedUpdate($update);
if (!isset($this->secret_chats[$update['message']['chat_id']])) {
$this->logger->logger(\sprintf(\danog\MadelineProto\Lang::$current_lang['secret_chat_skipping'], $update['message']['chat_id']));
return false;
}
$this->secretFeeders[$update['message']['chat_id']]->feed($update);
$this->secretFeeders[$update['message']['chat_id']]->resume();
return;
}
/*

View File

@ -19,6 +19,7 @@
namespace danog\MadelineProto\SecretChats;
use danog\MadelineProto\Loop\Update\SecretFeedLoop;
use danog\MadelineProto\Loop\Update\UpdateLoop;
use danog\MadelineProto\MTProto;
use danog\MadelineProto\MTProtoTools\Crypt;
@ -67,7 +68,34 @@ trait AuthKeyHandler
$key['fingerprint'] = \substr(\sha1($key['auth_key'], true), -8);
$key['visualization_orig'] = \substr(\sha1($key['auth_key'], true), 16);
$key['visualization_46'] = \substr(\hash('sha256', $key['auth_key'], true), 20);
$this->secret_chats[$params['id']] = ['key' => $key, 'admin' => false, 'user_id' => $params['admin_id'], 'InputEncryptedChat' => ['_' => 'inputEncryptedChat', 'chat_id' => $params['id'], 'access_hash' => $params['access_hash']], 'in_seq_no_x' => 1, 'out_seq_no_x' => 0, 'in_seq_no' => 0, 'out_seq_no' => 0, 'layer' => 8, 'ttl' => 0, 'ttr' => 100, 'updated' => \time(), 'incoming' => [], 'outgoing' => [], 'created' => \time(), 'rekeying' => [0], 'key_x' => 'from server', 'mtproto' => 1];
$this->secret_chats[$params['id']] = [
'key' => $key,
'admin' => false,
'user_id' => $params['admin_id'],
'InputEncryptedChat' => [
'_' => 'inputEncryptedChat',
'chat_id' => $params['id'],
'access_hash' => $params['access_hash']
],
'in_seq_no_x' => 1,
'out_seq_no_x' => 0,
'in_seq_no' => 0,
'out_seq_no' => 0,
'layer' => 8,
'ttl' => 0,
'ttr' => 100,
'updated' => \time(),
'incoming' => [],
'outgoing' => [],
'created' => \time(),
'rekeying' => [0],
'key_x' => 'from server',
'mtproto' => 1
];
$this->secretFeeders[$params['id']] = new SecretFeedLoop($this, $params['id']);
if ($this->secretFeeders[$params['id']]->start()) {
$this->secretFeeders[$params['id']]->resume();
}
$g_b = $dh_config['g']->powMod($b, $dh_config['p']);
Crypt::checkG($g_b, $dh_config['p']);
yield from $this->methodCallAsyncRead('messages.acceptEncryption', ['peer' => $params['id'], 'g_b' => $g_b->toBytes(), 'key_fingerprint' => $key['fingerprint']]);
@ -129,6 +157,10 @@ trait AuthKeyHandler
$key['visualization_orig'] = \substr(\sha1($key['auth_key'], true), 16);
$key['visualization_46'] = \substr(\hash('sha256', $key['auth_key'], true), 20);
$this->secret_chats[$params['id']] = ['key' => $key, 'admin' => true, 'user_id' => $params['participant_id'], 'InputEncryptedChat' => ['chat_id' => $params['id'], 'access_hash' => $params['access_hash'], '_' => 'inputEncryptedChat'], 'in_seq_no_x' => 0, 'out_seq_no_x' => 1, 'in_seq_no' => 0, 'out_seq_no' => 0, 'layer' => 8, 'ttl' => 0, 'ttr' => 100, 'updated' => \time(), 'incoming' => [], 'outgoing' => [], 'created' => \time(), 'rekeying' => [0], 'key_x' => 'to server', 'mtproto' => 1];
$this->secretFeeders[$params['id']] = new SecretFeedLoop($this, $params['id']);
if ($this->secretFeeders[$params['id']]->start()) {
$this->secretFeeders[$params['id']]->resume();
}
yield from $this->notifyLayer($params['id']);
$this->logger->logger('Secret chat '.$params['id'].' completed successfully!', \danog\MadelineProto\Logger::NOTICE);
}

View File

@ -52,7 +52,8 @@ trait MessageHandler
$this->secret_chats[$chat_id]['out_seq_no']++;
}
$this->secret_chats[$chat_id]['outgoing'][$this->secret_chats[$chat_id]['out_seq_no']] = $message;
$message = (yield from $this->TL->serializeObject(['type' => $constructor = $this->secret_chats[$chat_id]['layer'] === 8 ? 'DecryptedMessage' : 'DecryptedMessageLayer'], $message, $constructor, $this->secret_chats[$chat_id]['layer']));
$constructor = $this->secret_chats[$chat_id]['layer'] === 8 ? 'DecryptedMessage' : 'DecryptedMessageLayer';
$message = yield from $this->TL->serializeObject(['type' => $constructor], $message, $constructor, $this->secret_chats[$chat_id]['layer']);
$message = \danog\MadelineProto\Tools::packUnsignedInt(\strlen($message)).$message;
if ($this->secret_chats[$chat_id]['mtproto'] === 2) {
$padding = \danog\MadelineProto\Tools::posmod(-\strlen($message), 16);
@ -70,7 +71,15 @@ trait MessageHandler
$message = $this->secret_chats[$chat_id]['key']['fingerprint'].$message_key.Crypt::igeEncrypt($message, $aes_key, $aes_iv);
return $message;
}
private function handleEncryptedUpdate(array $message): \Generator
/**
* Handle encrypted update.
*
* @internal
*
* @param array $message
* @return \Generator
*/
public function handleEncryptedUpdate(array $message): \Generator
{
if (!isset($this->secret_chats[$message['message']['chat_id']])) {
$this->logger->logger(\sprintf(\danog\MadelineProto\Lang::$current_lang['secret_chat_skipping'], $message['message']['chat_id']));
@ -127,6 +136,7 @@ trait MessageHandler
$message['message']['decrypted_message'] = $deserialized;
$this->secret_chats[$message['message']['chat_id']]['incoming'][$this->secret_chats[$message['message']['chat_id']]['in_seq_no']] = $message['message'];
yield from $this->handleDecryptedUpdate($message);
return true;
}
/**
* @return false|string

View File

@ -24,73 +24,73 @@ namespace danog\MadelineProto\SecretChats;
*/
trait ResponseHandler
{
private function handleDecryptedUpdate($update): \Generator
private function handleDecryptedUpdate(array $update): \Generator
{
// already checked in TL.php
switch ($update['message']['decrypted_message']['_']) {
case 'decryptedMessageService':
switch ($update['message']['decrypted_message']['action']['_']) {
case 'decryptedMessageActionRequestKey':
yield from $this->acceptRekey($update['message']['chat_id'], $update['message']['decrypted_message']['action']);
return;
case 'decryptedMessageActionAcceptKey':
yield from $this->commitRekey($update['message']['chat_id'], $update['message']['decrypted_message']['action']);
return;
case 'decryptedMessageActionCommitKey':
yield from $this->completeRekey($update['message']['chat_id'], $update['message']['decrypted_message']['action']);
return;
case 'decryptedMessageActionNotifyLayer':
$this->secret_chats[$update['message']['chat_id']]['layer'] = $update['message']['decrypted_message']['action']['layer'];
if ($update['message']['decrypted_message']['action']['layer'] >= 17 && \time() - $this->secret_chats[$update['message']['chat_id']]['created'] > 15) {
yield from $this->notifyLayer($update['message']['chat_id']);
}
if ($update['message']['decrypted_message']['action']['layer'] >= 73) {
$this->secret_chats[$update['message']['chat_id']]['mtproto'] = 2;
}
return;
case 'decryptedMessageActionSetMessageTTL':
$this->secret_chats[$update['message']['chat_id']]['ttl'] = $update['message']['decrypted_message']['action']['ttl_seconds'];
yield from $this->saveUpdate($update);
return;
case 'decryptedMessageActionNoop':
return;
case 'decryptedMessageActionResend':
$update['message']['decrypted_message']['action']['start_seq_no'] -= $this->secret_chats[$update['message']['chat_id']]['out_seq_no_x'];
$update['message']['decrypted_message']['action']['end_seq_no'] -= $this->secret_chats[$update['message']['chat_id']]['out_seq_no_x'];
$update['message']['decrypted_message']['action']['start_seq_no'] /= 2;
$update['message']['decrypted_message']['action']['end_seq_no'] /= 2;
$this->logger->logger('Resending messages for secret chat '.$update['message']['chat_id'], \danog\MadelineProto\Logger::WARNING);
foreach ($this->secret_chats[$update['message']['chat_id']]['outgoing'] as $seq => $message) {
if ($seq >= $update['message']['decrypted_message']['action']['start_seq_no'] && $seq <= $update['message']['decrypted_message']['action']['end_seq_no']) {
//throw new \danog\MadelineProto\ResponseException(\danog\MadelineProto\Lang::$current_lang['resending_unsupported']);
yield from $this->methodCallAsyncRead('messages.sendEncrypted', ['peer' => $update['message']['chat_id'], 'message' => $update['message']['decrypted_message']]);
}
}
return;
default:
// yield $this->saveUpdate(['_' => 'updateNewDecryptedMessage', 'peer' => $this->secret_chats[$update['message']['chat_id']]['InputEncryptedChat'], 'in_seq_no' => $this->get_in_seq_no($update['message']['chat_id']), 'out_seq_no' => $this->get_out_seq_no($update['message']['chat_id']), 'message' => $update['message']['decrypted_message']]);
yield from $this->saveUpdate($update);
}
break;
case 'decryptedMessage':
yield from $this->saveUpdate($update);
break;
case 'decryptedMessageLayer':
if ((yield from $this->checkSecretOutSeqNo($update['message']['chat_id'], $update['message']['decrypted_message']['out_seq_no'])) && (yield from $this->checkSecretInSeqNo($update['message']['chat_id'], $update['message']['decrypted_message']['in_seq_no']))) {
$this->secret_chats[$update['message']['chat_id']]['in_seq_no']++;
if ($update['message']['decrypted_message']['layer'] >= 17) {
$this->secret_chats[$update['message']['chat_id']]['layer'] = $update['message']['decrypted_message']['layer'];
if ($update['message']['decrypted_message']['layer'] >= 17 && \time() - $this->secret_chats[$update['message']['chat_id']]['created'] > 15) {
yield from $this->notifyLayer($update['message']['chat_id']);
$chatId = $update['message']['chat_id'];
$decryptedMessage = $update['message']['decrypted_message'];
if ($decryptedMessage['_'] === 'decryptedMessage') {
yield from $this->saveUpdate($update);
return;
}
if ($decryptedMessage['_'] === 'decryptedMessageService') {
$action = $decryptedMessage['action'];
switch ($action['_']) {
case 'decryptedMessageActionRequestKey':
yield from $this->acceptRekey($chatId, $action);
return;
case 'decryptedMessageActionAcceptKey':
yield from $this->commitRekey($chatId, $action);
return;
case 'decryptedMessageActionCommitKey':
yield from $this->completeRekey($chatId, $action);
return;
case 'decryptedMessageActionNotifyLayer':
$this->secret_chats[$chatId]['layer'] = $action['layer'];
if ($action['layer'] >= 17 && \time() - $this->secret_chats[$chatId]['created'] > 15) {
yield from $this->notifyLayer($chatId);
}
if ($action['layer'] >= 73) {
$this->secret_chats[$chatId]['mtproto'] = 2;
}
return;
case 'decryptedMessageActionSetMessageTTL':
$this->secret_chats[$chatId]['ttl'] = $action['ttl_seconds'];
yield from $this->saveUpdate($update);
return;
case 'decryptedMessageActionNoop':
return;
case 'decryptedMessageActionResend':
$action['start_seq_no'] -= $this->secret_chats[$chatId]['out_seq_no_x'];
$action['end_seq_no'] -= $this->secret_chats[$chatId]['out_seq_no_x'];
$action['start_seq_no'] /= 2;
$action['end_seq_no'] /= 2;
$this->logger->logger('Resending messages for secret chat '.$chatId, \danog\MadelineProto\Logger::WARNING);
foreach ($this->secret_chats[$chatId]['outgoing'] as $seq => $message) {
if ($seq >= $action['start_seq_no'] && $seq <= $action['end_seq_no']) {
yield from $this->methodCallAsyncRead('messages.sendEncrypted', ['peer' => $chatId, 'message' => $message]);
}
}
$update['message']['decrypted_message'] = $update['message']['decrypted_message']['message'];
yield from $this->handleDecryptedUpdate($update);
}
break;
default:
throw new \danog\MadelineProto\ResponseException('Unrecognized decrypted message received: '.\var_export($update, true));
break;
return;
default:
yield from $this->saveUpdate($update);
}
return;
}
if ($decryptedMessage['_'] === 'decryptedMessageLayer') {
if ((yield from $this->checkSecretOutSeqNo($chatId, $decryptedMessage['out_seq_no']))
&& (yield from $this->checkSecretInSeqNo($chatId, $decryptedMessage['in_seq_no']))) {
$this->secret_chats[$chatId]['in_seq_no']++;
if ($decryptedMessage['layer'] >= 17) {
$this->secret_chats[$chatId]['layer'] = $decryptedMessage['layer'];
if ($decryptedMessage['layer'] >= 17 && \time() - $this->secret_chats[$chatId]['created'] > 15) {
yield from $this->notifyLayer($chatId);
}
}
$update['message']['decrypted_message'] = $decryptedMessage['message'];
yield from $this->handleDecryptedUpdate($update);
}
return;
}
throw new \danog\MadelineProto\ResponseException('Unrecognized decrypted message received: '.\var_export($update, true));
}
}

View File

@ -19,6 +19,8 @@
namespace danog\MadelineProto\SecretChats;
use danog\MadelineProto\Logger;
/**
* Manages sequence numbers.
*/
@ -31,6 +33,7 @@ trait SeqNoHandler
foreach ($this->secret_chats[$chat_id]['incoming'] as $message) {
if (isset($message['decrypted_message']['in_seq_no'])) {
if (($message['decrypted_message']['in_seq_no'] - $this->secret_chats[$chat_id]['out_seq_no_x']) / 2 < $last) {
$this->logger->logger("Discarding secret chat $chat_id, in_seq_no is not increasing", Logger::LEVEL_FATAL);
yield from $this->discardSecretChat($chat_id);
throw new \danog\MadelineProto\SecurityException('in_seq_no is not increasing');
}
@ -38,6 +41,7 @@ trait SeqNoHandler
}
}
if ($seqno > $this->secret_chats[$chat_id]['out_seq_no'] + 1) {
$this->logger->logger("Discarding secret chat $chat_id, in_seq_no is too big", Logger::LEVEL_FATAL);
yield from $this->discardSecretChat($chat_id);
throw new \danog\MadelineProto\SecurityException('in_seq_no is too big');
}
@ -49,9 +53,11 @@ trait SeqNoHandler
$C = 0;
foreach ($this->secret_chats[$chat_id]['incoming'] as $message) {
if (isset($message['decrypted_message']['out_seq_no']) && $C < $this->secret_chats[$chat_id]['in_seq_no']) {
if (($message['decrypted_message']['out_seq_no'] - $this->secret_chats[$chat_id]['in_seq_no_x']) / 2 !== $C) {
$temp = ($message['decrypted_message']['out_seq_no'] - $this->secret_chats[$chat_id]['in_seq_no_x']) / 2;
if ($temp !== $C) {
$this->logger->logger("Discarding secret chat $chat_id, out_seq_no hole: should be $C, is $temp", Logger::LEVEL_FATAL);
yield from $this->discardSecretChat($chat_id);
throw new \danog\MadelineProto\SecurityException('out_seq_no hole: should be '.$C.', is '.($message['decrypted_message']['out_seq_no'] - $this->secret_chats[$chat_id]['in_seq_no_x']) / 2);
throw new \danog\MadelineProto\SecurityException("out_seq_no hole: should be $C, is $temp");
}
$C++;
}
@ -64,6 +70,7 @@ trait SeqNoHandler
}
if ($seqno > $C) {
// > C+1
$this->logger->logger("Discarding secret chat $chat_id, out_seq_no gap detected: ($seqno > $C)", Logger::LEVEL_FATAL);
yield from $this->discardSecretChat($chat_id);
throw new \danog\MadelineProto\SecurityException('WARNING: out_seq_no gap detected ('.$seqno.' > '.$C.')!');
}

View File

@ -537,7 +537,6 @@ class TL
return $object;
}
}
$auto = false;
if ($type['type'] === 'InputMessage' && !\is_array($object)) {
$object = ['_' => 'inputMessageID', 'id' => $object];
} elseif (isset($this->callbacks[TLCallback::TYPE_MISMATCH_CALLBACK][$type['type']]) && (!\is_array($object) || isset($object['_']) && $this->constructors->findByPredicate($object['_'])['type'] !== $type['type'])) {
@ -569,7 +568,6 @@ class TL
$type['type'] = \substr($type['type'], 1);
}
if ($predicate === $type['type']) {
//} && !$auto) {
$bare = true;
}
if ($predicate === 'messageEntityMentionName') {
@ -652,7 +650,7 @@ class TL
} elseif (isset($arguments['id'])) {
$method = 'photos.updateProfilePhoto';
}
} else if ($method === 'messages.uploadMedia') {
} elseif ($method === 'messages.uploadMedia') {
if (!isset($arguments['peer']) && !$this->API->getSelf()['bot']) {
$arguments['peer'] = 'me';
}