diff --git a/phpseclib/Net/SSH2.php b/phpseclib/Net/SSH2.php index 9af20c072..9747efc66 100644 --- a/phpseclib/Net/SSH2.php +++ b/phpseclib/Net/SSH2.php @@ -151,6 +151,10 @@ class SSH2 * Outputs the message numbers real-time */ const LOG_SIMPLE_REALTIME = 5; + /* + * Dumps the message numbers real-time + */ + const LOG_REALTIME_SIMPLE = 5; /** * Make sure that the log never gets larger than this * @@ -1125,6 +1129,44 @@ class SSH2 */ private $extra_packets; + /** + * Bytes Transferred Since Last Key Exchange + * + * Includes outbound and inbound totals + * + * @var int + */ + private $bytesTransferredSinceLastKEX = 0; + + /** + * After how many transferred byte should phpseclib initiate a key re-exchange? + * + * @var int + */ + private $doKeyReexchangeAfterXBytes = 256 * 1024; + + /** + * Has a key re-exchange been initialized? + * + * @var bool + * @access private + */ + private $keyExchangeInProgress = false; + + /** + * KEX Buffer + * + * If we're in the middle of a key exchange we want to buffer any additional packets we get until + * the key exchange is over + * + * @see self::_get_binary_packet() + * @see self::_key_exchange() + * @see self::exec() + * @var array + * @access private + */ + private $kex_buffer = []; + /** * Default Constructor. * @@ -1535,8 +1577,13 @@ private function generate_identifier() */ private function key_exchange($kexinit_payload_server = false) { + $this->bytesTransferredSinceLastKEX = 0; + $preferred = $this->preferred; - $send_kex = true; + // for the initial key exchange $send_kex is true (no key re-exchange has been started) + // for phpseclib initiated key exchanges $send_kex is false + $send_kex = !$this->keyExchangeInProgress; + $this->keyExchangeInProgress = true; $kex_algorithms = isset($preferred['kex']) ? $preferred['kex'] : @@ -1616,11 +1663,21 @@ private function key_exchange($kexinit_payload_server = false) 0 // reserved for future extension ); - if ($kexinit_payload_server === false) { + if ($kexinit_payload_server === false && $send_kex) { $this->send_binary_packet($kexinit_payload_client); - $this->extra_packets = 0; - $kexinit_payload_server = $this->get_binary_packet_or_close(NET_SSH2_MSG_KEXINIT); + while (true) { + $kexinit_payload_server = $this->get_binary_packet(); + switch (ord($kexinit_payload_server[0])) { + case NET_SSH2_MSG_KEXINIT: + break 2; + case NET_SSH2_MSG_DISCONNECT: + return $this->handleDisconnect($kexinit_payload_server); + } + + $this->kex_buffer[] = $kexinit_payload_server; + } + $send_kex = false; } @@ -1642,7 +1699,7 @@ private function key_exchange($kexinit_payload_server = false) $first_kex_packet_follows ) = Strings::unpackSSH2('L10C', $response); if (in_array('kex-strict-s-v00@openssh.com', $this->kex_algorithms)) { - if ($this->session_id === false && $this->extra_packets) { + if ($this->session_id === false && count($this->kex_buffer)) { throw new \UnexpectedValueException('Possible Terrapin Attack detected'); } } @@ -1881,6 +1938,8 @@ private function key_exchange($kexinit_payload_server = false) $this->send_binary_packet($packet); $response = $this->get_binary_packet_or_close(NET_SSH2_MSG_NEWKEYS); + $this->keyExchangeInProgress = false; + if (in_array('kex-strict-s-v00@openssh.com', $this->kex_algorithms)) { $this->get_seq_no = $this->send_seq_no = 0; } @@ -3529,6 +3588,9 @@ private function get_binary_packet() if (!is_resource($this->fsock)) { throw new \InvalidArgumentException('fsock is not a resource.'); } + if (!$this->keyExchangeInProgress && count($this->kex_buffer)) { + return $this->filter(array_shift($this->kex_buffer)); + } if ($this->binary_packet_buffer == null) { // buffer the packet to permit continued reads across timeouts $this->binary_packet_buffer = (object) [ @@ -3655,6 +3717,11 @@ private function get_binary_packet() if ($padding_length > 0) { Strings::pop($payload, $padding_length); } + + if (!$this->keyExchangeInProgress) { + $this->bytesTransferredSinceLastKEX += $packet->packet_length + $padding_length + 5; + } + if (empty($payload)) { $this->disconnect_helper(NET_SSH2_DISCONNECT_PROTOCOL_ERROR); throw new ConnectionClosedException('Plaintext is too short'); @@ -3708,7 +3775,12 @@ private function get_binary_packet() } $this->last_packet = microtime(true); - return $this->filter($payload); + if ($this->bytesTransferredSinceLastKEX > $this->doKeyReexchangeAfterXBytes) { + $this->key_exchange(); + } + + // don't filter if we're in the middle of a key exchange (since _filter might send out packets) + return $this->keyExchangeInProgress ? $payload : $this->filter($payload); } /** @@ -3773,6 +3845,25 @@ private function get_binary_packet_size(&$packet) $packet->packet_length = $packet_length; } + /** + * Handle Disconnect + * + * Because some binary packets need to be ignored... + * + * @see self::filter() + * @see self::key_exchange() + * @return boolean + * @access private + */ + private function handleDisconnect($payload) + { + Strings::shift($payload, 1); + list($reason_code, $message) = Strings::unpackSSH2('Ns', $payload); + $this->errors[] = 'SSH_MSG_DISCONNECT: ' . self::$disconnect_reasons[$reason_code] . "\r\n$message"; + $this->disconnect_helper(NET_SSH2_DISCONNECT_CONNECTION_LOST); + throw new ConnectionClosedException('Connection closed by server'); + } + /** * Filter Binary Packets * @@ -3786,11 +3877,7 @@ private function filter($payload) { switch (ord($payload[0])) { case NET_SSH2_MSG_DISCONNECT: - Strings::shift($payload, 1); - list($reason_code, $message) = Strings::unpackSSH2('Ns', $payload); - $this->errors[] = 'SSH_MSG_DISCONNECT: ' . self::$disconnect_reasons[$reason_code] . "\r\n$message"; - $this->disconnect_helper(NET_SSH2_DISCONNECT_CONNECTION_LOST); - throw new ConnectionClosedException('Connection closed by server'); + return $this->handleDisconnect($payload); case NET_SSH2_MSG_IGNORE: $this->extra_packets++; $payload = $this->get_binary_packet(); @@ -3805,7 +3892,7 @@ private function filter($payload) case NET_SSH2_MSG_UNIMPLEMENTED: break; // return payload case NET_SSH2_MSG_KEXINIT: - // this is here for key re-exchanges after the initial key exchange + // this is here for server initiated key re-exchanges after the initial key exchange if ($this->session_id !== false) { if (!$this->key_exchange($payload)) { $this->disconnect_helper(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED); @@ -4339,6 +4426,10 @@ protected function send_binary_packet($data, $logged = null) $packet .= $this->encrypt && $this->encrypt->usesNonce() ? $this->encrypt->getTag() : $hmac; + if (!$this->keyExchangeInProgress) { + $this->bytesTransferredSinceLastKEX += strlen($packet); + } + $start = microtime(true); $sent = @fputs($this->fsock, $packet); $stop = microtime(true); @@ -4359,6 +4450,10 @@ protected function send_binary_packet($data, $logged = null) "Only $sent of " . strlen($packet) . " bytes were sent"; throw new \RuntimeException($message); } + + if ($this->bytesTransferredSinceLastKEX > $this->doKeyReexchangeAfterXBytes) { + $this->key_exchange(); + } } /** @@ -4481,6 +4576,10 @@ protected function append_log_helper($constant, $message_number, $message, array $realtime_log_wrap = true; } fputs($realtime_log_file, $entry); + break; + case self::LOG_REALTIME_SIMPLE: + echo $message_number; + echo PHP_SAPI == 'cli' ? "\r\n" : '
'; } }