diff --git a/phpseclib/Net/SSH2.php b/phpseclib/Net/SSH2.php index 9a78e60b..020a644c 100644 --- a/phpseclib/Net/SSH2.php +++ b/phpseclib/Net/SSH2.php @@ -115,6 +115,7 @@ class SSH2 public const MASK_LOGIN_REQ = 0x00000004; public const MASK_LOGIN = 0x00000008; public const MASK_SHELL = 0x00000010; + public const MASK_DISCONNECT = 0x00000020; /* * Channel constants @@ -4249,7 +4250,12 @@ class SSH2 */ protected function disconnect_helper(int $reason): bool { - if ($this->bitmap & self::MASK_CONNECTED) { + if ($this->bitmap & self::MASK_DISCONNECT) { + // Disregard subsequent disconnect requests + return false; + } + $this->bitmap |= self::MASK_DISCONNECT; + if ($this->isConnected()) { $data = Strings::packSSH2('CNss', MessageType::DISCONNECT, $reason, '', ''); try { $this->send_binary_packet($data); diff --git a/tests/Unit/Net/SSH2UnitTest.php b/tests/Unit/Net/SSH2UnitTest.php index b314ebfb..3ec03152 100644 --- a/tests/Unit/Net/SSH2UnitTest.php +++ b/tests/Unit/Net/SSH2UnitTest.php @@ -35,6 +35,24 @@ class SSH2UnitTest extends PhpseclibTestCase ]; } + /** + * @requires PHPUnit < 10 + * Verify that MASK_* constants remain distinct + */ + public function testBitmapMasks(): void + { + $reflection = new \ReflectionClass(SSH2::class); + $masks = array_filter($reflection->getConstants(), function ($k) { + return str_starts_with($k, 'MASK_'); + }, ARRAY_FILTER_USE_KEY); + $bitmap = 0; + foreach ($masks as $mask => $bit) { + $this->assertEquals(0, $bitmap & $bit, "Got unexpected mask {$mask}"); + $bitmap |= $bit; + $this->assertEquals($bit, $bitmap & $bit, "Absent expected mask {$mask}"); + } + } + /** * @dataProvider formatLogDataProvider * @requires PHPUnit < 10 @@ -381,6 +399,31 @@ class SSH2UnitTest extends PhpseclibTestCase self::callFunc($ssh, 'send_channel_packet', [1, 'hello world']); } + /** + * @requires PHPUnit < 10 + */ + public function testDisconnectHelper(): void + { + $ssh = $this->getMockBuilder('phpseclib3\Net\SSH2') + ->disableOriginalConstructor() + ->setMethods(['__destruct', 'isConnected', 'send_binary_packet']) + ->getMock(); + $ssh->expects($this->once()) + ->method('isConnected') + ->willReturn(true); + $ssh->expects($this->once()) + ->method('send_binary_packet') + ->with($this->isType('string')) + ->willReturnCallback(function () use ($ssh): void { + self::callFunc($ssh, 'disconnect_helper', [1]); + throw new \Exception('catch me'); + }); + + $this->assertEquals(0, self::getVar($ssh, 'bitmap')); + self::callFunc($ssh, 'disconnect_helper', [1]); + $this->assertEquals(0, self::getVar($ssh, 'bitmap')); + } + protected function createSSHMock(): SSH2 { return $this->getMockBuilder('phpseclib3\Net\SSH2')