* @copyright 2014 Andreas Fischer * @license http://www.opensource.org/licenses/mit-license.html MIT License */ namespace phpseclib3\Tests\Functional\Net; use phpseclib3\Exception\NoSupportedAlgorithmsException; use phpseclib3\Net\SSH2; use phpseclib3\Tests\PhpseclibFunctionalTestCase; class SSH2Test extends PhpseclibFunctionalTestCase { /** * @return SSH2 */ public function getSSH2() { return new SSH2($this->getEnv('SSH_HOSTNAME'), 22); } /** * @return SSH2 */ public function getSSH2Login() { $ssh = $this->getSSH2(); $username = $this->getEnv('SSH_USERNAME'); $password = $this->getEnv('SSH_PASSWORD'); $this->assertTrue( $ssh->login($username, $password), 'SSH2 login using password failed.' ); return $ssh; } public function testConstructor() { $ssh = $this->getSSH2(); $this->assertIsObject( $ssh, 'Could not construct NET_SSH2 object.' ); return $ssh; } /** * @depends testConstructor * @group github408 * @group github412 */ public function testPreLogin(SSH2 $ssh) { $this->assertFalse( $ssh->isConnected(), 'Failed asserting that SSH2 is not connected after construction.' ); $this->assertFalse( $ssh->isAuthenticated(), 'Failed asserting that SSH2 is not authenticated after construction.' ); $this->assertFalse( $ssh->isShellOpen(), 'Failed asserting that SSH2 does not have open shell after construction.' ); $this->assertEquals( 0, $ssh->getInteractiveChannelId(), 'Failed asserting that channel identifier 0 is returned.' ); $this->assertNotEmpty( $ssh->getServerPublicHostKey(), 'Failed asserting that a non-empty public host key was fetched.' ); $this->assertTrue( $ssh->isConnected(), 'Failed asserting that SSH2 is connected after public key fetch.' ); $this->assertNotEmpty( $ssh->getServerIdentification(), 'Failed asserting that the server identifier was set after connect.' ); return $ssh; } /** * @depends testPreLogin */ public function testBadPassword(SSH2 $ssh) { $username = $this->getEnv('SSH_USERNAME'); $password = $this->getEnv('SSH_PASSWORD'); $this->assertFalse( $ssh->login($username, 'zzz' . $password), 'SSH2 login using password succeeded.' ); $this->assertTrue( $ssh->isConnected(), 'Failed asserting that SSH2 is connected after bad login attempt.' ); $this->assertFalse( $ssh->isAuthenticated(), 'Failed asserting that SSH2 is not authenticated after bad login attempt.' ); $this->assertFalse( $ssh->isShellOpen(), 'Failed asserting that SSH2 does not have open shell after bad login attempt.' ); return $ssh; } /** * @depends testBadPassword */ public function testPasswordLogin(SSH2 $ssh) { $username = $this->getEnv('SSH_USERNAME'); $password = $this->getEnv('SSH_PASSWORD'); $this->assertTrue( $ssh->login($username, $password), 'SSH2 login using password failed.' ); $this->assertTrue( $ssh->isAuthenticated(), 'Failed asserting that SSH2 is authenticated after good login attempt.' ); $this->assertFalse( $ssh->isShellOpen(), 'Failed asserting that SSH2 does not have open shell after good login attempt.' ); return $ssh; } /** * @depends testPasswordLogin * @group github280 * @requires PHPUnit < 10 */ public function testExecWithMethodCallback(SSH2 $ssh) { $callbackObject = $this->getMockBuilder('stdClass') ->setMethods(['callbackMethod']) ->getMock(); $callbackObject ->expects($this->atLeastOnce()) ->method('callbackMethod') ->will($this->returnValue(true)); $ssh->exec('pwd', [$callbackObject, 'callbackMethod']); $this->assertFalse( $ssh->isPTYOpen(), 'Failed asserting that SSH2 does not have open exec channel after exec.' ); $this->assertFalse( $ssh->isShellOpen(), 'Failed asserting that SSH2 does not have open shell channel after exec.' ); $this->assertEquals( 0, $ssh->getInteractiveChannelId(), 'Failed asserting that channel identifier 0 is returned after exec.' ); return $ssh; } public function testGetServerPublicHostKey() { $ssh = $this->getSSH2(); $this->assertIsString($ssh->getServerPublicHostKey()); } public function testOpenSocketConnect() { $fsock = fsockopen($this->getEnv('SSH_HOSTNAME'), 22); $ssh = new SSH2($fsock); $username = $this->getEnv('SSH_USERNAME'); $password = $this->getEnv('SSH_PASSWORD'); $this->assertTrue( $ssh->login($username, $password), 'SSH2 login using an open socket failed.' ); } /** * @depends testExecWithMethodCallback * @group github1009 */ public function testDisablePTY(SSH2 $ssh) { $ssh->enablePTY(); $this->assertTrue( $ssh->isPTYEnabled(), 'Failed asserting that pty was enabled.' ); $this->assertFalse( $ssh->isPTYOpen(), 'Failed asserting that pty was not open after enable.' ); $this->assertEquals( 0, $ssh->getInteractiveChannelId(), 'Failed asserting that 0 channel identifier is returned prior to opening.' ); $ssh->exec('ls -latr'); $this->assertTrue( $ssh->isPTYOpen(), 'Failed asserting that pty was open.' ); $this->assertFalse( $ssh->isShellOpen(), 'Failed asserting that shell was not open after pty exec.' ); $this->assertEquals( SSH2::CHANNEL_EXEC, $ssh->getInteractiveChannelId(), 'Failed asserting that exec channel identifier is returned after exec.' ); $ssh->disablePTY(); $this->assertFalse( $ssh->isPTYEnabled(), 'Failed asserting that pty was disabled.' ); $this->assertFalse( $ssh->isPTYOpen(), 'Failed asserting that pty was not open after disable.' ); $this->assertEquals( SSH2::CHANNEL_EXEC, $ssh->getInteractiveChannelId(), 'Failed asserting that exec channel identifier is returned after pty exec close.' ); $ssh->exec('pwd'); $this->assertFalse( $ssh->isPTYOpen(), 'Failed asserting that pty was not open after exec.' ); return $ssh; } /** * @depends testDisablePTY * @group github1167 */ public function testChannelDataAfterOpen(SSH2 $ssh) { // Ubuntu's OpenSSH from 5.8 to 6.9 didn't work with multiple channels. see // https://bugs.launchpad.net/ubuntu/+source/openssh/+bug/1334916 for more info. // https://lists.ubuntu.com/archives/oneiric-changes/2011-July/005772.html discusses // when consolekit was incorporated. // https://marc.info/?l=openssh-unix-dev&m=163409903417589&w=2 discusses some of the // issues with how Ubuntu incorporated consolekit $pattern = '#^SSH-2\.0-OpenSSH_([\d.]+)[^ ]* Ubuntu-.*$#'; $match = preg_match($pattern, $ssh->getServerIdentification(), $matches); $match = $match && version_compare('5.8', $matches[1], '<='); $match = $match && version_compare('6.9', $matches[1], '>='); if ($match) { self::markTestSkipped('Ubuntu\'s OpenSSH >= 5.8 <= 6.9 didn\'t work well with multiple channels'); } $ssh->write("ping 127.0.0.1\n"); $this->assertTrue( $ssh->isShellOpen(), 'Failed asserting that SSH2 has open shell after shell read/write.' ); $this->assertFalse( $ssh->isPTYOpen(), 'Failed asserting that pty was not open after shell read/write.' ); $this->assertEquals( SSH2::CHANNEL_SHELL, $ssh->getInteractiveChannelId(), 'Failed asserting that shell channel identifier is returned after shell read/write.' ); $ssh->enablePTY(); $this->assertTrue( $ssh->exec('bash'), 'Failed asserting exec command succeeded.' ); $this->assertTrue( $ssh->isShellOpen(), 'Failed asserting that SSH2 has open shell after pty exec.' ); $this->assertTrue( $ssh->isPTYOpen(), 'Failed asserting that pty was not open after exec.' ); $this->assertEquals( SSH2::CHANNEL_EXEC, $ssh->getInteractiveChannelId(), 'Failed asserting that exec channel identifier is returned after pty exec.' ); $ssh->write("ls -latr\n"); $ssh->setTimeout(1); $this->assertIsString($ssh->read()); $this->assertTrue( $ssh->isTimeout(), 'Failed asserting that pty exec read timed out' ); $this->assertTrue( $ssh->isShellOpen(), 'Failed asserting that SSH2 shell remains open across pty exec read/write.' ); $this->assertTrue( $ssh->isPTYOpen(), 'Failed asserting that pty was open after read timeout.' ); } public function testOpenShell() { $ssh = $this->getSSH2Login(); $this->assertTrue( $ssh->openShell(), 'SSH2 shell initialization failed.' ); $this->assertTrue( $ssh->isShellOpen(), 'Failed asserting that SSH2 has open shell after init.' ); $this->assertNotFalse( $ssh->read(), 'Failed asserting that read succeeds.' ); $ssh->write('hello'); $this->assertTrue( $ssh->isShellOpen(), 'Failed asserting that SSH2 has open shell after read/write.' ); $this->assertEquals( SSH2::CHANNEL_SHELL, $ssh->getInteractiveChannelId(), 'Failed asserting that shell channel identifier is returned after read/write.' ); return $ssh; } /** * @depends testOpenShell */ public function testResetOpenShell(SSH2 $ssh) { $ssh->reset(); $this->assertFalse( $ssh->isShellOpen(), 'Failed asserting that SSH2 has open shell after reset.' ); $this->assertEquals( SSH2::CHANNEL_SHELL, $ssh->getInteractiveChannelId(), 'Failed asserting that shell channel identifier is returned after reset.' ); } public function testMultipleExecPty() { $this->expectException(\RuntimeException::class); $this->expectExceptionMessage('Please close the channel (1) before trying to open it again'); $ssh = $this->getSSH2Login(); $ssh->enablePTY(); $ssh->exec('bash'); $ssh->exec('bash'); } public function testMultipleInteractiveChannels() { $ssh = $this->getSSH2Login(); $this->assertTrue( $ssh->openShell(), 'SSH2 shell initialization failed.' ); $this->assertEquals( SSH2::CHANNEL_SHELL, $ssh->getInteractiveChannelId(), 'Failed asserting that shell channel identifier is returned after open shell.' ); $ssh->setTimeout(1); $this->assertIsString( $ssh->read(), 'Failed asserting that read succeeds after shell init' ); $directory = $ssh->exec('pwd'); $this->assertFalse( $ssh->isTimeout(), 'failed' ); $this->assertIsString( $directory, 'Failed asserting that exec succeeds after shell read/write' ); $ssh->write("pwd\n"); $this->assertStringContainsString( trim($directory), $ssh->read(), 'Failed asserting that current directory can be read from shell after exec' ); $ssh->enablePTY(); $this->assertTrue( $ssh->exec('bash'), 'Failed asserting that pty exec succeeds' ); $this->assertTrue( $ssh->isShellOpen(), 'Failed asserting that SSH2 has open shell after pty exec.' ); $this->assertEquals( SSH2::CHANNEL_EXEC, $ssh->getInteractiveChannelId(), 'Failed asserting that exec channel identifier is returned after pty exec.' ); $ssh->write("pwd\n", SSH2::CHANNEL_SHELL); $this->assertStringContainsString( trim($directory), $ssh->read('', SSH2::READ_SIMPLE, SSH2::CHANNEL_SHELL), 'Failed asserting that current directory can be read from shell after pty exec' ); $this->assertTrue( $ssh->isPTYOpen(), 'Failed asserting that SSH2 has open pty exec after shell read/write.' ); $ssh->write("pwd\n", SSH2::CHANNEL_EXEC); $this->assertIsString( $ssh->read('', SSH2::READ_SIMPLE, SSH2::CHANNEL_EXEC), 'Failed asserting that pty exec read succeeds' ); $ssh->reset(SSH2::CHANNEL_EXEC); $this->assertFalse( $ssh->isPTYOpen(), 'Failed asserting that SSH2 has closed pty exec after reset.' ); $ssh->disablePTY(); $this->assertTrue( $ssh->isShellOpen(), 'Failed asserting that SSH2 has open shell after pty exec.' ); $ssh->write("pwd\n", SSH2::CHANNEL_SHELL); $this->assertStringContainsString( trim($directory), $ssh->read('', SSH2::READ_SIMPLE, SSH2::CHANNEL_SHELL), 'Failed asserting that current directory can be read from shell after pty exec' ); $ssh->reset(SSH2::CHANNEL_SHELL); $this->assertFalse( $ssh->isShellOpen(), 'Failed asserting that SSH2 has closed shell after reset.' ); $this->assertEquals( SSH2::CHANNEL_EXEC, $ssh->getInteractiveChannelId(), 'Failed asserting that exec channel identifier is maintained as last opened channel.' ); } public function testReadingOfClosedChannel() { $ssh = $this->getSSH2Login(); $this->assertSame(0, $ssh->getOpenChannelCount()); $ssh->enablePTY(); $ssh->exec('ping -c 3 127.0.0.1; exit'); $ssh->write("ping 127.0.0.2\n", SSH2::CHANNEL_SHELL); $ssh->setTimeout(3); $output = $ssh->read('', SSH2::READ_SIMPLE, SSH2::CHANNEL_SHELL); $this->assertStringContainsString('PING 127.0.0.2', $output); $output = $ssh->read('', SSH2::READ_SIMPLE, SSH2::CHANNEL_EXEC); $this->assertStringContainsString('PING 127.0.0.1', $output); $this->assertSame(1, $ssh->getOpenChannelCount()); $ssh->reset(SSH2::CHANNEL_SHELL); $this->assertSame(0, $ssh->getOpenChannelCount()); } public function testPing() { $ssh = $this->getSSH2(); // assert on unauthenticated ssh2 $this->assertNotEmpty($ssh->getServerIdentification()); $this->assertFalse($ssh->ping()); $this->assertTrue($ssh->isConnected()); $this->assertSame(0, $ssh->getOpenChannelCount()); $ssh = $this->getSSH2Login(); $this->assertTrue($ssh->ping()); $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 */ public static function getCryptoAlgorithms() { $map = [ 'kex' => SSH2::getSupportedKEXAlgorithms(), 'hostkey' => SSH2::getSupportedHostKeyAlgorithms(), 'comp' => SSH2::getSupportedCompressionAlgorithms(), 'crypt' => SSH2::getSupportedEncryptionAlgorithms(), 'mac' => SSH2::getSupportedMACAlgorithms(), ]; $tests = []; foreach ($map as $type => $algorithms) { foreach ($algorithms as $algorithm) { $tests[] = [$type, $algorithm]; } } return $tests; } /** * @dataProvider getCryptoAlgorithms * @param string $type * @param string $algorithm */ public function testCryptoAlgorithms($type, $algorithm) { $ssh = $this->getSSH2(); try { switch ($type) { case 'kex': case 'hostkey': $ssh->setPreferredAlgorithms([$type => [$algorithm]]); $this->assertEquals($algorithm, $ssh->getAlgorithmsNegotiated()[$type]); break; case 'comp': case 'crypt': $ssh->setPreferredAlgorithms([ 'client_to_server' => [$type => [$algorithm]], 'server_to_client' => [$type => [$algorithm]], ]); $this->assertEquals($algorithm, $ssh->getAlgorithmsNegotiated()['client_to_server'][$type]); $this->assertEquals($algorithm, $ssh->getAlgorithmsNegotiated()['server_to_client'][$type]); break; case 'mac': $macCryptAlgorithms = array_filter( SSH2::getSupportedEncryptionAlgorithms(), function ($algorithm) use ($ssh) { return !self::callFunc($ssh, 'encryption_algorithm_to_crypt_instance', [$algorithm]) ->usesNonce(); } ); $ssh->setPreferredAlgorithms([ 'client_to_server' => ['crypt' => $macCryptAlgorithms, 'mac' => [$algorithm]], 'server_to_client' => ['crypt' => $macCryptAlgorithms, 'mac' => [$algorithm]], ]); $this->assertEquals($algorithm, $ssh->getAlgorithmsNegotiated()['client_to_server']['mac']); $this->assertEquals($algorithm, $ssh->getAlgorithmsNegotiated()['server_to_client']['mac']); break; } } catch (NoSupportedAlgorithmsException $e) { self::markTestSkipped("{$type} algorithm {$algorithm} is not supported by server"); } $username = $this->getEnv('SSH_USERNAME'); $password = $this->getEnv('SSH_PASSWORD'); $this->assertTrue( $ssh->login($username, $password), "SSH2 login using {$type} {$algorithm} failed." ); $ssh->setTimeout(1); $ssh->write("pwd\n"); $this->assertNotEmpty($ssh->read('', SSH2::READ_NEXT)); $ssh->disconnect(); } }