<?php

/**
 * @author    Andreas Fischer <bantu@phpbb.com>
 * @copyright 2014 Andreas Fischer
 * @license   http://www.opensource.org/licenses/mit-license.html  MIT License
 */

namespace phpseclib3\Tests\Functional\Net;

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());
    }
}
