diff --git a/.travis.yml b/.travis.yml index 7522f609..f6205ebf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,9 +4,9 @@ group: edge language: php php: - '7.0' +- '7.1' - nightly - hhvm -- '5.6' addons: apt: diff --git a/README.md b/README.md index 4b3fd163..7e196411 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Logo created by [Matthew Hesketh](http://matthewhesketh.com) (thanks again!). PHP implementation of MTProto, based on [telepy](https://github.com/griganton/telepy_old). -This project can run on PHP 7, PHP 5.6 and HHVM, only 64 bit systems are supported ATM. +This project can run on PHP 7 and HHVM, only 64 bit systems are supported ATM. Also note that MadelineProto will perform better if a big math extension like gmp or bcmath is installed. @@ -68,7 +68,7 @@ $MadelineProto = new \danog\MadelineProto\API(); ### Settings The constructor accepts an optional parameter, which is the settings array. This array contains some other arrays, which are the settings for a specific MadelineProto function. -See https://github.com/danog/MadelineProto/blob/master/src/danog/MadelineProto/MTProto.php#L99 for the default values for the settings\ arrays and explanations for every setting. +See [here](https://github.com/danog/MadelineProto/blob/master/src/danog/MadelineProto/MTProto.php#L99) for the default values for the settings arrays and explanations for every setting. You can provide part of any subsetting array, that way the remaining arrays will be automagically set to default and undefined values of specified subsetting arrays will be set to the default values. Example: @@ -324,9 +324,13 @@ The same operation should be done when serializing to another destination manual MadelineProto can throw three different exceptions: * \danog\MadelineProto\Exception - Default exception, thrown when a php error occures and in a lot of other cases + * \danog\MadelineProto\RPCErrorException - Thrown when an RPC error occurres (an error received via the mtproto API) + * \danog\MadelineProto\TL\Exception - Thrown on TL serialization/deserialization errors +* \danog\MadelineProto\NothingInTheSocketException - Thrown if no data can be read from the TCP socket + ## Contributing diff --git a/composer.json b/composer.json index 4fe50e39..4e3850b6 100644 --- a/composer.json +++ b/composer.json @@ -5,11 +5,19 @@ "license": "AGPLV3", "homepage": "https://daniil.it/MadelineProto", "keywords": ["telegram", "mtproto", "protocol", "bytes", "messenger", "client", "PHP", "video", "stickers", "audio", "files", "GB"], + + "repositories": [ + { + "type": "git", + "url": "https://github.com/danog/phpseclib" + } + ], "require": { "php": ">=5.6.0", - "danog/phpstruct": "^1.2", - "phpseclib/phpseclib": "^2.0.4", - "vlucas/phpdotenv": "^2.4" + "danog/phpstruct": "dev-fast", + "phpseclib/phpseclib": "dev-master", + "vlucas/phpdotenv": "^2.4", + "krakjoe/pthreads-polyfill": "dev-master" }, "require-dev": { "phpdocumentor/reflection-docblock": "^3.1" diff --git a/docs/index.md b/docs/index.md index bafdd911..366876e0 100644 --- a/docs/index.md +++ b/docs/index.md @@ -14,7 +14,7 @@ Logo created by [Matthew Hesketh](http://matthewhesketh.com) (thanks again!). PHP implementation of MTProto, based on [telepy](https://github.com/griganton/telepy_old). -This project can run on PHP 7, PHP 5.6 and HHVM, only 64 bit systems are supported ATM. +This project can run on PHP 7 and HHVM, only 64 bit systems are supported ATM. Also note that MadelineProto will perform better if a big math extension like gmp or bcmath is installed. @@ -72,7 +72,7 @@ $MadelineProto = new \danog\MadelineProto\API(); ### Settings The constructor accepts an optional parameter, which is the settings array. This array contains some other arrays, which are the settings for a specific MadelineProto function. -See https://github.com/danog/MadelineProto/blob/master/src/danog/MadelineProto/MTProto.php#L99 for the default values for the settings\ arrays and explanations for every setting. +See [here](https://github.com/danog/MadelineProto/blob/master/src/danog/MadelineProto/MTProto.php#L99) for the default values for the settings arrays and explanations for every setting. You can provide part of any subsetting array, that way the remaining arrays will be automagically set to default and undefined values of specified subsetting arrays will be set to the default values. Example: @@ -328,9 +328,13 @@ The same operation should be done when serializing to another destination manual MadelineProto can throw three different exceptions: * \danog\MadelineProto\Exception - Default exception, thrown when a php error occures and in a lot of other cases + * \danog\MadelineProto\RPCErrorException - Thrown when an RPC error occurres (an error received via the mtproto API) + * \danog\MadelineProto\TL\Exception - Thrown on TL serialization/deserialization errors +* \danog\MadelineProto\NothingInTheSocketException - Thrown if no data can be read from the TCP socket + ## Contributing diff --git a/pipesbot.php b/pipesbot.php new file mode 100755 index 00000000..6bdc57c8 --- /dev/null +++ b/pipesbot.php @@ -0,0 +1,185 @@ +#!/usr/bin/env php +bot_login($token); + \danog\MadelineProto\Logger::log([$authorization], \danog\MadelineProto\Logger::NOTICE); +} +if ($uMadelineProto === false) { + echo 'Loading MadelineProto...'.PHP_EOL; + $uMadelineProto = new \danog\MadelineProto\API(['updates' => ['handle_updates' => false]]); + $sentCode = $uMadelineProto->phone_login(readline()); + \danog\MadelineProto\Logger::log([$sentCode], \danog\MadelineProto\Logger::NOTICE); + echo 'Enter the code you received: '; + $code = fgets(STDIN, (isset($sentCode['type']['length']) ? $sentCode['type']['length'] : 5) + 1); + $authorization = $uMadelineProto->complete_phone_login($code); + \danog\MadelineProto\Logger::log([$authorization], \danog\MadelineProto\Logger::NOTICE); + if ($authorization['_'] === 'account.noPassword') { + throw new \danog\MadelineProto\Exception('2FA is enabled but no password is set!'); + } + if ($authorization['_'] === 'account.password') { + \danog\MadelineProto\Logger::log(['2FA is enabled'], \danog\MadelineProto\Logger::NOTICE); + $authorization = $uMadelineProto->complete_2fa_login(readline('Please enter your password (hint '.$authorization['hint'].'): ')); + } + echo 'Serializing MadelineProto to session.madeline...'.PHP_EOL; + echo 'Wrote '.\danog\MadelineProto\Serialization::serialize('session.madeline', $uMadelineProto).' bytes'.PHP_EOL; +} +function inputify(&$stuff) { + $stuff['_'] = 'input'.ucfirst($stuff['_']); + return $stuff; +} +function translatetext (&$value) { + inputify($value); + if (isset($value['entities'])) { + foreach ($value['entities'] as &$entity) { + if ($entity['_'] === 'messageEntityMentionName') inputify($entity); + } + } + if (isset($value['geo'])) { + $value['geo_point'] = inputify($value['geo']); + } +} +function translate (&$value, $key) { + switch ($value['_']) { + case 'botInlineResult': + $value['_'] = 'inputBotInlineResult'; + translatetext($value['send_message']); + return $value; + case 'botInlineMediaResult': + if (isset($value['game'])) throw new \danog\MadelineProto\RPCErrorException('Games are not supported.'); + if (isset($value['photo'])) $value['_'] = 'inputBotInlineResultPhoto'; + if (isset($value['document'])) $value['_'] = 'inputBotInlineResultDocument'; + translatetext($value['send_message']); + return $value; + } +} + +$offset = 0; +$start = "This bot can create a pipeline between inline bots. +To use it, simply type an inline query with the following syntax: + +Query | @ainlinebot:1 | @binlinebot:lel | @inlinebot \$ + +This will make an inline query with text \"Query\" to @ainlinebot, take the first result if it's a text message (entities will be ignored, if it's a media message you will be redirected here), then it will make an inline query to @binlinebot with the text received out of the first bot, select the result that is a text message with the word \"lel\" in it (regexes are supported), and finally pipe it to @inlinebot, fetch all results and return them to you. +Note that the query must be terminated by a \$ + +Created by @danogentili (@daniilgentili) using the daniil.it/MadelineProto PHP MTProto client."; +while (true) { + $updates = $MadelineProto->API->get_updates(['offset' => $offset, 'limit' => 50, 'timeout' => 0]); // Just like in the bot API, you can specify an offset, a limit and a timeout + foreach ($updates as $update) { + $offset = $update['update_id'] + 1; // Just like in the bot API, the offset must be set to the last update_id + switch ($update['update']['_']) { + case 'updateNewMessage': +var_dump($update); + if (isset($update['update']['message']['out']) && $update['update']['message']['out']) { + continue; + } + try { + if (preg_match('|/start|', $update['update']['message']['message'])){ + $MadelineProto->messages->sendMessage(['peer' => $update['update']['message']['from_id'], 'message' => $start, 'reply_to_msg_id' => $update['update']['message']['id']]); + } + } catch (\danog\MadelineProto\RPCErrorException $e) { + $MadelineProto->messages->sendMessage(['peer' => '@danogentili', 'message' => $e->getCode().': '.$e->getMessage().PHP_EOL.$e->getTraceAsString()]); + } + break; + case 'updateNewChannelMessage': + if (isset($update['update']['message']['out']) && $update['update']['message']['out']) { + continue; + } + try { + if (preg_match('|/start|', $update['update']['message']['message'])){ + $MadelineProto->messages->sendMessage(['peer' => $update['update']['message']['to_id'], 'message' => $start, 'reply_to_msg_id' => $update['update']['message']['id']]); + } + } catch (\danog\MadelineProto\RPCErrorException $e) { + $MadelineProto->messages->sendMessage(['peer' => '@danogentili', 'message' => $e->getCode().': '.$e->getMessage().PHP_EOL.$e->getTraceAsString()]); + } catch (\danog\MadelineProto\Exception $e) { + $MadelineProto->messages->sendMessage(['peer' => '@danogentili', 'message' => $e->getCode().': '.$e->getMessage().PHP_EOL.$e->getTraceAsString()]); + } + break; + case 'updateBotInlineQuery': + try { + $sswitch = ['_' => 'inlineBotSwitchPM', 'text' => 'FAQ', 'start_param' => 'lel']; + if ($update['update']['query'] === '') { + $MadelineProto->messages->setInlineBotResults(['query_id' => $update['update']['query_id'], 'results' => [], 'cache_time' => 0, 'switch_pm' => $sswitch]); + } else { + $toset = ['query_id' => $update['update']['query_id'], 'results' => [], 'cache_time' => 0, 'private' => true]; + if (preg_match('|\$\s*$|', $update['update']['query'])) { + + $exploded = explode('|', preg_replace('/\$\s*$/', '', $update['update']['query'])); + array_walk($exploded, function (&$value, $key) { $value = preg_replace(['/^\s+/', '/\s+$/'], '', $value); }); + $query = array_shift($exploded); + foreach ($exploded as $current => $botq) { + $bot = preg_replace('|:.*|', '', $botq); + if ($bot === '' || $uMadelineProto->get_info($bot)['bot_api_id'] === $MadelineProto->API->datacenter->authorization['user']['id']) { + $toset['switch_pm'] = $sswitch; + break; + } + $select = preg_replace('|'.$bot.':|', '', $botq); + $results = $uMadelineProto->messages->getInlineBotResults(['bot' => $bot, 'peer' => $update['update']['user_id'], 'query' => $query, 'offset' => $offset]); + if (isset($results['switch_pm'])) { + $toset['switch_pm'] = $results['switch_pm']; + break; + } + $toset['gallery'] = $results['gallery']; + $toset['results'] = []; + if (is_numeric($select)) { + $toset['results'][0] = $results['results'][$select-1]; + } else if ($select === '') { + $toset['results'] = $results['results']; + } else { + foreach ($results['results'] as $result) { + if (isset($result['send_message']['message']) && preg_match('|'.$select.'|', $result['send_message']['message'])) { + $toset['results'][0] = $result; + } + } + } + if (!isset($toset['results'][0])) $toset['results'] = $results['results']; + if (count($exploded) - 1 === $current || !isset($toset['results'][0]['send_message']['message'])) break; + $query = $toset['results'][0]['send_message']['message']; + } + } + if (empty($toset['results'])) { + $toset['switch_pm'] = $sswitch; + } else { + array_walk($toset['results'], 'translate'); + } + $MadelineProto->messages->setInlineBotResults($toset); + } + } catch (\danog\MadelineProto\RPCErrorException $e) { + $MadelineProto->messages->sendMessage(['peer' => '@danogentili', 'message' => $e->getCode().': '.$e->getMessage().PHP_EOL.$e->getTraceAsString()]); + try { + $MadelineProto->messages->sendMessage(['peer' => $update['update']['user_id'], 'message' => $e->getCode().': '.$e->getMessage().PHP_EOL.$e->getTraceAsString()]); + } catch (\danog\MadelineProto\RPCErrorException $e) { + } catch (\danog\MadelineProto\Exception $e) { ; } + try { + $toset['switch_pm'] = $sswitch; + $MadelineProto->messages->setInlineBotResults($toset); + } catch (\danog\MadelineProto\RPCErrorException $e) { + } catch (\danog\MadelineProto\Exception $e) { ; } + } catch (\danog\MadelineProto\Exception $e) { + $MadelineProto->messages->sendMessage(['peer' => '@danogentili', 'message' => $e->getCode().': '.$e->getMessage().PHP_EOL.$e->getTraceAsString()]); + try { + $MadelineProto->messages->sendMessage(['peer' => $update['update']['user_id'], 'message' => $e->getCode().': '.$e->getMessage().PHP_EOL.$e->getTraceAsString()]); + } catch (\danog\MadelineProto\RPCErrorException $e) { + } catch (\danog\MadelineProto\Exception $e) { ; } + try { + $toset['switch_pm'] = $sswitch; + $MadelineProto->messages->setInlineBotResults($toset); + } catch (\danog\MadelineProto\RPCErrorException $e) { + } catch (\danog\MadelineProto\Exception $e) { ; } + } + } + } + \danog\MadelineProto\Serialization::serialize('bot.madeline', $MadelineProto); + \danog\MadelineProto\Serialization::serialize('pwr.madeline', $uMadelineProto); +} diff --git a/src/danog/MadelineProto/Connection.php b/src/danog/MadelineProto/Connection.php index e369f65b..7b669a22 100644 --- a/src/danog/MadelineProto/Connection.php +++ b/src/danog/MadelineProto/Connection.php @@ -103,8 +103,6 @@ class Connection case 'tcp_abridged': case 'tcp_intermediate': case 'tcp_full': - fclose($this->sock); - break; case 'http': case 'https': fclose($this->sock); @@ -128,16 +126,6 @@ class Connection $this->__construct($this->ip, $this->port, $this->protocol, $this->timeout); } - /** - * Function to get hex crc32. - * - * @param $data Data to encode. - */ - public function newcrc32($data) - { - return hexdec(hash('crc32b', $data)); - } - public function write($what, $length = null) { if ($length !== null) { @@ -179,6 +167,9 @@ class Connection throw new Exception("Connection: couldn't connect to socket."); } $packet = stream_get_contents($this->sock, $length); + if ($packet === false) { + throw new NothingInTheSocketException('Nothing in the socket!'); + } if (strlen($packet) != $length) { throw new \danog\MadelineProto\Exception("WARNING: Wrong length was read (should've read ".($length).', read '.strlen($packet).')!'); } @@ -197,12 +188,9 @@ class Connection switch ($this->protocol) { case 'tcp_full': $packet_length_data = $this->read(4); - if (strlen($packet_length_data) < 4) { - throw new Exception('Nothing in the socket!'); - } $packet_length = \danog\PHP\Struct::unpack('read($packet_length - 4); - if ($this->newcrc32($packet_length_data.substr($packet, 0, -4)) != \danog\PHP\Struct::unpack('in_seq_no++; @@ -211,22 +199,13 @@ class Connection throw new Exception('Incoming seq_no mismatch'); } - $payload = $this->fopen_and_write('php://memory', 'rw+b', substr($packet, 4, $packet_length - 12)); - break; + return substr($packet, 4, $packet_length - 12); case 'tcp_intermediate': $packet_length_data = $this->read(4); - if (strlen($packet_length_data) < 4) { - throw new Exception('Nothing in the socket!'); - } $packet_length = \danog\PHP\Struct::unpack('read($packet_length); - $payload = $this->fopen_and_write('php://memory', 'rw+b', $packet); - break; + return $this->read($packet_length); case 'tcp_abridged': $packet_length_data = $this->read(1); - if (strlen($packet_length_data) < 1) { - throw new Exception('Nothing in the socket!'); - } $packet_length = ord($packet_length_data); if ($packet_length < 127) { $packet_length <<= 2; @@ -234,9 +213,7 @@ class Connection $packet_length_data = $this->read(3); $packet_length = \danog\PHP\Struct::unpack('read($packet_length); - $payload = $this->fopen_and_write('php://memory', 'rw+b', $packet); - break; + return $this->read($packet_length); case 'http': case 'https': $headers = []; @@ -261,19 +238,17 @@ class Connection } $headers[] = $current_header; } - $payload = $this->fopen_and_write('php://memory', 'rw+b', $this->read($length)); + $read = $this->read($length); if ($headers[0] !== 'HTTP/1.1 200 OK') { throw new Exception($headers[0]); } if ($close) { $this->close_and_reopen(); } - break; + return $read; case 'udp': throw new Exception("Connection: This protocol wasn't implemented yet."); } - - return $payload; } public function send_message($message) @@ -282,7 +257,7 @@ class Connection case 'tcp_full': $this->out_seq_no++; $step1 = \danog\PHP\Struct::pack('out_seq_no).$message; - $step2 = $step1.\danog\PHP\Struct::pack('newcrc32($step1)); + $step2 = $step1.strrev(hash('crc32b', $step1, true)); $this->write($step2); break; case 'tcp_intermediate': diff --git a/src/danog/MadelineProto/Exception.php b/src/danog/MadelineProto/Exception.php index e7eec499..0fe97048 100644 --- a/src/danog/MadelineProto/Exception.php +++ b/src/danog/MadelineProto/Exception.php @@ -26,7 +26,7 @@ class Exception extends \Exception return true; // return true to continue through the others error handlers } if (\danog\MadelineProto\Logger::$constructed) { - \danog\MadelineProto\Logger::log([$errstr], \danog\MadelineProto\Logger::FATAL_ERROR); + \danog\MadelineProto\Logger::log([$errstr.' in '.basename($errfile).':'.$errline], \danog\MadelineProto\Logger::FATAL_ERROR); } $e = new \danog\MadelineProto\Exception($errstr, $errno); $e->file = $errfile; diff --git a/src/danog/MadelineProto/MTProto.php b/src/danog/MadelineProto/MTProto.php index 46706a32..bbce3ab5 100644 --- a/src/danog/MadelineProto/MTProto.php +++ b/src/danog/MadelineProto/MTProto.php @@ -202,6 +202,7 @@ Slv8kg9qv1m6XHVQY3PnEw+QQtqSIXklHwIDAQAB 'incoming' => 1000, 'outgoing' => 1000, ], + 'peer' => ['full_info_cache_time' => 60], 'updates' => [ 'handle_updates' => true, // Should I handle updates? 'callback' => [$this, 'get_updates_update_handler'], // A callable function that will be called every time an update is received, must accept an array (for the update) as the only parameter @@ -336,14 +337,17 @@ Slv8kg9qv1m6XHVQY3PnEw+QQtqSIXklHwIDAQAB public function parse_config() { - foreach ($this->config['dc_options'] as $dc) { + $this->parse_dc_options($this->config['dc_options']); + unset($this->config['dc_options']); + \danog\MadelineProto\Logger::log(['Updated config!', $this->config], Logger::NOTICE); + } + public function parse_dc_options($dc_options) { + foreach ($dc_options as $dc) { $test = $this->config['test_mode'] ? 'test' : 'main'; $ipv6 = ($dc['ipv6'] ? 'ipv6' : 'ipv4'); $id = $dc['id']; $test .= (isset($this->settings['connection'][$test][$ipv6][$id]) && $this->settings['connection'][$test][$ipv6][$id]['ip_address'] != $dc['ip_address']) ? '_bk' : ''; $this->settings['connection'][$test][$ipv6][$id] = $dc; } - unset($this->config['dc_options']); - \danog\MadelineProto\Logger::log(['Updated config!', $this->config], Logger::NOTICE); } } diff --git a/src/danog/MadelineProto/MTProtoTools/AuthKeyHandler.php b/src/danog/MadelineProto/MTProtoTools/AuthKeyHandler.php index 03dcccc7..0c2e6651 100644 --- a/src/danog/MadelineProto/MTProtoTools/AuthKeyHandler.php +++ b/src/danog/MadelineProto/MTProtoTools/AuthKeyHandler.php @@ -213,13 +213,13 @@ trait AuthKeyHandler * int $server_time * ] */ - $server_DH_inner_data = $this->deserialize($this->fopen_and_write('php://memory', 'rw+b', $answer)); + $server_DH_inner_data = $this->deserialize($answer); /* * *********************************************************************** * Do some checks */ - $server_DH_inner_data_length = $this->get_length($this->fopen_and_write('php://memory', 'rw+b', $answer)); + $server_DH_inner_data_length = $this->get_length(new \danog\MadelineProto\Stream($answer)); if (sha1(substr($answer, 0, $server_DH_inner_data_length), true) != $answer_hash) { throw new \danog\MadelineProto\Exception('answer_hash mismatch.'); } diff --git a/src/danog/MadelineProto/MTProtoTools/Crypt.php b/src/danog/MadelineProto/MTProtoTools/Crypt.php index e9df24a8..30f838ce 100644 --- a/src/danog/MadelineProto/MTProtoTools/Crypt.php +++ b/src/danog/MadelineProto/MTProtoTools/Crypt.php @@ -29,62 +29,17 @@ trait Crypt public function ige_encrypt($message, $key, $iv) { - return $this->_ige($message, $key, $iv, 'encrypt'); + $cipher = new \phpseclib\Crypt\AES(\phpseclib\Crypt\AES::MODE_IGE); + $cipher->setKey($key); + $cipher->setIV($iv); + return $cipher->encrypt($message); } public function ige_decrypt($message, $key, $iv) { - return $this->_ige($message, $key, $iv, 'decrypt'); - } - - /** - * Given a key, given an iv, and message - * do whatever operation asked in the operation field. - * Operation will be checked for: "decrypt" and "encrypt" strings. - * Returns the message encrypted/decrypted. - * message must be a multiple by 16 bytes (for division in 16 byte blocks) - * key must be 32 byte - * iv must be 32 byte (it's not internally used in AES 256 ECB, but it's - * needed for IGE). - */ - public function _ige($message, $key, $iv, $operation = 'decrypt') - { - if (strlen($key) != 32) { - throw new \danog\MadelineProto\Exception('key must be 32 bytes long (was '.strlen($key).' bytes)'); - } - if (strlen($iv) != 32) { - throw new \danog\MadelineProto\Exception('iv must be 32 bytes long (was '.strlen($iv).' bytes)'); - } - $cipher = new \phpseclib\Crypt\AES(\phpseclib\Crypt\AES::MODE_ECB); + $cipher = new \phpseclib\Crypt\AES(\phpseclib\Crypt\AES::MODE_IGE); $cipher->setKey($key); - $cipher->paddable = false; - $blocksize = $cipher->block_size; - if ((strlen($message) % $blocksize) != 0) { - throw new \danog\MadelineProto\Exception('message must be a multiple of 16 bytes (try adding '.(16 - (strlen($message) % 16)).' bytes of padding)'); - } - $ivp = substr($iv, 0, $blocksize); - $ivp2 = substr($iv, $blocksize); - $ciphered = ''; - for ($i = 0; $i <= strlen($message); $i += $blocksize) { - $indata = substr($message, $i, $blocksize); - if ($operation === 'decrypt') { - $xored = $indata ^ $ivp2; - $decrypt_xored = $cipher->decrypt($xored); - $outdata = $decrypt_xored ^ $ivp; - $ivp = $indata; - $ivp2 = $outdata; - } elseif ($operation === 'encrypt') { - $xored = $indata ^ $ivp; - $encrypt_xored = $cipher->encrypt($xored); - $outdata = $encrypt_xored ^ $ivp2; - $ivp = $outdata; - $ivp2 = $indata; - } else { - throw new \danog\MadelineProto\Exception('Crypt: operation must be either \'decrypt\' or \'encrypt\''); - } - $ciphered .= $outdata; - } - - return $ciphered; + $cipher->setIV($iv); + return $cipher->decrypt($message); } } diff --git a/src/danog/MadelineProto/MTProtoTools/MessageHandler.php b/src/danog/MadelineProto/MTProtoTools/MessageHandler.php index 04ddeacf..88cd33b5 100644 --- a/src/danog/MadelineProto/MTProtoTools/MessageHandler.php +++ b/src/danog/MadelineProto/MTProtoTools/MessageHandler.php @@ -35,11 +35,11 @@ trait MessageHandler $message = str_repeat(chr(0), 8).$message_id.\danog\PHP\Struct::pack('generate_seq_no($content_related); - $encrypted_data = \danog\PHP\Struct::pack('datacenter->temp_auth_key['server_salt']).$this->datacenter->session_id.$message_id.\danog\PHP\Struct::pack('random($this->posmod(-strlen($encrypted_data), 16)); + $data2enc = \danog\PHP\Struct::pack('datacenter->temp_auth_key['server_salt']).$this->datacenter->session_id.$message_id.\danog\PHP\Struct::pack('random($this->posmod(-strlen($data2enc), 16)); + $message_key = substr(sha1($data2enc, true), -16); list($aes_key, $aes_iv) = $this->aes_calculate($message_key, $this->datacenter->temp_auth_key['auth_key']); - $message = $this->datacenter->temp_auth_key['id'].$message_key.$this->ige_encrypt($encrypted_data.$padding, $aes_key, $aes_iv); + $message = $this->datacenter->temp_auth_key['id'].$message_key.$this->ige_encrypt($data2enc.$padding, $aes_key, $aes_iv); $this->datacenter->outgoing_messages[$int_message_id]['seq_no'] = $seq_no; } $this->datacenter->outgoing_messages[$int_message_id]['response'] = -1; @@ -49,13 +49,13 @@ trait MessageHandler } /** - * Reading connectionet and receiving message from server. Check the CRC32. + * Reading connection and receiving message from server. */ public function recv_message() { $payload = $this->datacenter->read_message(); - if (fstat($payload)['size'] === 4) { - $error = \danog\PHP\Struct::unpack('datacenter->temp_auth_key != null) { \danog\MadelineProto\Logger::log(['WARNING: Resetting auth key...'], \danog\MadelineProto\Logger::WARNING); @@ -68,14 +68,14 @@ trait MessageHandler } throw new \danog\MadelineProto\RPCErrorException($error, $error); } - $auth_key_id = stream_get_contents($payload, 8); + $auth_key_id = substr($payload, 0, 8); if ($auth_key_id === str_repeat(chr(0), 8)) { - list($message_id, $message_length) = \danog\PHP\Struct::unpack('check_message_id($message_id, false); - $message_data = stream_get_contents($payload, $message_length); + $message_data = substr($payload, 20, $message_length); } elseif ($auth_key_id === $this->datacenter->temp_auth_key['id']) { - $message_key = stream_get_contents($payload, 16); - $encrypted_data = stream_get_contents($payload); + $message_key = substr($payload, 8, 16); + $encrypted_data = substr($payload, 24); list($aes_key, $aes_iv) = $this->aes_calculate($message_key, $this->datacenter->temp_auth_key['auth_key'], 'from server'); $decrypted_data = $this->ige_decrypt($encrypted_data, $aes_key, $aes_iv); @@ -121,7 +121,7 @@ trait MessageHandler } else { throw new \danog\MadelineProto\Exception('Got unknown auth_key id'); } - $deserialized = $this->deserialize($this->fopen_and_write('php://memory', 'rw+b', $message_data)); + $deserialized = $this->deserialize($message_data); $this->datacenter->incoming_messages[$message_id]['content'] = $deserialized; $this->datacenter->incoming_messages[$message_id]['response'] = -1; $this->datacenter->new_incoming[$message_id] = $message_id; diff --git a/src/danog/MadelineProto/MTProtoTools/PeerHandler.php b/src/danog/MadelineProto/MTProtoTools/PeerHandler.php index 68a49e68..75668bb7 100644 --- a/src/danog/MadelineProto/MTProtoTools/PeerHandler.php +++ b/src/danog/MadelineProto/MTProtoTools/PeerHandler.php @@ -262,9 +262,18 @@ trait PeerHandler return $res; } + public function full_chat_last_updated($id) + { + $id = $this->get_info($id)['bot_api_id']; + + return isset($this->full_chats[$id]['last_update']) ? $this->full_chats[$id]['last_update'] : 0; + } public function get_full_info($id) { + if (time() - $this->full_chat_last_updated($id) < (isset($this->settings['peer']['full_info_cache_time']) ? $this->settings['peer']['full_info_cache_time'] : 0)) { + return $this->full_chats[$id]; + } $partial = $this->get_info($id); switch ($partial['type']) { case 'user': @@ -283,6 +292,8 @@ trait PeerHandler } $partial = $this->get_info($id); $partial['full'] = $full; + $partial['last_update'] = time(); + $this->full_chats[$partial['id']] = $partial; return $partial; } diff --git a/src/danog/MadelineProto/MTProtoTools/UpdateHandler.php b/src/danog/MadelineProto/MTProtoTools/UpdateHandler.php index e7862d38..5255aa10 100644 --- a/src/danog/MadelineProto/MTProtoTools/UpdateHandler.php +++ b/src/danog/MadelineProto/MTProtoTools/UpdateHandler.php @@ -23,32 +23,22 @@ trait UpdateHandler public $updates_key = 0; private $getting_state = false; public $full_chats; - - public function full_chat_last_updated($id) - { - $id = $this->get_info($id)['bot_api_id']; - - return isset($this->full_chats[$id]['last_update']) ? $this->full_chats[$id]['last_update'] : 0; - } + private $msg_ids = []; public function pwr_update_handler($update) { - if (isset($update['message']['to_id']) && time() - $this->full_chat_last_updated($update['message']['to_id']) <= 600) { + if (isset($update['message']['to_id'])) { try { $full_chat = $this->get_pwr_chat($update['message']['to_id']); - $full_chat['last_update'] = time(); - $this->full_chats[$full_chat['id']] = $full_chat; } catch (\danog\MadelineProto\Exception $e) { \danog\MadelineProto\Logger::log([$e->getMessage()], \danog\MadelineProto\Logger::WARNING); } catch (\danog\MadelineProto\RPCErrorException $e) { \danog\MadelineProto\Logger::log([$e->getMessage()], \danog\MadelineProto\Logger::WARNING); } } - if (isset($update['message']['from_id']) && time() - $this->full_chat_last_updated($update['message']['from_id']) <= 600) { + if (isset($update['message']['from_id'])) { try { $full_chat = $this->get_pwr_chat($update['message']['from_id']); - $full_chat['last_update'] = time(); - $this->full_chats[$full_chat['id']] = $full_chat; } catch (\danog\MadelineProto\Exception $e) { \danog\MadelineProto\Logger::log([$e->getMessage()], \danog\MadelineProto\Logger::WARNING); } catch (\danog\MadelineProto\RPCErrorException $e) { @@ -121,7 +111,15 @@ trait UpdateHandler $this->get_channel_state($channel)['pts'] = $data['pts']; } } - + public function get_msg_id($peer) { + $id = $this->get_info($peer)['bot_api_id']; + return isset($this->msg_ids[$id]) ? $this->msg_ids[$id] : false; + } + public function set_msg_id($peer, $msg_id) { + $id = $this->get_info($peer)['bot_api_id']; + $this->msg_ids[$id] = $msg_id; + $this->should_serialize = true; + } public function get_channel_difference($channel) { if (!$this->settings['updates']['handle_updates']) { @@ -138,6 +136,7 @@ trait UpdateHandler } $difference = $this->method_call('updates.getChannelDifference', ['channel' => $input, 'filter' => ['_' => 'channelMessagesFilterEmpty'], 'pts' => $this->get_channel_state($channel)['pts'], 'limit' => 30]); \danog\MadelineProto\Logger::log(['Got '.$difference['_']], \danog\MadelineProto\Logger::VERBOSE); + $this->get_channel_state($channel)['sync_loading'] = false; switch ($difference['_']) { case 'updates.channelDifferenceEmpty': $this->set_channel_state($channel, $difference); @@ -163,7 +162,6 @@ trait UpdateHandler throw new \danog\MadelineProto\Exception('Unrecognized update difference received: '.var_export($difference, true)); break; } - $this->get_channel_state($channel)['sync_loading'] = false; } public function set_update_state($data) @@ -207,6 +205,8 @@ trait UpdateHandler $difference = $this->method_call('updates.getDifference', ['pts' => $this->get_update_state()['pts'], 'date' => $this->get_update_state()['date'], 'qts' => -1]); \danog\MadelineProto\Logger::log(['Got '.$difference['_']], \danog\MadelineProto\Logger::VERBOSE); + $this->get_update_state()['sync_loading'] = false; + switch ($difference['_']) { case 'updates.differenceEmpty': $this->set_update_state($difference); @@ -227,7 +227,6 @@ trait UpdateHandler throw new \danog\MadelineProto\Exception('Unrecognized update difference received: '.var_export($difference, true)); break; } - $this->get_update_state()['sync_loading'] = false; } public function get_updates_state() @@ -245,7 +244,6 @@ trait UpdateHandler return; } \danog\MadelineProto\Logger::log(['Handling an update of type '.$update['_'].'...'], \danog\MadelineProto\Logger::VERBOSE); - //var_dump($update, $options); $channel_id = false; switch ($update['_']) { @@ -276,13 +274,12 @@ trait UpdateHandler } else { $cur_state = &$this->get_channel_state($channel_id, (isset($update['pts']) ? $update['pts'] : 0) - (isset($update['pts_count']) ? $update['pts_count'] : 0)); } -/* - if ($cur_state['sync_loading']) { + if ($cur_state['sync_loading'] && in_array($update['_'], ['updateNewMessage', 'updateEditMessage', 'updateNewChannelMessage', 'updateEditChannelMessage'])) { \danog\MadelineProto\Logger::log(['Sync loading, not handling update'], \danog\MadelineProto\Logger::NOTICE); -// return false; + return false; } -*/ + switch ($update['_']) { case 'updateChannelTooLong': $this->get_channel_difference($channel_id); @@ -340,6 +337,7 @@ trait UpdateHandler return false; } if ($update['pts'] > $cur_state['pts']) { + \danog\MadelineProto\Logger::log(['Applying pts. current pts: '.$cur_state['pts'].' + pts count: '.(isset($update['pts_count']) ? $update['pts_count'] : 0).' = new pts: '.$new_pts.', channel id: '.$channel_id], \danog\MadelineProto\Logger::VERBOSE); $cur_state['pts'] = $update['pts']; $this->should_serialize = true; $pop_pts = true; @@ -372,7 +370,6 @@ trait UpdateHandler $pop_seq = true; } } - $this->save_update($update); if ($pop_pts) { @@ -447,11 +444,11 @@ trait UpdateHandler } if ($channel === false) { foreach ($updates as $update) { - $this->handle_update($update, $options); + $this->handle_update($update, $options); } } else { foreach ($updates as $update) { - $this->handle_update($update); + $this->handle_update($update); } } } @@ -468,6 +465,11 @@ trait UpdateHandler public function save_update($update) { + if ($update['_'] === 'updateDcOptions') { + \danog\MadelineProto\Logger::log(['Got new dc options'], \danog\MadelineProto\Logger::VERBOSE); + $this->parse_dc_options($update['dc_options']); + return; + } if (!$this->settings['updates']['handle_updates']) { return; } diff --git a/src/danog/MadelineProto/NothingInTheSocketException.php b/src/danog/MadelineProto/NothingInTheSocketException.php new file mode 100644 index 00000000..87b87e5d --- /dev/null +++ b/src/danog/MadelineProto/NothingInTheSocketException.php @@ -0,0 +1,17 @@ +. +*/ + +namespace danog\MadelineProto; + +class NothingInTheSocketException extends \Exception +{ +} diff --git a/src/danog/MadelineProto/RSA.php b/src/danog/MadelineProto/RSA.php index 19e286c2..7957d80c 100644 --- a/src/danog/MadelineProto/RSA.php +++ b/src/danog/MadelineProto/RSA.php @@ -25,12 +25,8 @@ class RSA $key = new \phpseclib\Crypt\RSA(); \danog\MadelineProto\Logger::log(['Loading key...'], Logger::ULTRA_VERBOSE); - if (method_exists($key, 'load')) { - $key->load($rsa_key); - } else { - $key->loadKey($rsa_key); - } - $this->keydata = ['n' => $key->modulus, 'e' => $key->exponent]; + $key->load($rsa_key); + $this->keydata = ['n' => \phpseclib\Common\Functions\Objects::getVar($key, 'modulus'), 'e' => \phpseclib\Common\Functions\Objects::getVar($key, 'exponent')]; \danog\MadelineProto\Logger::log(['Computing fingerprint...'], Logger::ULTRA_VERBOSE); $this->keydata['fp'] = \danog\PHP\Struct::unpack('. +*/ + +namespace danog\MadelineProto; + +/** + * Manages connection to telegram servers. + */ +class Stream +{ + public $pos = 0; + + public function __construct($string) + { + $this->string = $string; + } + + public function read($length) + { + $d = substr($this->string, $this->pos, $length); + $this->pos += $length; + return $d; + } +} diff --git a/src/danog/MadelineProto/TL/TL.php b/src/danog/MadelineProto/TL/TL.php index e82eaa81..4eeb491d 100644 --- a/src/danog/MadelineProto/TL/TL.php +++ b/src/danog/MadelineProto/TL/TL.php @@ -287,7 +287,7 @@ trait TL { $this->deserialize($bytes_io, $type); - return ftell($bytes_io); + return $bytes_io->pos; } /** @@ -295,49 +295,47 @@ trait TL */ public function deserialize($bytes_io, $type = ['type' => '']) { - if (!(!is_string($bytes_io) && (get_resource_type($bytes_io) === 'file' || get_resource_type($bytes_io) === 'stream'))) { - if (is_string($bytes_io)) { - $bytes_io = $this->fopen_and_write('php://memory', 'rw+b', $bytes_io); - } else { - throw new Exception('An invalid bytes_io handle was provided.'); - } + if (is_string($bytes_io)) { + $bytes_io = new \danog\MadelineProto\Stream($bytes_io); + } else if (!is_object($bytes_io)) { + throw new Exception('An invalid bytes_io handle was provided.'); } - //\danog\MadelineProto\Logger::log(['Deserializing '.$type['type'].' at byte '.ftell($bytes_io)); + //\danog\MadelineProto\Logger::log(['Deserializing '.$type['type'].' at byte '.$bytes_io->pos); switch ($type['type']) { case 'Bool': - return $this->deserialize_bool(stream_get_contents($bytes_io, 4)); + return $this->deserialize_bool($bytes_io->read(4)); case 'int': - return \danog\PHP\Struct::unpack('read(4))[0]; case '#': - return \danog\PHP\Struct::unpack('read(4))[0]; case 'long': - return \danog\PHP\Struct::unpack('read(8))[0]; case 'double': - return \danog\PHP\Struct::unpack('read(8))[0]; case 'int128': - return stream_get_contents($bytes_io, 16); + return $bytes_io->read(16); case 'int256': - return stream_get_contents($bytes_io, 32); + return $bytes_io->read(32); case 'int512': - return stream_get_contents($bytes_io, 32); + return $bytes_io->read(32); case 'string': case 'bytes': - $l = \danog\PHP\Struct::unpack('read(1))[0]; if ($l > 254) { throw new Exception('Length is too big'); } if ($l === 254) { - $long_len = \danog\PHP\Struct::unpack('read(3).chr(0))[0]; + $x = $bytes_io->read($long_len); $resto = $this->posmod(-$long_len, 4); if ($resto > 0) { - stream_get_contents($bytes_io, $resto); + $bytes_io->read($resto); } } else { - $x = stream_get_contents($bytes_io, $l); + $x = $bytes_io->read($l); $resto = $this->posmod(-($l + 1), 4); if ($resto > 0) { - stream_get_contents($bytes_io, $resto); + $bytes_io->read($resto); } } if (!is_string($x)) { @@ -348,14 +346,14 @@ trait TL case 'true': return true; case 'Vector t': - $id = \danog\PHP\Struct::unpack('read(4))[0]; $constructorData = $this->constructors->find_by_id($id); if ($constructorData === false) { throw new Exception('Could not extract type: '.$type['type'].' with id '.$id); } switch ($constructorData['predicate']) { case 'gzip_packed': - return $this->deserialize($this->fopen_and_write('php://memory', 'rw+b', gzdecode($this->deserialize($bytes_io, ['type' => 'string'])))); + return $this->deserialize(gzdecode($this->deserialize($bytes_io, ['type' => 'string']))); case 'Vector t': case 'vector': break; @@ -363,7 +361,7 @@ trait TL throw new Exception('Invalid vector constructor: '.$constructorData['predicate']); } case 'vector': - $count = \danog\PHP\Struct::unpack('read(4))[0]; $result = []; for ($i = 0; $i < $count; $i++) { $result[] = $this->deserialize($bytes_io, ['type' => $type['subtype']]); @@ -380,7 +378,7 @@ trait TL } else { $constructorData = $this->constructors->find_by_predicate($type['type']); if ($constructorData === false) { - $id = \danog\PHP\Struct::unpack('read(4))[0]; $constructorData = $this->constructors->find_by_id($id); if ($constructorData === false) { throw new Exception('Could not extract type: '.$type['type'].' with id '.$id); @@ -388,7 +386,7 @@ trait TL } } if ($constructorData['predicate'] === 'gzip_packed') { - return $this->deserialize($this->fopen_and_write('php://memory', 'rw+b', gzdecode($this->deserialize($bytes_io, ['type' => 'string'])))); + return $this->deserialize(gzdecode($this->deserialize($bytes_io, ['type' => 'string']))); } $x = ['_' => $constructorData['predicate']]; foreach ($constructorData['params'] as $arg) { diff --git a/src/danog/MadelineProto/Threads/SocketHandler.php b/src/danog/MadelineProto/Threads/SocketHandler.php new file mode 100644 index 00000000..0aa0f69a --- /dev/null +++ b/src/danog/MadelineProto/Threads/SocketHandler.php @@ -0,0 +1,112 @@ +. +*/ + +namespace danog\MadelineProto\Threads; + +/** + * Manages packing and unpacking of messages, and the list of sent and received messages. + */ +class SocketHandler extends Threaded +{ + public $payloads = []; + public function __construct(&$me) { + $this->API = $me; + } + /** + * Reading connection and receiving message from server. Check the CRC32. + */ + public function run() + { + $this->socket_handler->synchronized(function ($thread) { + if (empty($thread->payloads)) { + $thread->wait(); + } else { + foreach ($thread->payloads as $payload) { + if (fstat($payload)['size'] === 4) { + $error = \danog\PHP\Struct::unpack('API->datacenter->temp_auth_key != null) { + \danog\MadelineProto\Logger::log(['WARNING: Resetting auth key...'], \danog\MadelineProto\Logger::WARNING); + $thread->API->datacenter->temp_auth_key = null; + $thread->API->init_authorization(); + $thread->API->config = $thread->API->write_client_info('help.getConfig'); + $thread->API->parse_config(); + continue; + //throw new \danog\MadelineProto\Exception('I had to recreate the temporary authorization key'); + } + } + throw new \danog\MadelineProto\RPCErrorException($error, $error); + } + $auth_key_id = stream_get_contents($payload, 8); + if ($auth_key_id === str_repeat(chr(0), 8)) { + list($message_id, $message_length) = \danog\PHP\Struct::unpack('API->check_message_id($message_id, false); + $message_data = stream_get_contents($payload, $message_length); + } elseif ($auth_key_id === $thread->API->datacenter->temp_auth_key['id']) { + $message_key = stream_get_contents($payload, 16); + $encrypted_data = stream_get_contents($payload); + list($aes_key, $aes_iv) = $thread->API->aes_calculate($message_key, $thread->API->datacenter->temp_auth_key['auth_key'], 'from server'); + $decrypted_data = $thread->API->ige_decrypt($encrypted_data, $aes_key, $aes_iv); + + $server_salt = \danog\PHP\Struct::unpack('API->datacenter->temp_auth_key['server_salt']) { + //\danog\MadelineProto\Logger::log(['WARNING: Server salt mismatch (my server salt '.$thread->API->datacenter->temp_auth_key['server_salt'].' is not equal to server server salt '.$server_salt.').'], \danog\MadelineProto\Logger::WARNING); + } + + $session_id = substr($decrypted_data, 8, 8); + if ($session_id != $thread->API->datacenter->session_id) { + throw new \danog\MadelineProto\Exception('Session id mismatch.'); + } + + $message_id = \danog\PHP\Struct::unpack('API->check_message_id($message_id, false); + + $seq_no = \danog\PHP\Struct::unpack(' strlen($decrypted_data)) { + throw new \danog\MadelineProto\Exception('message_data_length is too big'); + } + + if ((strlen($decrypted_data) - 32) - $message_data_length > 15) { + throw new \danog\MadelineProto\Exception('difference between message_data_length and the length of the remaining decrypted buffer is too big'); + } + + if ($message_data_length < 0) { + throw new \danog\MadelineProto\Exception('message_data_length not positive'); + } + + if ($message_data_length % 4 != 0) { + throw new \danog\MadelineProto\Exception('message_data_length not divisible by 4'); + } + + $message_data = substr($decrypted_data, 32, $message_data_length); + if ($message_key != substr(sha1(substr($decrypted_data, 0, 32 + $message_data_length), true), -16)) { + throw new \danog\MadelineProto\Exception('msg_key mismatch'); + } + $thread->API->datacenter->incoming_messages[$message_id]['seq_no'] = $seq_no; + } else { + throw new \danog\MadelineProto\Exception('Got unknown auth_key id'); + } + $deserialized = $thread->API->deserialize($message_data); + $thread->API->datacenter->incoming_messages[$message_id]['content'] = $deserialized; + $thread->API->datacenter->incoming_messages[$message_id]['response'] = -1; + $thread->API->datacenter->new_incoming[$message_id] = $message_id; + $thread->API->handle_messages(); + } + } + }, $this); + + } +} diff --git a/src/danog/MadelineProto/Threads/SocketReader.php b/src/danog/MadelineProto/Threads/SocketReader.php new file mode 100644 index 00000000..5a806203 --- /dev/null +++ b/src/danog/MadelineProto/Threads/SocketReader.php @@ -0,0 +1,36 @@ +. +*/ + +namespace danog\MadelineProto\Threads; + +/** + * Manages packing and unpacking of messages, and the list of sent and received messages. + */ +class SocketReader extends Threaded +{ + public function __construct(&$me) { + $this->API = $me; + } + /** + * Reading connection and receiving message from server. Check the CRC32. + */ + public function run() + { + try { + $payload = $this->API->datacenter->read_message(); + $this->socket_handler->synchronized(function ($thread, $payload) { + $thread->payloads[] = $payload; + $thread->notify(); + }, $this->API->socket_handler, $payload); + } catch (\danog\MadelineProto\NothingInTheSocketException $e) { ; }; + } +} diff --git a/src/danog/MadelineProto/Tools.php b/src/danog/MadelineProto/Tools.php index 4e25e183..1cd433bc 100644 --- a/src/danog/MadelineProto/Tools.php +++ b/src/danog/MadelineProto/Tools.php @@ -40,15 +40,6 @@ trait Tools return $resto; } - public function fopen_and_write($filename, $mode, $data) - { - $handle = fopen($filename, $mode); - fwrite($handle, $data); - rewind($handle); - - return $handle; - } - public function utf8ize($d) { if (is_array($d)) {