Merge branch '3.0'

This commit is contained in:
terrafrost 2024-07-24 23:17:25 -05:00
commit 0d8ec30267
4 changed files with 241 additions and 113 deletions

View File

@ -0,0 +1,10 @@
<?php
namespace phpseclib3\Exception;
/**
* Indicates a timeout awaiting server response
*/
class TimeoutException extends \RuntimeException
{
}

View File

@ -2891,6 +2891,7 @@ class SFTP extends SSH2
// in SSH2.php the timeout is cumulative per function call. eg. exec() will // in SSH2.php the timeout is cumulative per function call. eg. exec() will
// timeout after 10s. but for SFTP.php it's cumulative per packet // timeout after 10s. but for SFTP.php it's cumulative per packet
$this->curTimeout = $this->timeout; $this->curTimeout = $this->timeout;
$this->is_timeout = false;
$packet = $this->use_request_id ? $packet = $this->use_request_id ?
pack('NCNa*', strlen($data) + 5, $type, $request_id, $data) : pack('NCNa*', strlen($data) + 5, $type, $request_id, $data) :
@ -2953,6 +2954,7 @@ class SFTP extends SSH2
// in SSH2.php the timeout is cumulative per function call. eg. exec() will // in SSH2.php the timeout is cumulative per function call. eg. exec() will
// timeout after 10s. but for SFTP.php it's cumulative per packet // timeout after 10s. but for SFTP.php it's cumulative per packet
$this->curTimeout = $this->timeout; $this->curTimeout = $this->timeout;
$this->is_timeout = false;
$start = microtime(true); $start = microtime(true);

View File

