Merge branch '3.0'

This commit is contained in:
terrafrost 2024-06-30 07:36:55 -05:00
commit cddea87362
4 changed files with 116 additions and 9 deletions

View File

@ -712,9 +712,11 @@ class SSH2
private bool $quiet_mode = false; private bool $quiet_mode = false;
/** /**
* Time of first network activity * Time of last read/write network activity
*
* @var float
*/ */
private float $last_packet; private float|null $last_packet = null;
/** /**
* Exit status returned from ssh if any * Exit status returned from ssh if any
@ -3164,9 +3166,10 @@ class SSH2
} }
if ($this->keepAlive > 0) { if ($this->keepAlive > 0) {
$elapsed = microtime(true) - $this->last_packet; $elapsed = microtime(true) - $this->last_packet;
if ($elapsed < $this->curTimeout) { $timeout = max($this->keepAlive - $elapsed, 0);
$sec = (int) floor($elapsed); if (!$this->curTimeout || $timeout < $this->curTimeout) {
$usec = (int) (1000000 * ($elapsed - $sec)); $sec = (int) floor($timeout);
$usec = (int) (1000000 * ($timeout - $sec));
} }
} }
return [$sec, $usec]; return [$sec, $usec];
@ -3188,6 +3191,7 @@ class SSH2
if ($this->binary_packet_buffer == null) { if ($this->binary_packet_buffer == null) {
// buffer the packet to permit continued reads across timeouts // buffer the packet to permit continued reads across timeouts
$this->binary_packet_buffer = (object) [ $this->binary_packet_buffer = (object) [
'read_time' => 0, // the time to read the packet from the socket
'raw' => '', // the raw payload read from the socket 'raw' => '', // the raw payload read from the socket
'plain' => '', // the packet in plain text, excluding packet_length header 'plain' => '', // the packet in plain text, excluding packet_length header
'packet_length' => null, // the packet_length value pulled from the payload 'packet_length' => null, // the packet_length value pulled from the payload
@ -3212,6 +3216,7 @@ class SSH2
$start = microtime(true); $start = microtime(true);
$raw = stream_get_contents($this->fsock, $packet->size - strlen($packet->raw)); $raw = stream_get_contents($this->fsock, $packet->size - strlen($packet->raw));
$elapsed = microtime(true) - $start; $elapsed = microtime(true) - $start;
$packet->read_time += $elapsed;
if ($this->curTimeout > 0) { if ($this->curTimeout > 0) {
$this->curTimeout -= $elapsed; $this->curTimeout -= $elapsed;
} }
@ -3361,11 +3366,11 @@ class SSH2
? "SSH_MSG_$constantName" ? "SSH_MSG_$constantName"
: "UNKNOWN ($value)", : "UNKNOWN ($value)",
round($current - $this->last_packet, 4), round($current - $this->last_packet, 4),
round($elapsed, 4) round($packet->read_time, 4)
); );
$this->append_log($message_number, $payload); $this->append_log($message_number, $payload);
$this->last_packet = $current;
} }
$this->last_packet = microtime(true);
return $this->filter($payload); return $this->filter($payload);
} }
@ -4016,8 +4021,8 @@ class SSH2
round($stop - $start, 4) round($stop - $start, 4)
); );
$this->append_log($message_number, $logged); $this->append_log($message_number, $logged);
$this->last_packet = $current;
} }
$this->last_packet = microtime(true);
if (strlen($packet) != $sent) { if (strlen($packet) != $sent) {
$this->bitmap = 0; $this->bitmap = 0;

View File

@ -571,6 +571,27 @@ class SSH2Test extends PhpseclibFunctionalTestCase
$this->assertSame(0, $ssh->getOpenChannelCount()); $this->assertSame(0, $ssh->getOpenChannelCount());
} }
public function testKeepAlive()
{
$ssh = $this->getSSH2();
$username = $this->getEnv('SSH_USERNAME');
$password = $this->getEnv('SSH_PASSWORD');
$ssh->setKeepAlive(1);
$ssh->setTimeout(1);
$this->assertNotEmpty($ssh->getServerIdentification());
$this->assertTrue(
$ssh->login($username, $password),
'SSH2 login using password failed.'
);
$ssh->write("pwd\n");
sleep(1); // permit keep alive to proc on next read
$this->assertNotEmpty($ssh->read('', SSH2::READ_NEXT));
$ssh->disconnect();
}
/** /**
* @return array * @return array
*/ */

