Introduce buffering to send channel packet for capability to resume across timeout

This commit is contained in:
Robert 2024-07-12 16:40:13 -04:00
parent 18d4c79bd4
commit e401ee05f5
2 changed files with 138 additions and 2 deletions

View File

@ -626,7 +626,7 @@ class SSH2
protected $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
* be placed in a buffer
@ -637,6 +637,17 @@ class SSH2
*/
private $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
*
@ -3446,6 +3457,8 @@ class SSH2
$this->get_seq_no = $this->send_seq_no = 0;
$this->channel_status = [];
$this->channel_id_last_interactive = 0;
$this->channel_buffers = [];
$this->channel_buffers_write = [];
}
/**
@ -4480,6 +4493,14 @@ class SSH2
*/
protected function send_channel_packet($client_channel, $data)
{
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)) {
if (!$this->window_size_client_to_server[$client_channel]) {
// using an invalid channel will let the buffers be built up for the valid channels
@ -4487,7 +4508,7 @@ class SSH2
if ($this->isTimeout()) {
throw new TimeoutException('Timed out waiting for server');
} elseif (!$this->window_size_client_to_server[$client_channel]) {
throw new \RuntimeException('Client to server window was not adjusted');
throw new \RuntimeException('Data window was not adjusted');
}
}
@ -4509,7 +4530,9 @@ class SSH2
);
$this->window_size_client_to_server[$client_channel] -= strlen($temp);
$this->send_binary_packet($packet);
$this->channel_buffers_write[$client_channel] .= $temp;
}
unset($this->channel_buffers_write[$client_channel]);
}
/**

View File

@ -8,8 +8,11 @@
namespace phpseclib3\Tests\Unit\Net;
use phpseclib3\Common\Functions\Strings;
use phpseclib3\Exception\InsufficientSetupException;
use phpseclib3\Exception\TimeoutException;
use phpseclib3\Net\SSH2;
use phpseclib3\Net\SSH2\MessageType;
use phpseclib3\Tests\PhpseclibTestCase;
class SSH2UnitTest extends PhpseclibTestCase
@ -271,6 +274,116 @@ class SSH2UnitTest extends PhpseclibTestCase
$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', MessageType::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', MessageType::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', MessageType::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
*/