diff --git a/socket.php b/server.php similarity index 67% rename from socket.php rename to server.php index d2c5da4c..d79ba168 100644 --- a/socket.php +++ b/server.php @@ -2,5 +2,5 @@ require 'vendor/autoload.php'; -$handler = new \danog\MadelineProto\Server(['type' => AF_INET, 'protocol' => 0, 'address' => 'localhost', 'port' => 8002]); +$handler = new \danog\MadelineProto\Server(['type' => AF_INET, 'protocol' => 0, 'address' => 'localhost', 'port' => 8005]); $handler->start(); diff --git a/src/danog/MadelineProto/API.php b/src/danog/MadelineProto/API.php index 737ac96b..18deed2b 100644 --- a/src/danog/MadelineProto/API.php +++ b/src/danog/MadelineProto/API.php @@ -114,7 +114,7 @@ class API extends APIFactory return; } $this->serialize(); - restore_error_handler(); + //restore_error_handler(); } public function __sleep() diff --git a/src/danog/MadelineProto/Server/Handler.php b/src/danog/MadelineProto/Server/Handler.php index d815774a..b4a207d4 100644 --- a/src/danog/MadelineProto/Server/Handler.php +++ b/src/danog/MadelineProto/Server/Handler.php @@ -19,6 +19,10 @@ namespace danog\MadelineProto\Server; class Handler extends \danog\MadelineProto\Connection { use \danog\MadelineProto\TL\TL; + use \danog\MadelineProto\TL\Conversion\BotAPI; + use \danog\MadelineProto\TL\Conversion\BotAPIFiles; + use \danog\MadelineProto\TL\Conversion\Extension; + use \danog\MadelineProto\TL\Conversion\TD; use \danog\MadelineProto\Tools; private $madeline; @@ -26,6 +30,7 @@ class Handler extends \danog\MadelineProto\Connection { $this->sock = $socket; $this->sock->setBlocking(true); + $this->must_open = false; $timeout = 2; $this->sock->setOption(\SOL_SOCKET, \SO_RCVTIMEO, $timeout); $this->sock->setOption(\SOL_SOCKET, \SO_SNDTIMEO, $timeout); @@ -34,7 +39,7 @@ class Handler extends \danog\MadelineProto\Connection public function __destruct() { - \danog\MadelineProto\Logger::log('Closing socket in fork '.getmypid()); + echo('Closing socket in fork '.getmypid().PHP_EOL); unset($this->sock); $this->destruct_madeline(); exit(); @@ -58,6 +63,7 @@ class Handler extends \danog\MadelineProto\Connection $buffer = ''; $first_byte = $this->sock->read(1); + if ($first_byte === chr(239)) { $this->protocol = 'tcp_abridged'; } else { @@ -133,11 +139,10 @@ class Handler extends \danog\MadelineProto\Connection $args[1]['updates']['callback'] = [$this, 'update_handler']; } $this->madeline = new \danog\MadelineProto\API(...$args); - return true; } if ($method[0] === '__destruct') { - return $this->destruct_madeline(); + return $this->__destruct(); } if ($this->madeline === null) { throw new \danog\MadelineProto\Exception('__construct was not called'); @@ -199,38 +204,55 @@ class Handler extends \danog\MadelineProto\Connection $tl_frame['function'] = $frame['function']; } if (isset($frame['args'])) { - $tl_frame['args'] = json_encode($frame['args']); + $args = json_encode($frame['args']); + if ($args !== false) $tl_frame['args'] = $args; } $tl = false; } $exception['trace']['frames'][] = $tl_frame; } - $this->send_message($this->serialize_object(['type' => 'socketMessageException'], ['request_id' => $request_id, 'exception' => $exception])); + $this->send_message_safe($this->serialize_object(['type' => ''], ['_' => 'socketMessageException', 'request_id' => $request_id, 'exception' => $exception], 'exception')); } public function send_response($request_id, $response) { - $this->send_message($this->serialize_object(['type' => 'socketMessageResponse'], ['request_id' => $request_id, 'data' => $response])); + $this->send_message_safe($this->serialize_object(['type' => ''], ['_' => 'socketMessageResponse', 'request_id' => $request_id, 'data' => $response], 'exception')); } public function send_data($stream_id, $data) { - $this->send_message($this->serialize_object(['type' => 'socketMessageRawData'], ['stream_id' => $stream_id, 'data' => $data])); + $this->send_message_safe($this->serialize_object(['type' => ''], ['_' => 'socketMessageRawData', 'stream_id' => $stream_id, 'data' => $data], 'data')); } - + public $logging = false; public function logger($message, $level) { - $message = ['_' => 'socketMessageLog', 'data' => $message, 'level' => $level, 'thread' => \danog\MadelineProto\Logger::$has_thread && is_object(\Thread::getCurrentThread()), 'process' => \danog\MadelineProto\Logger::is_fork(), 'file' => basename(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1)[0]['file'], '.php')]; - $this->send_message($this->serialize_object(['type' => 'socketMessageLog'], $message)); - } + if (!$this->logging) { + try { + $this->logging = true; + $message = ['_' => 'socketMessageLog', 'data' => $message, 'level' => $level, 'thread' => \danog\MadelineProto\Logger::$has_thread && is_object(\Thread::getCurrentThread()), 'process' => \danog\MadelineProto\Logger::is_fork(), 'file' => basename(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]['file'], '.php')]; + + + $this->send_message_safe($this->serialize_object(['type' => ''], $message, 'log')); + } finally { + $this->logging = false; + } + } + } + public function send_message_safe($message) { + if (!isset($this->sock)) return false; + try { + $this->send_message($message); + } catch (\danog\MadelineProto\Exception $e) { + $this->__destruct(); + } + } public function update_handler($update) { - $this->send_message($this->serialize_object(['type' => 'socketMessageUpdate'], ['data' => $update])); + $this->send_message_safe($this->serialize_object(['type' => ''], ['_' => 'socketMessageUpdate', 'data' => $update], 'update')); } - public function __call($method, $args) { - $this->send_message($this->serialize_object(['type' => 'socketMessageRequest'], ['request_id' => 0, 'method' => $method, 'args' => $args])); + $this->send_message_safe($this->serialize_object(['type' => ''], ['_' => 'socketMessageRequest', 'request_id' => 0, 'method' => $method, 'args' => $args], 'method')); } } diff --git a/src/danog/MadelineProto/TL/TL.php b/src/danog/MadelineProto/TL/TL.php index e4e8165b..23de97a3 100644 --- a/src/danog/MadelineProto/TL/TL.php +++ b/src/danog/MadelineProto/TL/TL.php @@ -499,7 +499,8 @@ trait TL throw new Exception(\danog\MadelineProto\Lang::$current_lang['params_missing'], $current_argument['name']); } - if ($current_argument['type'] === 'DataJSON') { + + if (in_array($current_argument['type'], ['DataJSON', '%DataJSON'])) { $arguments[$current_argument['name']] = ['_' => 'dataJSON', 'data' => json_encode($arguments[$current_argument['name']])]; } if (!is_array($arguments[$current_argument['name']]) && $current_argument['type'] === 'InputFile' && $this->settings['upload']['allow_automatic_upload']) { diff --git a/src/danog/MadelineProto/TL_socket.tl b/src/danog/MadelineProto/TL_socket.tl index b59ec856..8d757bdc 100644 --- a/src/danog/MadelineProto/TL_socket.tl +++ b/src/danog/MadelineProto/TL_socket.tl @@ -1,11 +1,10 @@ - dataJSON#7d748d04 data:string = DataJSON; socketMessageRequest request_id:int method:vector args:vector<%DataJSON> = SocketMessage; socketMessageResponse request_id:int data:%DataJSON = SocketMessage; socketMessageException request_id:int exception:SocketException = SocketMessage; socketMessageUpdate data:%DataJSON = SocketMessage; -socketMessageLog flags:# thread:flags.0?true process:flags.1?true file:flags.2?string level:int data:string = SocketMessage; +socketMessageLog flags:# thread:flags.0?true process:flags.1?true file:string level:int data:%DataJSON = SocketMessage; socketMessageRawData stream_id:int data:bytes = SocketMessage; socketException message:string code:int trace:%SocketTLTrace = SocketException; @@ -15,4 +14,4 @@ socketDOMException message:string code:int trace:%SocketTLTrace = SocketExceptio socketTLTrace frames:vector<%SocketTLFrame> = SocketTLTrace; -socketTLFrame flags:# file:flags.0?string line:flags.1?string function:flags.2?string args:flags.3?string tl_param:flags.4?string = SocketTLFrame; \ No newline at end of file +socketTLFrame flags:# file:flags.0?string line:flags.1?int function:flags.2?string args:flags.3?string tl_param:flags.4?string = SocketTLFrame; diff --git a/src/danog/MadelineProto/VoIP.php b/src/danog/MadelineProto/VoIP.php new file mode 100644 index 00000000..20b0292d --- /dev/null +++ b/src/danog/MadelineProto/VoIP.php @@ -0,0 +1,282 @@ +. +*/ + +namespace danog\MadelineProto; + +if (!extension_loaded('php-libtgvoip') && false) { + class VoIP + { + use \danog\MadelineProto\VoIP\MessageHandler; + + const PHP_LIBTGVOIP_VERSION = '1.1.2'; + 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; + const NET_TYPE_3G = 3; + const NET_TYPE_HSPA = 4; + const NET_TYPE_LTE = 5; + const NET_TYPE_WIFI = 6; + const NET_TYPE_ETHERNET = 7; + const NET_TYPE_OTHER_HIGH_SPEED = 8; + const NET_TYPE_OTHER_LOW_SPEED = 9; + const NET_TYPE_DIALUP = 10; + const NET_TYPE_OTHER_MOBILE = 11; + + const DATA_SAVING_NEVER = 0; + const DATA_SAVING_MOBILE = 1; + const DATA_SAVING_ALWAYS = 2; + + const PROXY_NONE = 0; + const PROXY_SOCKS5 = 1; + + const AUDIO_STATE_NONE = -1; + const AUDIO_STATE_CREATED = 0; + 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; + + private $MadelineProto; + public $configuration = ['endpoints' => [], 'shared_config' => []]; + public $storage = []; + public $internalStorage = []; + private $signal = 0; + private $callState; + private $callID; + private $creatorID; + private $otherID; + private $protocol; + private $visualization; + private $holdFiles = []; + private $inputFiles; + private $outputFile; + private $isPlaying = false; + + private $connection_settings = []; + private $dclist = []; + + private $datacenter; + + public function __construct($creator, $otherID, $callID, $MadelineProto, $callState, $protocol) + { + $this->creator = $creator; + $this->otherID = $otherID; + $this->callID = $callID; + $this->MadelineProto = $MadelineProto; + $this->callState = $callState; + $this->protocol = $protocol; + } + + public function deInitVoIPController() + { + } + + public function setVisualization($visualization) + { + $this->visualization = $visualization; + } + + public function getVisualization() + { + return $this->visualization; + } + + public function discard($reason = ['_' => 'phoneCallDiscardReasonDisconnect'], $rating = [], $debug = false) + { + if ($this->callState === self::CALL_STATE_ENDED || empty($this->configuration)) { + return false; + } + $this->MadelineProto->discard_call($this->callID, $reason, $rating, $debug); + $this->deinitVoIPController(); + + return $this; + } + + public function accept() + { + if ($this->callState !== self::CALL_STATE_INCOMING) { + return false; + } + $this->callState = self::CALL_STATE_ACCEPTED; + if (!$this->MadelineProto->accept_call($this->callID)) { + $this->discard_call(['_' => 'phoneCallDiscardReasonDisconnect']); + + return false; + } + + return $this; + } + + public function close() + { + $this->deinitVoIPController(); + } + + public function startTheMagic() + { + return $this; + } + + public function play($file) + { + $this->inputFiles[] = $file; + + return $this; + } + + public function playOnHold($files) + { + $this->holdFiles = $files; + } + + public function setOutputFile($file) + { + $this->outputFile = $file; + } + + public function unsetOutputFile() + { + $this->outputFile = null; + } + + public function setMadeline($MadelineProto) + { + $this->MadelineProto = $MadelineProto; + } + + public function getProtocol() + { + return $this->protocol; + } + + public function getOtherID() + { + return $this->otherID; + } + + public function getCallID() + { + return $this->callID; + } + + public function isCreator() + { + return $this->creator; + } + + public function whenCreated() + { + return isset($this->internalStorage['created']) ? $this->internalStorage['created'] : false; + } + public function parseConfig() + { + if (count($this->configuration['endpoints'])) { + $this->connection_settings['all'] = $this->MadelineProto->settings['connection_settings']['all']; + $this->connection_settings['all']['protocol'] = 'obfuscated2'; + $this->connection_settings['all']['do_not_retry'] = true; + + $test = $this->connection_settings['all']['test_mode'] ? 'test' : 'main'; + foreach ($this->configuration['endpoints'] as $endpoint) { + $this->dclist[$test]['ipv6'][$endpoint['id']] = ['ip_address' => $endpoint['ipv6'], 'port' => $endpoint['port'], 'peer_tag' => $endpoint['peer_tag']]; + $this->dclist[$test]['ipv4'][$endpoint['id']] = ['ip_address' => $endpoint['ip'], 'port' => $endpoint['port'], 'peer_tag' => $endpoint['peer_tag']]; + } + if (!isset($this->datacenter)) { + $this->datacenter = new DataCenter($this->dclist, $this->connection_settings); + } else { + //$this->datacenter->__construct($this->dclist, $this->connection_settings); + } + foreach ($this->datacenter->get_dcs() as $new_dc) { + $this->datacenter->dc_connect($new_dc); + } + $this->init_all(); + foreach ($this->datacenter->get_dcs(false) as $new_dc) { + $this->datacenter->dc_connect($new_dc); + } + $this->init_all(); + } + } + + private function init_all() + { + $test = $this->connection_settings['all']['test_mode'] ? 'test' : 'main'; + foreach ($this->datacenter->sockets as $dc_id => $socket) { + if ($socket->auth_key === null) { + $socket->auth_key = ['id' => $this->configuration['auth_key_id'], 'auth_key' => $this->configuration['auth_key'], 'connection_inited' => self::STATE_CREATED]; + } + if ($socket->type === Connection::API_ENDPOINT) { + $socket->type = Connection::VOIP_TCP_REFLECTOR_ENDPOINT; + } + if ($socket->peer_tag === null) { + switch ($socket->type) { + case Connection::VOIP_TCP_REFLECTOR_ENDPOINT: + case Connection::VOIP_UDP_REFLECTOR_ENDPOINT: + $socket->peer_tag = $this->dclist[$test]['ipv4'][$dc_id]['peer_tag']; + break; + default: + $socket->peer_tag = $this->configuration['call_id']; + } + } + if ($socket->auth_key['connection_inited'] === self::STATE_CREATED) { + + } + } + } + + public function getCallState() + { + return $this->callState; + } + + public function getVersion() + { + return 'libponyvoip-1.0'; + } + + public function getPreferredRelayID() + { + return 0; + } + + public function getLastError() + { + return ''; + } + + public function getDebugLog() + { + return ''; + } + + public function getSignalBarsCount() + { + return $this->signal; + } + } +} diff --git a/src/danog/MadelineProto/VoIP/MessageHandler.php b/src/danog/MadelineProto/VoIP/MessageHandler.php new file mode 100644 index 00000000..9368af85 --- /dev/null +++ b/src/danog/MadelineProto/VoIP/MessageHandler.php @@ -0,0 +1,152 @@ +. +*/ + +namespace danog\MadelineProto\VoIP; + +/** + * Manages packing and unpacking of messages, and the list of sent and received messages. + */ +trait MessageHandler +{ + public function send_message($message, $datacenter) + { + //$has_ack = false; + + if (count($this->datacenter->sockets[$datacenter]->object_queue) > 1) { + $messages = []; + \danog\MadelineProto\Logger::log("Sending msg_container as encrypted message to DC $datacenter", \danog\MadelineProto\Logger::ULTRA_VERBOSE); + + foreach ($this->datacenter->sockets[$datacenter]->object_queue as $message) { + $message['seqno'] = $this->generate_out_seq_no($datacenter, $message['content_related']); + $message['bytes'] = strlen($message['body']); + //$has_ack = $has_ack || $message['_'] === 'msgs_ack'; + \danog\MadelineProto\Logger::log("Inside of msg_container, sending {$message['_']} as encrypted message to DC $datacenter", \danog\MadelineProto\Logger::ULTRA_VERBOSE); + $message['_'] = 'MTmessage'; + $messages[] = $message; + $this->datacenter->sockets[$datacenter]->outgoing_messages[$message['msg_id']] = ['seq_no' => $message['seqno'], 'response' => -1]; //, 'content' => $this->deserialize($message['body'], ['type' => '', 'datacenter' => $datacenter])]; + } + $message_data = $this->serialize_object(['type' => ''], ['_' => 'msg_container', 'messages' => $messages], 'lol'); + $message_id = $this->generate_message_id($datacenter); + $seq_no = $this->generate_out_seq_no($datacenter, false); + } elseif (count($this->datacenter->sockets[$datacenter]->object_queue)) { + $message = array_shift($this->datacenter->sockets[$datacenter]->object_queue); + \danog\MadelineProto\Logger::log("Sending {$message['_']} as encrypted message to DC $datacenter", \danog\MadelineProto\Logger::ULTRA_VERBOSE); + $message_data = $message['body']; + $message_id = $message['msg_id']; + $seq_no = $this->generate_out_seq_no($datacenter, $message['content_related']); + } else { + return; + } + $plaintext = $this->datacenter->sockets[$datacenter]->temp_auth_key['server_salt'].$this->datacenter->sockets[$datacenter]->session_id.$message_id.pack('VV', $seq_no, strlen($message_data)).$message_data; + $padding = $this->posmod(-strlen($plaintext), 16); + if ($padding < 12) { + $padding += 16; + } + $padding = $this->random($padding); + $message_key = substr(hash('sha256', substr($this->datacenter->sockets[$datacenter]->temp_auth_key['auth_key'], 88, 32).$plaintext.$padding, true), 8, 16); + list($aes_key, $aes_iv) = $this->aes_calculate($message_key, $this->datacenter->sockets[$datacenter]->temp_auth_key['auth_key']); + $message = $this->datacenter->sockets[$datacenter]->temp_auth_key['id'].$message_key.$this->ige_encrypt($plaintext.$padding, $aes_key, $aes_iv); + $this->datacenter->sockets[$datacenter]->outgoing_messages[$message_id] = ['seq_no' => $seq_no, 'response' => -1]; + $this->datacenter->sockets[$datacenter]->send_message($message); + $this->datacenter->sockets[$datacenter]->object_queue = []; + + /*if ($has_ack) { + foreach ($this->datacenter->sockets[$datacenter]->ack_queue as $msg_id) { + $this->datacenter->sockets[$datacenter]->incoming_messages[$msg_id]['ack'] = true; + } + $this->datacenter->sockets[$datacenter]->ack_queue = []; + }*/ + } + + /** + * Reading connection and receiving message from server. + */ + public function recv_message($datacenter) + { + if ($this->datacenter->sockets[$datacenter]->must_open) { + \danog\MadelineProto\Logger::log('Trying to read from closed socket, sending initial ping'); + if ($this->is_http($datacenter)) { + $this->method_call('http_wait', ['max_wait' => 500, 'wait_after' => 150, 'max_delay' => 500], ['datacenter' => $datacenter]); + } elseif (isset($this->datacenter->sockets[$datacenter]->temp_auth_key['connection_inited']) && $this->datacenter->sockets[$datacenter]->temp_auth_key['connection_inited']) { + $this->method_call('ping', ['ping_id' => 0], ['datacenter' => $datacenter]); + } else { + throw new \danog\MadelineProto\Exception('Resend query'); + } + } + $payload = $this->datacenter->sockets[$datacenter]->read_message(); + if (strlen($payload) === 4) { + $payload = $this->unpack_signed_int($payload); + \danog\MadelineProto\Logger::log("Received $payload from DC $datacenter", \danog\MadelineProto\Logger::ULTRA_VERBOSE); + + return $payload; + } + $auth_key_id = substr($payload, 0, 8); + if ($auth_key_id === "\0\0\0\0\0\0\0\0") { + $message_id = substr($payload, 8, 8); + $this->check_message_id($message_id, ['outgoing' => false, 'datacenter' => $datacenter, 'container' => false]); + $message_length = unpack('V', substr($payload, 16, 4))[1]; + $message_data = substr($payload, 20, $message_length); + $this->datacenter->sockets[$datacenter]->incoming_messages[$message_id] = []; + } elseif ($auth_key_id === $this->datacenter->sockets[$datacenter]->temp_auth_key['id']) { + $message_key = substr($payload, 8, 16); + $encrypted_data = substr($payload, 24); + list($aes_key, $aes_iv) = $this->aes_calculate($message_key, $this->datacenter->sockets[$datacenter]->temp_auth_key['auth_key'], false); + $decrypted_data = $this->ige_decrypt($encrypted_data, $aes_key, $aes_iv); + /* + $server_salt = substr($decrypted_data, 0, 8); + if ($server_salt != $this->datacenter->sockets[$datacenter]->temp_auth_key['server_salt']) { + \danog\MadelineProto\Logger::log('WARNING: Server salt mismatch (my server salt '.$this->datacenter->sockets[$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 != $this->datacenter->sockets[$datacenter]->session_id) { + throw new \danog\MadelineProto\Exception('Session id mismatch.'); + } + $message_id = substr($decrypted_data, 16, 8); + $this->check_message_id($message_id, ['outgoing' => false, 'datacenter' => $datacenter, 'container' => false]); + $seq_no = unpack('V', substr($decrypted_data, 24, 4))[1]; + // Dunno how to handle any incorrect sequence numbers + $message_data_length = unpack('V', substr($decrypted_data, 28, 4))[1]; + if ($message_data_length > strlen($decrypted_data)) { + throw new \danog\MadelineProto\SecurityException('message_data_length is too big'); + } + if (strlen($decrypted_data) - 32 - $message_data_length < 12) { + throw new \danog\MadelineProto\SecurityException('padding is too small'); + } + if (strlen($decrypted_data) - 32 - $message_data_length > 1024) { + throw new \danog\MadelineProto\SecurityException('padding is too big'); + } + if ($message_data_length < 0) { + throw new \danog\MadelineProto\SecurityException('message_data_length not positive'); + } + if ($message_data_length % 4 != 0) { + throw new \danog\MadelineProto\SecurityException('message_data_length not divisible by 4'); + } + $message_data = substr($decrypted_data, 32, $message_data_length); + if ($message_key != substr(hash('sha256', substr($this->datacenter->sockets[$datacenter]->temp_auth_key['auth_key'], 96, 32).$decrypted_data, true), 8, 16)) { + throw new \danog\MadelineProto\SecurityException('msg_key mismatch'); + } + $this->datacenter->sockets[$datacenter]->incoming_messages[$message_id] = ['seq_no' => $seq_no]; + } else { + $this->close_and_reopen($datacenter); + throw new \danog\MadelineProto\Exception('Got unknown auth_key id'); + } + $deserialized = $this->deserialize($message_data, ['type' => '', 'datacenter' => $datacenter]); + $this->datacenter->sockets[$datacenter]->incoming_messages[$message_id]['content'] = $deserialized; + $this->datacenter->sockets[$datacenter]->incoming_messages[$message_id]['response'] = -1; + $this->datacenter->sockets[$datacenter]->new_incoming[$message_id] = $message_id; + $this->datacenter->sockets[$datacenter]->last_recv = time(); + + return true; + } +}