View File

@ -83,11 +83,41 @@ abstract class PhpseclibTestCase extends TestCase
protected static function getVar($obj, $var) protected static function getVar($obj, $var)
{ {
$reflection = new \ReflectionClass($obj::class); $reflection = new \ReflectionClass($obj::class);
$prop = $reflection->getProperty($var); // private variables are not inherited, climb hierarchy until located
while (true) {
try {
$prop = $reflection->getProperty($var);
break;
} catch (\ReflectionException $e) {
$reflection = $reflection->getParentClass();
if (!$reflection) {
throw $e;
}
}
}
$prop->setAccessible(true); $prop->setAccessible(true);
return $prop->getValue($obj); return $prop->getValue($obj);
} }
protected static function setVar($obj, $var, $value)
{
$reflection = new \ReflectionClass(get_class($obj));
// private variables are not inherited, climb hierarchy until located
while (true) {
try {
$prop = $reflection->getProperty($var);
break;
} catch (\ReflectionException $e) {
$reflection = $reflection->getParentClass();
if (!$reflection) {
throw $e;
}
}
}
$prop->setAccessible(true);
$prop->setValue($obj, $value);
}
public static function callFunc($obj, $func, $params = []) public static function callFunc($obj, $func, $params = [])
{ {
$reflection = new \ReflectionClass($obj::class); $reflection = new \ReflectionClass($obj::class);

View File

@ -219,6 +219,57 @@ class SSH2UnitTest extends PhpseclibTestCase
} }
/** /**
* @requires PHPUnit < 10
*/
public function testGetStreamTimeout()
{
// no curTimeout, no keepAlive
$ssh = $this->createSSHMock();
$this->assertEquals([0, 0], self::callFunc($ssh, 'get_stream_timeout'));
// curTimeout, no keepAlive
$ssh = $this->createSSHMock();
$ssh->setTimeout(1);
$this->assertEquals([1, 0], self::callFunc($ssh, 'get_stream_timeout'));
// no curTimeout, keepAlive
$ssh = $this->createSSHMock();
$ssh->setKeepAlive(2);
self::setVar($ssh, 'last_packet', microtime(true));
list($sec, $usec) = self::callFunc($ssh, 'get_stream_timeout');
$this->assertGreaterThanOrEqual(1, $sec);
$this->assertLessThanOrEqual(2, $sec);
// smaller curTimeout, keepAlive
$ssh = $this->createSSHMock();
$ssh->setTimeout(1);
$ssh->setKeepAlive(2);
self::setVar($ssh, 'last_packet', microtime(true));
$this->assertEquals([1, 0], self::callFunc($ssh, 'get_stream_timeout'));
// curTimeout, smaller keepAlive
$ssh = $this->createSSHMock();
$ssh->setTimeout(5);
$ssh->setKeepAlive(2);
self::setVar($ssh, 'last_packet', microtime(true));
list($sec, $usec) = self::callFunc($ssh, 'get_stream_timeout');
$this->assertGreaterThanOrEqual(1, $sec);
$this->assertLessThanOrEqual(2, $sec);
// no curTimeout, keepAlive, no last_packet
$ssh = $this->createSSHMock();
$ssh->setKeepAlive(2);
$this->assertEquals([0, 0], self::callFunc($ssh, 'get_stream_timeout'));
// no curTimeout, keepAlive, last_packet exceeds keepAlive
$ssh = $this->createSSHMock();
$ssh->setKeepAlive(2);
self::setVar($ssh, 'last_packet', microtime(true) - 2);
$this->assertEquals([0, 0], self::callFunc($ssh, 'get_stream_timeout'));
}
/**
* @return \phpseclib3\Net\SSH2
*/ */
protected function createSSHMock(): SSH2 protected function createSSHMock(): SSH2
{ {