@ -71,6 +71,7 @@ use phpseclib3\Exception\LengthException;
use phpseclib3\Exception\LogicException; use phpseclib3\Exception\LogicException;
use phpseclib3\Exception\NoSupportedAlgorithmsException; use phpseclib3\Exception\NoSupportedAlgorithmsException;
use phpseclib3\Exception\RuntimeException; use phpseclib3\Exception\RuntimeException;
use phpseclib3\Exception\TimeoutException;
use phpseclib3\Exception\UnableToConnectException; use phpseclib3\Exception\UnableToConnectException;
use phpseclib3\Exception\UnexpectedValueException; use phpseclib3\Exception\UnexpectedValueException;
use phpseclib3\Exception\UnsupportedAlgorithmException; use phpseclib3\Exception\UnsupportedAlgorithmException;
@ -114,7 +115,6 @@ class SSH2
public const MASK_LOGIN_REQ = 0x00000004; public const MASK_LOGIN_REQ = 0x00000004;
public const MASK_LOGIN = 0x00000008; public const MASK_LOGIN = 0x00000008;
public const MASK_SHELL = 0x00000010; public const MASK_SHELL = 0x00000010;
public const MASK_WINDOW_ADJUST = 0x00000020;
/* /*
* Channel constants * Channel constants
@ -530,7 +530,7 @@ class SSH2
protected array $server_channels = []; protected array $server_channels = [];
/** /**
* Channel Buffers * Channel Read Buffers
* *
* If a client requests a packet from one channel but receives two packets from another those packets should * If a client requests a packet from one channel but receives two packets from another those packets should
* be placed in a buffer * be placed in a buffer
@ -540,6 +540,17 @@ class SSH2
*/ */
private array $channel_buffers = []; private array $channel_buffers = [];
/**
* Channel Write Buffers
*
* If a client sends a packet and receives a timeout error mid-transmission, buffer the data written so it
* can be de-duplicated upon resuming write
*
* @see self::send_channel_packet()
* @var array
*/
private $channel_buffers_write = [];
/** /**
* Channel Status * Channel Status
* *
@ -1196,13 +1207,7 @@ class SSH2
} }
if (!$this->send_kex_first) { if (!$this->send_kex_first) {
$response = $this->get_binary_packet(); $response = $this->get_binary_packet_or_close(MessageType::KEXINIT);
if (is_bool($response) || !strlen($response) || ord($response[0]) != MessageType::KEXINIT) {
$this->bitmap = 0;
throw new UnexpectedValueException('Expected SSH_MSG_KEXINIT');
}
$this->key_exchange($response); $this->key_exchange($response);
} }
@ -1333,17 +1338,7 @@ class SSH2
$this->send_binary_packet($kexinit_payload_client); $this->send_binary_packet($kexinit_payload_client);
$this->extra_packets = 0; $this->extra_packets = 0;
$kexinit_payload_server = $this->get_binary_packet(); $kexinit_payload_server = $this->get_binary_packet_or_close(MessageType::KEXINIT);
if (
is_bool($kexinit_payload_server)
|| !strlen($kexinit_payload_server)
|| ord($kexinit_payload_server[0]) != MessageType::KEXINIT
) {
$this->disconnect_helper(DisconnectReason::PROTOCOL_ERROR);
throw new UnexpectedValueException('Expected SSH_MSG_KEXINIT');
}
$send_kex = false; $send_kex = false;
} }
@ -1487,14 +1482,10 @@ class SSH2
$this->send_binary_packet($packet); $this->send_binary_packet($packet);
$this->updateLogHistory('UNKNOWN (34)', 'SSH_MSG_KEXDH_GEX_REQUEST'); $this->updateLogHistory('UNKNOWN (34)', 'SSH_MSG_KEXDH_GEX_REQUEST');
$response = $this->get_binary_packet(); $response = $this->get_binary_packet_or_close(MessageTypeExtra::KEXDH_GEX_GROUP);
list($type, $primeBytes, $gBytes) = Strings::unpackSSH2('Css', $response);
[$type, $primeBytes, $gBytes] = Strings::unpackSSH2('Css', $response);
if ($type != MessageTypeExtra::KEXDH_GEX_GROUP) {
$this->disconnect_helper(DisconnectReason::PROTOCOL_ERROR);
throw new UnexpectedValueException('Expected SSH_MSG_KEX_DH_GEX_GROUP');
}
$this->updateLogHistory('UNKNOWN (31)', 'SSH_MSG_KEXDH_GEX_GROUP'); $this->updateLogHistory('UNKNOWN (31)', 'SSH_MSG_KEXDH_GEX_GROUP');
$prime = new BigInteger($primeBytes, -256); $prime = new BigInteger($primeBytes, -256);
$g = new BigInteger($gBytes, -256); $g = new BigInteger($gBytes, -256);
@ -1532,7 +1523,7 @@ class SSH2
$this->updateLogHistory('UNKNOWN (32)', 'SSH_MSG_KEXDH_GEX_INIT'); $this->updateLogHistory('UNKNOWN (32)', 'SSH_MSG_KEXDH_GEX_INIT');
} }
$response = $this->get_binary_packet(); $response = $this->get_binary_packet_or_close(constant($serverKexReplyMessage));
[ [
$type, $type,
@ -1541,10 +1532,6 @@ class SSH2
$this->signature $this->signature
] = Strings::unpackSSH2('Csss', $response); ] = Strings::unpackSSH2('Csss', $response);
if ($type != $serverKexReplyMessage) {
$this->disconnect_helper(DisconnectReason::PROTOCOL_ERROR);
throw new UnexpectedValueException("Expected $serverKexReplyMessage");
}
switch ($serverKexReplyMessage) { switch ($serverKexReplyMessage) {
case MessageTypeExtra::KEX_ECDH_REPLY: case MessageTypeExtra::KEX_ECDH_REPLY:
$this->updateLogHistory('SSH_MSG_KEXDH_REPLY', 'SSH_MSG_KEX_ECDH_REPLY'); $this->updateLogHistory('SSH_MSG_KEXDH_REPLY', 'SSH_MSG_KEX_ECDH_REPLY');
@ -1612,18 +1599,7 @@ class SSH2
$packet = pack('C', MessageType::NEWKEYS); $packet = pack('C', MessageType::NEWKEYS);
$this->send_binary_packet($packet); $this->send_binary_packet($packet);
$response = $this->get_binary_packet(); $response = $this->get_binary_packet_or_close(MessageType::NEWKEYS);
if ($response === false) {
$this->disconnect_helper(DisconnectReason::CONNECTION_LOST);
throw new ConnectionClosedException('Connection closed by server');
}
[$type] = Strings::unpackSSH2('C', $response);
if ($type != MessageType::NEWKEYS) {
$this->disconnect_helper(DisconnectReason::PROTOCOL_ERROR);
throw new UnexpectedValueException('Expected SSH_MSG_NEWKEYS');
}
if (in_array('kex-strict-s-v00@openssh.com', $this->kex_algorithms)) { if (in_array('kex-strict-s-v00@openssh.com', $this->kex_algorithms)) {
$this->get_seq_no = $this->send_seq_no = 0; $this->get_seq_no = $this->send_seq_no = 0;
@ -2070,7 +2046,7 @@ class SSH2
$this->send_binary_packet($packet); $this->send_binary_packet($packet);
try { try {
$response = $this->get_binary_packet(); $response = $this->get_binary_packet_or_close(MessageType::SERVICE_ACCEPT);
} catch (InvalidPacketLengthException $e) { } catch (InvalidPacketLengthException $e) {
// the first opportunity to encounter the "bad key size" error // the first opportunity to encounter the "bad key size" error
if (!$this->bad_key_size_fix && $this->decryptName != null && self::bad_algorithm_candidate($this->decryptName)) { if (!$this->bad_key_size_fix && $this->decryptName != null && self::bad_algorithm_candidate($this->decryptName)) {
@ -2081,16 +2057,14 @@ class SSH2
return $this->reconnect(); return $this->reconnect();
} }
throw $e; throw $e;
} catch (\Exception $e) {
$this->disconnect_helper(DisconnectReason::CONNECTION_LOST);
throw $e;
} }
[$type] = Strings::unpackSSH2('C', $response); [$type] = Strings::unpackSSH2('C', $response);
[$service] = Strings::unpackSSH2('s', $response); [$service] = Strings::unpackSSH2('s', $response);
if ($type != MessageType::SERVICE_ACCEPT || $service != 'ssh-userauth') {
if ($service != 'ssh-userauth') {
$this->disconnect_helper(DisconnectReason::PROTOCOL_ERROR); $this->disconnect_helper(DisconnectReason::PROTOCOL_ERROR);
throw new UnexpectedValueException('Expected SSH_MSG_SERVICE_ACCEPT'); throw new \UnexpectedValueException('Expected SSH_MSG_SERVICE_ACCEPT');
} }
$this->bitmap |= self::MASK_LOGIN_REQ; $this->bitmap |= self::MASK_LOGIN_REQ;
} }
@ -2126,7 +2100,7 @@ class SSH2
$this->send_binary_packet($packet); $this->send_binary_packet($packet);
$response = $this->get_binary_packet(); $response = $this->get_binary_packet_or_close();
[$type] = Strings::unpackSSH2('C', $response); [$type] = Strings::unpackSSH2('C', $response);
switch ($type) { switch ($type) {
@ -2169,11 +2143,9 @@ class SSH2
$this->send_binary_packet($packet, $logged); $this->send_binary_packet($packet, $logged);
$response = $this->get_binary_packet(); $response = $this->get_binary_packet_or_close();
if ($response === false) {
return false;
}
[$type] = Strings::unpackSSH2('C', $response); [$type] = Strings::unpackSSH2('C', $response);
switch ($type) { switch ($type) {
case MessageTypeExtra::USERAUTH_PASSWD_CHANGEREQ: // in theory, the password can be changed case MessageTypeExtra::USERAUTH_PASSWD_CHANGEREQ: // in theory, the password can be changed
$this->updateLogHistory('SSH_MSG_USERAUTH_INFO_REQUEST', 'SSH_MSG_USERAUTH_PASSWD_CHANGEREQ'); $this->updateLogHistory('SSH_MSG_USERAUTH_INFO_REQUEST', 'SSH_MSG_USERAUTH_PASSWD_CHANGEREQ');
@ -2236,7 +2208,7 @@ class SSH2
if (strlen($this->last_interactive_response)) { if (strlen($this->last_interactive_response)) {
$response = $this->last_interactive_response; $response = $this->last_interactive_response;
} else { } else {
$orig = $response = $this->get_binary_packet(); $orig = $response = $this->get_binary_packet_or_close();
} }
[$type] = Strings::unpackSSH2('C', $response); [$type] = Strings::unpackSSH2('C', $response);
@ -2419,7 +2391,11 @@ class SSH2
$packet = $part1 . chr(0) . $part2; $packet = $part1 . chr(0) . $part2;
$this->send_binary_packet($packet); $this->send_binary_packet($packet);
$response = $this->get_binary_packet(); $response = $this->get_binary_packet_or_close(
MessageType::USERAUTH_SUCCESS,
MessageType::USERAUTH_FAILURE,
MessageType::USERAUTH_PK_OK
);
[$type] = Strings::unpackSSH2('C', $response); [$type] = Strings::unpackSSH2('C', $response);
switch ($type) { switch ($type) {
@ -2440,9 +2416,6 @@ class SSH2
case MessageType::USERAUTH_SUCCESS: case MessageType::USERAUTH_SUCCESS:
$this->bitmap |= self::MASK_LOGIN; $this->bitmap |= self::MASK_LOGIN;
return true; return true;
default:
$this->disconnect_helper(DisconnectReason::BY_APPLICATION);
throw new ConnectionClosedException('Unexpected response to publickey authentication pt 1');
} }
$packet = $part1 . chr(1) . $part2; $packet = $part1 . chr(1) . $part2;
@ -2455,7 +2428,10 @@ class SSH2
$this->send_binary_packet($packet); $this->send_binary_packet($packet);
$response = $this->get_binary_packet(); $response = $this->get_binary_packet_or_close(
MessageType::USERAUTH_SUCCESS,
MessageType::USERAUTH_FAILURE
);
[$type] = Strings::unpackSSH2('C', $response); [$type] = Strings::unpackSSH2('C', $response);
switch ($type) { switch ($type) {
@ -2468,9 +2444,6 @@ class SSH2
$this->bitmap |= self::MASK_LOGIN; $this->bitmap |= self::MASK_LOGIN;
return true; return true;
} }
$this->disconnect_helper(DisconnectReason::BY_APPLICATION);
throw new ConnectionClosedException('Unexpected response to publickey authentication pt 2');
} }
/** /**
@ -2884,6 +2857,7 @@ class SSH2
* @param int|null $channel Channel id returned by self::getInteractiveChannelId() * @param int|null $channel Channel id returned by self::getInteractiveChannelId()
* @throws RuntimeException on connection error * @throws RuntimeException on connection error
* @throws InsufficientSetupException on unexpected channel status, possibly due to closure * @throws InsufficientSetupException on unexpected channel status, possibly due to closure
* @throws TimeoutException if the write could not be completed within the requested self::setTimeout()
* @see SSH2::read() * @see SSH2::read()
*/ */
public function write(string $cmd, ?int $channel = null): void public function write(string $cmd, ?int $channel = null): void
@ -2904,6 +2878,8 @@ class SSH2
} }
} }
$this->curTimeout = $this->timeout;
$this->is_timeout = false;
$this->send_channel_packet($channel, $cmd); $this->send_channel_packet($channel, $cmd);
} }
@ -3151,6 +3127,8 @@ class SSH2
$this->get_seq_no = $this->send_seq_no = 0; $this->get_seq_no = $this->send_seq_no = 0;
$this->channel_status = []; $this->channel_status = [];
$this->channel_id_last_interactive = 0; $this->channel_id_last_interactive = 0;
$this->channel_buffers = [];
$this->channel_buffers_write = [];
} }
/** /**
@ -3175,15 +3153,39 @@ class SSH2
return [$sec, $usec]; return [$sec, $usec];
} }
/**
* Retrieves the next packet with added timeout and type handling
*
* @param string $message_types Message types to enforce in response, closing if not met
* @return string
* @throws ConnectionClosedException If an error has occurred preventing read of the next packet
*/
private function get_binary_packet_or_close(...$message_types)
{
try {
$packet = $this->get_binary_packet();
if (count($message_types) > 0 && !in_array(ord($packet[0]), $message_types)) {
$this->disconnect_helper(DisconnectReason::PROTOCOL_ERROR);
throw new ConnectionClosedException('Bad message type. Expected: #'
. implode(', #', $message_types) . '. Got: #' . ord($packet[0]));
}
return $packet;
} catch (TimeoutException $e) {
$this->disconnect_helper(DisconnectReason::BY_APPLICATION);
throw new ConnectionClosedException('Connection closed due to timeout');
}
}
/** /**
* Gets Binary Packets * Gets Binary Packets
* *
* See '6. Binary Packet Protocol' of rfc4253 for more info. * See '6. Binary Packet Protocol' of rfc4253 for more info.
* *
* @return bool|string
* @see self::_send_binary_packet() * @see self::_send_binary_packet()
* @throws TimeoutException If user requested timeout was reached while waiting for next packet
* @throws ConnectionClosedException If an error has occurred preventing read of the next packet
*/ */
private function get_binary_packet() private function get_binary_packet(): string
{ {
if (!is_resource($this->fsock)) { if (!is_resource($this->fsock)) {
throw new InvalidArgumentException('fsock is not a resource.'); throw new InvalidArgumentException('fsock is not a resource.');
@ -3207,7 +3209,7 @@ class SSH2
} }
if ($this->curTimeout < 0) { if ($this->curTimeout < 0) {
$this->is_timeout = true; $this->is_timeout = true;
return true; throw new TimeoutException('Timed out waiting for server');
} }
$this->send_keep_alive(); $this->send_keep_alive();
@ -3442,18 +3444,17 @@ class SSH2
* *
* Because some binary packets need to be ignored... * Because some binary packets need to be ignored...
* *
* @return string|bool
* @see self::_get_binary_packet() * @see self::_get_binary_packet()
*/ */
private function filter(string $payload) private function filter(string $payload): string
{ {
switch (ord($payload[0])) { switch (ord($payload[0])) {
case MessageType::DISCONNECT: case MessageType::DISCONNECT:
Strings::shift($payload, 1); Strings::shift($payload, 1);
[$reason_code, $message] = Strings::unpackSSH2('Ns', $payload); [$reason_code, $message] = Strings::unpackSSH2('Ns', $payload);
$this->errors[] = 'SSH_MSG_DISCONNECT: SSH_DISCONNECT_' . DisconnectReason::getConstantNameByValue($reason_code) . "\r\n$message"; $this->errors[] = 'SSH_MSG_DISCONNECT: SSH_DISCONNECT_' . DisconnectReason::getConstantNameByValue($reason_code) . "\r\n$message";
$this->bitmap = 0; $this->disconnect_helper(DisconnectReason::CONNECTION_LOST);
return false; throw new ConnectionClosedException('Connection closed by server');
case MessageType::IGNORE: case MessageType::IGNORE:
$this->extra_packets++; $this->extra_packets++;
$payload = $this->get_binary_packet(); $payload = $this->get_binary_packet();
@ -3466,13 +3467,13 @@ class SSH2
$payload = $this->get_binary_packet(); $payload = $this->get_binary_packet();
break; break;
case MessageType::UNIMPLEMENTED: case MessageType::UNIMPLEMENTED:
return false; break;
case MessageType::KEXINIT: case MessageType::KEXINIT:
// this is here for key re-exchanges after the initial key exchange // this is here for key re-exchanges after the initial key exchange
if ($this->session_id !== false) { if ($this->session_id !== false) {
if (!$this->key_exchange($payload)) { if (!$this->key_exchange($payload)) {
$this->bitmap = 0; $this->disconnect_helper(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED);
return false; throw new ConnectionClosedException('Key exchange failed');
} }
$payload = $this->get_binary_packet(); $payload = $this->get_binary_packet();
} }
@ -3490,7 +3491,7 @@ class SSH2
} }
// see http://tools.ietf.org/html/rfc4252#section-5.4; only called when the encryption has been activated and when we haven't already logged in // see http://tools.ietf.org/html/rfc4252#section-5.4; only called when the encryption has been activated and when we haven't already logged in
if (($this->bitmap & self::MASK_CONNECTED) && !$this->isAuthenticated() && !is_bool($payload) && ord($payload[0]) == MessageType::USERAUTH_BANNER) { if (($this->bitmap & self::MASK_CONNECTED) && !$this->isAuthenticated() && ord($payload[0]) == MessageType::USERAUTH_BANNER) {
Strings::shift($payload, 1); Strings::shift($payload, 1);
[$this->banner_message] = Strings::unpackSSH2('s', $payload); [$this->banner_message] = Strings::unpackSSH2('s', $payload);
$payload = $this->get_binary_packet(); $payload = $this->get_binary_packet();
@ -3498,10 +3499,6 @@ class SSH2
// only called when we've already logged in // only called when we've already logged in
if (($this->bitmap & self::MASK_CONNECTED) && $this->isAuthenticated()) { if (($this->bitmap & self::MASK_CONNECTED) && $this->isAuthenticated()) {
if (is_bool($payload)) {
return $payload;
}
switch (ord($payload[0])) { switch (ord($payload[0])) {
case MessageType::CHANNEL_REQUEST: case MessageType::CHANNEL_REQUEST:
if (strlen($payload) == 31) { if (strlen($payload) == 31) {
@ -3518,13 +3515,7 @@ class SSH2
Strings::shift($payload, 1); Strings::shift($payload, 1);
[$request_name] = Strings::unpackSSH2('s', $payload); [$request_name] = Strings::unpackSSH2('s', $payload);
$this->errors[] = "SSH_MSG_GLOBAL_REQUEST: $request_name"; $this->errors[] = "SSH_MSG_GLOBAL_REQUEST: $request_name";
try {
$this->send_binary_packet(pack('C', MessageType::REQUEST_FAILURE)); $this->send_binary_packet(pack('C', MessageType::REQUEST_FAILURE));
} catch (\RuntimeException $e) {
return $this->disconnect_helper(DisconnectReason::BY_APPLICATION);
}
$payload = $this->get_binary_packet(); $payload = $this->get_binary_packet();
break; break;
case MessageType::CHANNEL_OPEN: // see http://tools.ietf.org/html/rfc4254#section-5.1 case MessageType::CHANNEL_OPEN: // see http://tools.ietf.org/html/rfc4254#section-5.1
@ -3571,22 +3562,10 @@ class SSH2
'' // language tag '' // language tag
); );
try {
$this->send_binary_packet($packet); $this->send_binary_packet($packet);
} catch (\RuntimeException $e) {
return $this->disconnect_helper(DisconnectReason::BY_APPLICATION);
}
} }
$payload = $this->get_binary_packet(); $payload = $this->get_binary_packet();
break;
case MessageType::CHANNEL_WINDOW_ADJUST:
Strings::shift($payload, 1);
[$channel, $window_size] = Strings::unpackSSH2('NN', $payload);
$this->window_size_client_to_server[$channel] += $window_size;
$payload = ($this->bitmap & self::MASK_WINDOW_ADJUST) ? true : $this->get_binary_packet();
} }
} }
@ -3661,6 +3640,7 @@ class SSH2
* *
* - the server closes the channel * - the server closes the channel
* - if the connection times out * - if the connection times out
* - if a window adjust packet is received on the given negated client channel
* - if the channel status is CHANNEL_OPEN and the response was CHANNEL_OPEN_CONFIRMATION * - if the channel status is CHANNEL_OPEN and the response was CHANNEL_OPEN_CONFIRMATION
* - if the channel status is CHANNEL_REQUEST and the response was CHANNEL_SUCCESS * - if the channel status is CHANNEL_REQUEST and the response was CHANNEL_SUCCESS
* - if the channel status is CHANNEL_CLOSE and the response was CHANNEL_CLOSE * - if the channel status is CHANNEL_CLOSE and the response was CHANNEL_CLOSE
@ -3669,6 +3649,12 @@ class SSH2
* *
* - if the channel status is CHANNEL_REQUEST and the response was CHANNEL_FAILURE * - if the channel status is CHANNEL_REQUEST and the response was CHANNEL_FAILURE
* *
* @param int $client_channel Specifies the channel to return data for, and data received
* on other channels is buffered. The respective negative value of a channel is
* also supported for the case that the caller is awaiting adjustment of the data
* window, and where data received on that respective channel is also buffered.
* @param bool $skip_extended
* @return mixed
* @throws RuntimeException on connection error * @throws RuntimeException on connection error
*/ */
protected function get_channel_packet(int $client_channel, bool $skip_extended = false) protected function get_channel_packet(int $client_channel, bool $skip_extended = false)
@ -3691,21 +3677,18 @@ class SSH2
} }
while (true) { while (true) {
try {
$response = $this->get_binary_packet(); $response = $this->get_binary_packet();
if ($response === true && $this->is_timeout) { } catch (TimeoutException $e) {
if ($client_channel == self::CHANNEL_EXEC && !$this->request_pty) { if ($client_channel == self::CHANNEL_EXEC && !$this->request_pty) {
$this->close_channel($client_channel); $this->close_channel($client_channel);
} }
return true; return true;
} }
if ($response === false) { [$type] = Strings::unpackSSH2('C', $response);
$this->disconnect_helper(DisconnectReason::CONNECTION_LOST); if (strlen($response) >= 4) {
throw new ConnectionClosedException('Connection closed by server'); [$channel] = Strings::unpackSSH2('N', $response);
} }
if ($client_channel == -1 && $response === true) {
return true;
}
[$type, $channel] = Strings::unpackSSH2('CN', $response);
// will not be setup yet on incoming channel open request // will not be setup yet on incoming channel open request
if (isset($channel) && isset($this->channel_status[$channel]) && isset($this->window_size_server_to_client[$channel])) { if (isset($channel) && isset($this->channel_status[$channel]) && isset($this->window_size_server_to_client[$channel])) {
@ -3721,6 +3704,14 @@ class SSH2
} }
switch ($type) { switch ($type) {
case MessageType::CHANNEL_WINDOW_ADJUST:
[$window_size] = Strings::unpackSSH2('N', $response);
$this->window_size_client_to_server[$channel] += $window_size;
if ($channel == -$client_channel) {
return true;
}
continue 2;
case MessageType::CHANNEL_EXTENDED_DATA: case MessageType::CHANNEL_EXTENDED_DATA:
/* /*
if ($client_channel == self::CHANNEL_EXEC) { if ($client_channel == self::CHANNEL_EXEC) {
@ -3892,7 +3883,7 @@ class SSH2
protected function send_binary_packet(string $data, ?string $logged = null): void protected function send_binary_packet(string $data, ?string $logged = null): void
{ {
if (!is_resource($this->fsock) || feof($this->fsock)) { if (!is_resource($this->fsock) || feof($this->fsock)) {
$this->bitmap = 0; $this->disconnect_helper(NET_SSH2_DISCONNECT_CONNECTION_LOST);
throw new ConnectionClosedException('Connection closed prematurely'); throw new ConnectionClosedException('Connection closed prematurely');
} }
@ -4025,7 +4016,7 @@ class SSH2
$this->last_packet = microtime(true); $this->last_packet = microtime(true);
if (strlen($packet) != $sent) { if (strlen($packet) != $sent) {
$this->bitmap = 0; $this->disconnect_helper(NET_SSH2_DISCONNECT_BY_APPLICATION);
$message = $sent === false ? $message = $sent === false ?
'Unable to write ' . strlen($packet) . ' bytes' : 'Unable to write ' . strlen($packet) . ' bytes' :
"Only $sent of " . strlen($packet) . " bytes were sent"; "Only $sent of " . strlen($packet) . " bytes were sent";
@ -4152,12 +4143,23 @@ class SSH2
*/ */
protected function send_channel_packet(int $client_channel, string $data): void protected function send_channel_packet(int $client_channel, string $data): void
{ {
if (isset($this->channel_buffers_write[$client_channel])
&& strpos($data, $this->channel_buffers_write[$client_channel]) === 0
) {
// if buffer holds identical initial data content, resume send from the unmatched data portion
$data = substr($data, strlen($this->channel_buffers_write[$client_channel]));
} else {
$this->channel_buffers_write[$client_channel] = '';
}
while (strlen($data)) { while (strlen($data)) {
if (!$this->window_size_client_to_server[$client_channel]) { if (!$this->window_size_client_to_server[$client_channel]) {
$this->bitmap ^= self::MASK_WINDOW_ADJUST;
// using an invalid channel will let the buffers be built up for the valid channels // using an invalid channel will let the buffers be built up for the valid channels
$this->get_channel_packet(-1); $this->get_channel_packet(-$client_channel);
$this->bitmap ^= self::MASK_WINDOW_ADJUST; if ($this->isTimeout()) {
throw new TimeoutException('Timed out waiting for server');
} elseif (!$this->window_size_client_to_server[$client_channel]) {
throw new \RuntimeException('Data window was not adjusted');
}
} }
/* The maximum amount of data allowed is determined by the maximum /* The maximum amount of data allowed is determined by the maximum
@ -4178,7 +4180,9 @@ class SSH2
); );
$this->window_size_client_to_server[$client_channel] -= strlen($temp); $this->window_size_client_to_server[$client_channel] -= strlen($temp);
$this->send_binary_packet($packet); $this->send_binary_packet($packet);
$this->channel_buffers_write[$client_channel] .= $temp;
} }
unset($this->channel_buffers_write[$client_channel]);
} }
/** /**

View File

@ -10,7 +10,9 @@ declare(strict_types=1);
namespace phpseclib3\Tests\Unit\Net; namespace phpseclib3\Tests\Unit\Net;
use phpseclib3\Common\Functions\Strings;
use phpseclib3\Exception\InsufficientSetupException; use phpseclib3\Exception\InsufficientSetupException;
use phpseclib3\Exception\TimeoutException;
use phpseclib3\Net\SSH2; use phpseclib3\Net\SSH2;
use phpseclib3\Tests\PhpseclibTestCase; use phpseclib3\Tests\PhpseclibTestCase;
@ -268,6 +270,116 @@ class SSH2UnitTest extends PhpseclibTestCase
$this->assertEquals([0, 0], self::callFunc($ssh, 'get_stream_timeout')); $this->assertEquals([0, 0], self::callFunc($ssh, 'get_stream_timeout'));
} }
/**
* @requires PHPUnit < 10
*/
public function testSendChannelPacketNoBufferedData()
{
$ssh = $this->getMockBuilder('phpseclib3\Net\SSH2')
->disableOriginalConstructor()
->setMethods(['get_channel_packet', 'send_binary_packet'])
->getMock();
$ssh->expects($this->once())
->method('get_channel_packet')
->with(-1)
->willReturnCallback(function () use ($ssh) {
self::setVar($ssh, 'window_size_client_to_server', [1 => 0x7FFFFFFF]);
});
$ssh->expects($this->once())
->method('send_binary_packet')
->with(Strings::packSSH2('CNs', NET_SSH2_MSG_CHANNEL_DATA, 1, 'hello world'));
self::setVar($ssh, 'server_channels', [1 => 1]);
self::setVar($ssh, 'packet_size_client_to_server', [1 => 0x7FFFFFFF]);
self::setVar($ssh, 'window_size_client_to_server', [1 => 0]);
self::setVar($ssh, 'window_size_server_to_client', [1 => 0x7FFFFFFF]);
self::callFunc($ssh, 'send_channel_packet', [1, 'hello world']);
$this->assertEmpty(self::getVar($ssh, 'channel_buffers_write'));
}
/**
* @requires PHPUnit < 10
*/
public function testSendChannelPacketBufferedData()
{
$ssh = $this->getMockBuilder('phpseclib3\Net\SSH2')
->disableOriginalConstructor()
->setMethods(['get_channel_packet', 'send_binary_packet'])
->getMock();
$ssh->expects($this->once())
->method('get_channel_packet')
->with(-1)
->willReturnCallback(function () use ($ssh) {
self::setVar($ssh, 'window_size_client_to_server', [1 => 0x7FFFFFFF]);
});
$ssh->expects($this->once())
->method('send_binary_packet')
->with(Strings::packSSH2('CNs', NET_SSH2_MSG_CHANNEL_DATA, 1, ' world'));
self::setVar($ssh, 'channel_buffers_write', [1 => 'hello']);
self::setVar($ssh, 'server_channels', [1 => 1]);
self::setVar($ssh, 'packet_size_client_to_server', [1 => 0x7FFFFFFF]);
self::setVar($ssh, 'window_size_client_to_server', [1 => 0]);
self::setVar($ssh, 'window_size_server_to_client', [1 => 0x7FFFFFFF]);
self::callFunc($ssh, 'send_channel_packet', [1, 'hello world']);
$this->assertEmpty(self::getVar($ssh, 'channel_buffers_write'));
}
/**
* @requires PHPUnit < 10
*/
public function testSendChannelPacketTimeout()
{
$this->expectException(TimeoutException::class);
$this->expectExceptionMessage('Timed out waiting for server');
$ssh = $this->getMockBuilder('phpseclib3\Net\SSH2')
->disableOriginalConstructor()
->setMethods(['get_channel_packet', 'send_binary_packet'])
->getMock();
$ssh->expects($this->once())
->method('get_channel_packet')
->with(-1)
->willReturnCallback(function () use ($ssh) {
self::setVar($ssh, 'is_timeout', true);
});
$ssh->expects($this->once())
->method('send_binary_packet')
->with(Strings::packSSH2('CNs', NET_SSH2_MSG_CHANNEL_DATA, 1, 'hello'));
self::setVar($ssh, 'server_channels', [1 => 1]);
self::setVar($ssh, 'packet_size_client_to_server', [1 => 0x7FFFFFFF]);
self::setVar($ssh, 'window_size_client_to_server', [1 => 5]);
self::setVar($ssh, 'window_size_server_to_client', [1 => 0x7FFFFFFF]);
self::callFunc($ssh, 'send_channel_packet', [1, 'hello world']);
$this->assertEquals([1 => 'hello'], self::getVar($ssh, 'channel_buffers_write'));
}
/**
* @requires PHPUnit < 10
*/
public function testSendChannelPacketNoWindowAdjustment()
{
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('Data window was not adjusted');
$ssh = $this->getMockBuilder('phpseclib3\Net\SSH2')
->disableOriginalConstructor()
->setMethods(['get_channel_packet', 'send_binary_packet'])
->getMock();
$ssh->expects($this->once())
->method('get_channel_packet')
->with(-1);
$ssh->expects($this->never())
->method('send_binary_packet');
self::setVar($ssh, 'server_channels', [1 => 1]);
self::setVar($ssh, 'packet_size_client_to_server', [1 => 0x7FFFFFFF]);
self::setVar($ssh, 'window_size_client_to_server', [1 => 0]);
self::setVar($ssh, 'window_size_server_to_client', [1 => 0x7FFFFFFF]);
self::callFunc($ssh, 'send_channel_packet', [1, 'hello world']);
}
/** /**
* @return \phpseclib3\Net\SSH2 * @return \phpseclib3\Net\SSH2
*/ */