mirror of
https://github.com/phpseclib/phpseclib.git
synced 2025-02-04 21:08:28 +00:00
Introduce buffering to send channel packet for capability to resume across timeout
This commit is contained in:
parent
18d4c79bd4
commit
e401ee05f5
@ -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]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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
|
||||
*/
|
||||
|
Loading…
x
Reference in New Issue
Block a user