MadelineProto/src/danog/MadelineProto/MTProto.php

369 lines
17 KiB
PHP

<?php
/*
Copyright 2016-2017 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;
/**
* Manages all of the mtproto stuff.
*/
class MTProto extends PrimeModule
{
use \danog\MadelineProto\MTProtoTools\AckHandler;
use \danog\MadelineProto\MTProtoTools\AuthKeyHandler;
use \danog\MadelineProto\MTProtoTools\CallHandler;
use \danog\MadelineProto\MTProtoTools\Crypt;
use \danog\MadelineProto\MTProtoTools\MessageHandler;
use \danog\MadelineProto\MTProtoTools\MsgIdHandler;
use \danog\MadelineProto\MTProtoTools\PeerHandler;
use \danog\MadelineProto\MTProtoTools\ResponseHandler;
use \danog\MadelineProto\MTProtoTools\SaltHandler;
use \danog\MadelineProto\MTProtoTools\SeqNoHandler;
use \danog\MadelineProto\MTProtoTools\UpdateHandler;
use \danog\MadelineProto\TL\TL;
use \danog\MadelineProto\Tools;
public $settings = [];
public $config = ['expires' => -1];
public $ipv6 = false;
public $should_serialize = true;
public function __construct($settings = [])
{
// Parse settings
$this->parse_settings($settings);
// Connect to servers
\danog\MadelineProto\Logger::log(['Istantiating DataCenter...'], Logger::ULTRA_VERBOSE);
if (isset($this->datacenter)) {
$this->datacenter->__construct($this->settings['connection'], $this->settings['connection_settings']);
} else {
$this->datacenter = new DataCenter($this->settings['connection'], $this->settings['connection_settings']);
}
// Load rsa key
\danog\MadelineProto\Logger::log(['Loading RSA key...'], Logger::ULTRA_VERBOSE);
$this->key = new RSA($this->settings['authorization']['rsa_key']);
// Istantiate TL class
\danog\MadelineProto\Logger::log(['Translating tl schemas...'], Logger::ULTRA_VERBOSE);
$this->construct_TL($this->settings['tl_schema']['src']);
$this->switch_dc(2, true);
$this->get_config();
}
public function __wakeup()
{
$this->setup_logger();
if (!isset($this->v) || $this->v !== $this->getV()) {
\danog\MadelineProto\Logger::log(['Serialization is out of date, reconstructing object!'], Logger::WARNING);
$this->__construct($this->settings);
$this->v = $this->getV();
}
$this->datacenter->__construct($this->settings['connection'], $this->settings['connection_settings']);
$this->reset_session();
if ($this->datacenter->authorized && $this->settings['updates']['handle_updates']) {
\danog\MadelineProto\Logger::log(['Getting updates after deserialization...'], Logger::NOTICE);
$this->get_updates_difference();
}
}
public function parse_settings($settings)
{
// Detect ipv6
$google = '';
try {
$ctx = stream_context_create(['http'=> [
'timeout' => 1,
],
]);
$google = file_get_contents('https://ipv6.google.com', false, $ctx);
} catch (Exception $e) {
}
$this->ipv6 = strlen($google) > 0;
// Detect device model
try {
$device_model = php_uname('s');
} catch (Exception $e) {
$device_model = 'Web server';
}
// Detect system version
try {
$system_version = php_uname('r');
} catch (Exception $e) {
$system_version = phpversion();
}
// Set default settings
$default_settings = [
'authorization' => [ // Authorization settings
'default_temp_auth_key_expires_in' => 31557600, // validity of temporary keys and the binding of the temporary and permanent keys
'rsa_key' => '-----BEGIN RSA PUBLIC KEY-----
MIIBCgKCAQEAwVACPi9w23mF3tBkdZz+zwrzKOaaQdr01vAbU4E1pvkfj4sqDsm6
lyDONS789sVoD/xCS9Y0hkkC3gtL1tSfTlgCMOOul9lcixlEKzwKENj1Yz/s7daS
an9tqw3bfUV/nqgbhGX81v/+7RFAEd+RwFnK7a+XYl9sluzHRyVVaTTveB2GazTw
Efzk2DWgkBluml8OREmvfraX3bkHZJTKX4EQSjBbbdJ2ZXIsRrYOXfaA+xayEGB+
8hdlLmAjbCVfaigxX0CDqWeR1yFL9kwd9P0NsZRPsmoqVwMbMu7mStFai6aIhc3n
Slv8kg9qv1m6XHVQY3PnEw+QQtqSIXklHwIDAQAB
-----END RSA PUBLIC KEY-----', // RSA public key
],
'connection' => [ // List of datacenters/subdomains where to connect
'ssl_subdomains' => [ // Subdomains of web.telegram.org for https protocol
1 => 'pluto',
2 => 'venus',
3 => 'aurora',
4 => 'vesta',
5 => 'flora', // musa oh wait no :(
],
'test' => [ // Test datacenters
'ipv4' => [ // ipv4 addresses
2 => [ // The rest will be fetched using help.getConfig
'ip_address' => '149.154.167.40',
'port' => 443,
'media_only' => false,
'tcpo_only' => false,
],
],
'ipv6' => [ // ipv6 addresses
2 => [ // The rest will be fetched using help.getConfig
'ip_address' => '2001:067c:04e8:f002:0000:0000:0000:000e',
'port' => 443,
'media_only' => false,
'tcpo_only' => false,
],
],
],
'main' => [ // Main datacenters
'ipv4' => [ // ipv4 addresses
2 => [ // The rest will be fetched using help.getConfig
'ip_address' => '149.154.167.51',
'port' => 443,
'media_only' => false,
'tcpo_only' => false,
],
],
'ipv6' => [ // ipv6 addresses
2 => [ // The rest will be fetched using help.getConfig
'ip_address' => '2001:067c:04e8:f002:0000:0000:0000:000a',
'port' => 443,
'media_only' => false,
'tcpo_only' => false,
],
],
],
],
'connection_settings' => [ // connection settings
'all' => [ // These settings will be applied on every datacenter that hasn't a custom settings subarray...
'protocol' => 'tcp_full', // can be tcp_full, tcp_abridged, tcp_intermediate, http, https, udp (unsupported)
'test_mode' => false, // decides whether to connect to the main telegram servers or to the testing servers (deep telegram)
'ipv6' => $this->ipv6, // decides whether to use ipv6, ipv6 attribute of API attribute of API class contains autodetected boolean
'timeout' => 3, // timeout for sockets
],
],
'app_info' => [ // obtained in https://my.telegram.org
'api_id' => 65536,
'api_hash' => '4251a2777e179232705e2462706f4143',
'device_model' => $device_model,
'system_version' => $system_version,
'app_version' => 'Unicorn', // 🌚
'lang_code' => 'en',
],
'tl_schema' => [ // TL scheme files
'layer' => 62, // layer version
'src' => [
'mtproto' => __DIR__.'/TL_mtproto_v1.json', // mtproto TL scheme
'telegram' => __DIR__.'/TL_telegram_v62.tl', // telegram TL scheme
],
],
'logger' => [ // Logger settings
/*
* logger modes:
* 0 - No logger
* 1 - Log to the default logger destination
* 2 - Log to file defined in second parameter
* 3 - Echo logs
*/
'logger' => 1, // write to
'logger_param' => '/tmp/MadelineProto.log',
'logger' => 3, // overwrite previous setting and echo logs
'logger_level' => Logger::VERBOSE, // Logging level, available logging levels are: ULTRA_VERBOSE, VERBOSE, NOTICE, WARNING, ERROR, FATAL_ERROR. Can be provided as last parameter to the logging function.
],
'max_tries' => [
'query' => 5, // How many times should I try to call a method or send an object before throwing an exception
'authorization' => 5, // How many times should I try to generate an authorization key before throwing an exception
'response' => 5, // How many times should I try to get a response of a query before throwing an exception
],
'flood_timeout' => [
'wait_if_lt' => 20, // Sleeps if flood block time is lower than this
],
'msg_array_limit' => [ // How big should be the arrays containing the incoming and outgoing messages?
'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
],
'pwr' => ['pwr' => false, 'db_token' => false, 'strict' => false],
];
$settings = array_replace_recursive($default_settings, $settings);
if (isset($settings['connection_settings']['all'])) {
for ($n = 1; $n <= 6; $n++) {
if (!isset($settings['connection_settings'][$n])) {
$settings['connection_settings'][$n] = $settings['connection_settings']['all'];
}
}
unset($settings['connection_settings']['all']);
}
$this->settings = $settings;
// Setup logger
$this->setup_logger();
}
public function setup_logger()
{
\danog\MadelineProto\Logger::constructor($this->settings['logger']['logger'], $this->settings['logger']['logger_param'], isset($this->datacenter->authorization['user']) ? (isset($this->datacenter->authorization['user']['username']) ? $this->datacenter->authorization['user']['username'] : $this->datacenter->authorization['user']['id']) : '', isset($this->settings['logger']['logger_level']) ? $this->settings['logger']['logger_level'] : Logger::VERBOSE);
}
public function reset_session($de = true)
{
foreach ($this->datacenter->sockets as $id => &$socket) {
if ($de) {
\danog\MadelineProto\Logger::log(['Resetting session id and seq_no in DC '.$id.'...'], Logger::VERBOSE);
$socket->session_id = $this->random(8);
$socket->seq_no = 0;
}
$socket->incoming_messages = [];
$socket->outgoing_messages = [];
$socket->new_outgoing = [];
$socket->new_incoming = [];
}
}
// Switches to a new datacenter and if necessary creates authorization keys, binds them and writes client info
public function switch_dc($new_dc, $allow_nearest_dc_switch = false)
{
$old_dc = $this->datacenter->curdc;
\danog\MadelineProto\Logger::log(['Switching from DC '.$old_dc.' to DC '.$new_dc.'...'], Logger::NOTICE);
if (!isset($this->datacenter->sockets[$new_dc])) {
$this->datacenter->dc_connect($new_dc);
$this->init_authorization();
$this->get_config($this->write_client_info('help.getConfig'));
$this->get_nearest_dc($allow_nearest_dc_switch);
}
$this->datacenter->curdc = $new_dc;
if (
(isset($this->datacenter->sockets[$old_dc]->authorized) && $this->datacenter->sockets[$old_dc]->authorized) &&
!(isset($this->datacenter->sockets[$new_dc]->authorized) && $this->datacenter->sockets[$new_dc]->authorized && $this->datacenter->sockets[$new_dc]->authorization['user']['id'] === $this->datacenter->sockets[$old_dc]->authorization['user']['id'])
) {
\danog\MadelineProto\Logger::log(['Copying authorization...'], Logger::VERBOSE);
$this->should_serialize = true;
$this->datacenter->curdc = $old_dc;
$exported_authorization = $this->method_call('auth.exportAuthorization', ['dc_id' => $new_dc]);
$this->datacenter->curdc = $new_dc;
if (isset($this->datacenter->sockets[$new_dc]->authorized) && $this->datacenter->sockets[$new_dc]->authorized && $this->datacenter->sockets[$new_dc]->authorization['user']['id'] !== $this->datacenter->sockets[$old_dc]->authorization['user']['id']) {
$this->method_call('auth.logOut');
}
$this->datacenter->authorization = $this->method_call('auth.importAuthorization', $exported_authorization);
$this->datacenter->authorized = true;
}
\danog\MadelineProto\Logger::log(['Done! Current DC is '.$this->datacenter->curdc], Logger::NOTICE);
}
// Creates authorization keys
public function init_authorization()
{
if ($this->datacenter->session_id === null) {
$this->datacenter->session_id = $this->random(8);
}
if ($this->datacenter->temp_auth_key === null || $this->datacenter->auth_key === null) {
if ($this->datacenter->auth_key === null) {
\danog\MadelineProto\Logger::log(['Generating permanent authorization key...'], Logger::NOTICE);
$this->datacenter->auth_key = $this->create_auth_key(-1);
$this->should_serialize = true;
}
\danog\MadelineProto\Logger::log(['Generating temporary authorization key...'], Logger::NOTICE);
$this->datacenter->temp_auth_key = $this->create_auth_key($this->settings['authorization']['default_temp_auth_key_expires_in']);
$this->bind_temp_auth_key($this->settings['authorization']['default_temp_auth_key_expires_in']);
if (in_array($this->datacenter->protocol, ['http', 'https'])) {
$this->method_call('http_wait', ['max_wait' => 0, 'wait_after' => 0, 'max_delay' => 0]);
}
}
}
public function write_client_info($method, $arguments = [])
{
\danog\MadelineProto\Logger::log(['Writing client info (also executing '.$method.')...'], Logger::NOTICE);
return $this->method_call(
'invokeWithLayer',
[
'layer' => $this->settings['tl_schema']['layer'],
'query' => $this->serialize_method('initConnection',
array_merge(
$this->settings['app_info'],
['query' => $this->serialize_method($method, $arguments)]
)
),
]
);
}
public function get_nearest_dc($allow_switch)
{
$nearest_dc = $this->method_call('help.getNearestDc');
\danog\MadelineProto\Logger::log(["We're in ".$nearest_dc['country'].', current dc is '.$nearest_dc['this_dc'].', nearest dc is '.$nearest_dc['nearest_dc'].'.'], Logger::NOTICE);
if ($nearest_dc['nearest_dc'] != $nearest_dc['this_dc'] && $allow_switch) {
$this->switch_dc($nearest_dc['nearest_dc']);
$this->settings['connection_settings']['default_dc'] = $nearest_dc['nearest_dc'];
$this->should_serialize = true;
}
}
public function get_config($config = [])
{
if ($this->config['expires'] > time()) {
return;
}
$this->config = empty($config) ? $this->method_call('help.getConfig') : $config;
$this->should_serialize = true;
$this->parse_config();
}
public function parse_config()
{
$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;
}
}
public function getV()
{
return 1;
}
}