mirror of
https://github.com/phpseclib/phpseclib.git
synced 2024-12-29 12:32:45 +00:00
Merge pull request #1898 from rposky/master
SSH2: Better support for multiple interactive channels & expose shell functions: 3.0 Backport Master Merge
This commit is contained in:
commit
5a208267d6
@ -549,6 +549,13 @@ class SSH2
|
|||||||
*/
|
*/
|
||||||
protected array $channel_status = [];
|
protected array $channel_status = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The identifier of the interactive channel which was opened most recently
|
||||||
|
*
|
||||||
|
* @see self::getInteractiveChannelId()
|
||||||
|
*/
|
||||||
|
private int $channel_id_last_interactive = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Packet Size
|
* Packet Size
|
||||||
*
|
*
|
||||||
@ -721,16 +728,6 @@ class SSH2
|
|||||||
*/
|
*/
|
||||||
private bool $request_pty = false;
|
private bool $request_pty = false;
|
||||||
|
|
||||||
/**
|
|
||||||
* Flag set while exec() is running when using enablePTY()
|
|
||||||
*/
|
|
||||||
private bool $in_request_pty_exec = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Flag set after startSubsystem() is called
|
|
||||||
*/
|
|
||||||
private bool $in_subsystem = false;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Contents of stdError
|
* Contents of stdError
|
||||||
*/
|
*/
|
||||||
@ -2431,7 +2428,7 @@ class SSH2
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->in_request_pty_exec) {
|
if ($this->isPTYOpen()) {
|
||||||
throw new RuntimeException('If you want to run multiple exec()\'s you will need to disable (and re-enable if appropriate) a PTY for each one.');
|
throw new RuntimeException('If you want to run multiple exec()\'s you will need to disable (and re-enable if appropriate) a PTY for each one.');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2481,8 +2478,6 @@ class SSH2
|
|||||||
$this->disconnect_helper(DisconnectReason::BY_APPLICATION);
|
$this->disconnect_helper(DisconnectReason::BY_APPLICATION);
|
||||||
throw new RuntimeException('Unable to request pseudo-terminal');
|
throw new RuntimeException('Unable to request pseudo-terminal');
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->in_request_pty_exec = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// sending a pty-req SSH_MSG_CHANNEL_REQUEST message is unnecessary and, in fact, in most cases, slows things
|
// sending a pty-req SSH_MSG_CHANNEL_REQUEST message is unnecessary and, in fact, in most cases, slows things
|
||||||
@ -2512,7 +2507,8 @@ class SSH2
|
|||||||
|
|
||||||
$this->channel_status[self::CHANNEL_EXEC] = MessageType::CHANNEL_DATA;
|
$this->channel_status[self::CHANNEL_EXEC] = MessageType::CHANNEL_DATA;
|
||||||
|
|
||||||
if ($this->in_request_pty_exec) {
|
if ($this->request_pty === true) {
|
||||||
|
$this->channel_id_last_interactive = self::CHANNEL_EXEC;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2540,15 +2536,24 @@ class SSH2
|
|||||||
/**
|
/**
|
||||||
* Creates an interactive shell
|
* Creates an interactive shell
|
||||||
*
|
*
|
||||||
|
* Returns bool(true) if the shell was opened.
|
||||||
|
* Returns bool(false) if the shell was already open.
|
||||||
|
*
|
||||||
|
* @throws InsufficientSetupException if not authenticated
|
||||||
* @throws UnexpectedValueException on receipt of unexpected packets
|
* @throws UnexpectedValueException on receipt of unexpected packets
|
||||||
* @throws RuntimeException on other errors
|
* @throws RuntimeException on other errors
|
||||||
*@see self::read()
|
* @see self::isShellOpen()
|
||||||
|
* @see self::read()
|
||||||
* @see self::write()
|
* @see self::write()
|
||||||
*/
|
*/
|
||||||
private function initShell(): bool
|
public function openShell(): bool
|
||||||
{
|
{
|
||||||
if ($this->in_request_pty_exec === true) {
|
if ($this->isShellOpen()) {
|
||||||
return true;
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->isAuthenticated()) {
|
||||||
|
throw new InsufficientSetupException('Operation disallowed prior to login()');
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->window_size_server_to_client[self::CHANNEL_SHELL] = $this->window_size;
|
$this->window_size_server_to_client[self::CHANNEL_SHELL] = $this->window_size;
|
||||||
@ -2608,29 +2613,41 @@ class SSH2
|
|||||||
|
|
||||||
$this->channel_status[self::CHANNEL_SHELL] = MessageType::CHANNEL_DATA;
|
$this->channel_status[self::CHANNEL_SHELL] = MessageType::CHANNEL_DATA;
|
||||||
|
|
||||||
|
$this->channel_id_last_interactive = self::CHANNEL_SHELL;
|
||||||
|
|
||||||
$this->bitmap |= self::MASK_SHELL;
|
$this->bitmap |= self::MASK_SHELL;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the channel to be used with read() / write()
|
* Return the channel to be used with read(), write(), and reset(), if none were specified
|
||||||
*
|
* @deprecated for lack of transparency in intended channel target, to be potentially replaced
|
||||||
|
* with method which guarantees open-ness of all yielded channels and throws
|
||||||
|
* error for multiple open channels
|
||||||
* @see self::read()
|
* @see self::read()
|
||||||
* @see self::write()
|
* @see self::write()
|
||||||
*/
|
*/
|
||||||
private function get_interactive_channel(): int
|
private function get_interactive_channel(): int
|
||||||
{
|
{
|
||||||
switch (true) {
|
switch (true) {
|
||||||
case $this->in_subsystem:
|
case $this->is_channel_status_data(self::CHANNEL_SUBSYSTEM):
|
||||||
return self::CHANNEL_SUBSYSTEM;
|
return self::CHANNEL_SUBSYSTEM;
|
||||||
case $this->in_request_pty_exec:
|
case $this->is_channel_status_data(self::CHANNEL_EXEC):
|
||||||
return self::CHANNEL_EXEC;
|
return self::CHANNEL_EXEC;
|
||||||
default:
|
default:
|
||||||
return self::CHANNEL_SHELL;
|
return self::CHANNEL_SHELL;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates the DATA status on the given channel
|
||||||
|
*/
|
||||||
|
private function is_channel_status_data(int $channel): bool
|
||||||
|
{
|
||||||
|
return isset($this->channel_status[$channel]) && $this->channel_status[$channel] == MessageType::CHANNEL_DATA;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return an available open channel
|
* Return an available open channel
|
||||||
*
|
*
|
||||||
@ -2685,25 +2702,37 @@ class SSH2
|
|||||||
* Returns when there's a match for $expect, which can take the form of a string literal or,
|
* Returns when there's a match for $expect, which can take the form of a string literal or,
|
||||||
* if $mode == self::READ_REGEX, a regular expression.
|
* if $mode == self::READ_REGEX, a regular expression.
|
||||||
*
|
*
|
||||||
|
* If not specifying a channel, an open interactive channel will be selected, or, if there are
|
||||||
|
* no open channels, an interactive shell will be created. If there are multiple open
|
||||||
|
* interactive channels, a legacy behavior will apply in which channel selection prioritizes
|
||||||
|
* an active subsystem, the exec pty, and, lastly, the shell. If using multiple interactive
|
||||||
|
* channels, callers are discouraged from relying on this legacy behavior and should specify
|
||||||
|
* the intended channel.
|
||||||
|
*
|
||||||
|
* @param int $mode One of the self::READ_* constants
|
||||||
|
* @param int|null $channel Channel id returned by self::getInteractiveChannelId()
|
||||||
* @return string|bool|null
|
* @return string|bool|null
|
||||||
* @throws RuntimeException on connection error
|
* @throws RuntimeException on connection error
|
||||||
|
* @throws InsufficientSetupException on unexpected channel status, possibly due to closure
|
||||||
* @see self::write()
|
* @see self::write()
|
||||||
*/
|
*/
|
||||||
public function read(string $expect = '', int $mode = self::READ_SIMPLE)
|
public function read(string $expect = '', int $mode = self::READ_SIMPLE, int $channel = null)
|
||||||
{
|
{
|
||||||
$this->curTimeout = $this->timeout;
|
$this->curTimeout = $this->timeout;
|
||||||
$this->is_timeout = false;
|
$this->is_timeout = false;
|
||||||
|
|
||||||
if (!$this->isAuthenticated()) {
|
if ($channel === null) {
|
||||||
throw new InsufficientSetupException('Operation disallowed prior to login()');
|
$channel = $this->get_interactive_channel();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!($this->bitmap & self::MASK_SHELL) && !$this->initShell()) {
|
if (!$this->isInteractiveChannelOpen($channel)) {
|
||||||
throw new RuntimeException('Unable to initiate an interactive shell session');
|
if ($channel != self::CHANNEL_SHELL) {
|
||||||
|
throw new InsufficientSetupException('Data is not available on channel');
|
||||||
|
} elseif (!$this->openShell()) {
|
||||||
|
throw new RuntimeException('Unable to initiate an interactive shell session');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$channel = $this->get_interactive_channel();
|
|
||||||
|
|
||||||
if ($mode == self::READ_NEXT) {
|
if ($mode == self::READ_NEXT) {
|
||||||
return $this->get_channel_packet($channel);
|
return $this->get_channel_packet($channel);
|
||||||
}
|
}
|
||||||
@ -2720,7 +2749,6 @@ class SSH2
|
|||||||
}
|
}
|
||||||
$response = $this->get_channel_packet($channel);
|
$response = $this->get_channel_packet($channel);
|
||||||
if ($response === true) {
|
if ($response === true) {
|
||||||
$this->in_request_pty_exec = false;
|
|
||||||
return Strings::shift($this->interactiveBuffer, strlen($this->interactiveBuffer));
|
return Strings::shift($this->interactiveBuffer, strlen($this->interactiveBuffer));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2731,20 +2759,33 @@ class SSH2
|
|||||||
/**
|
/**
|
||||||
* Inputs a command into an interactive shell.
|
* Inputs a command into an interactive shell.
|
||||||
*
|
*
|
||||||
|
* If not specifying a channel, an open interactive channel will be selected, or, if there are
|
||||||
|
* no open channels, an interactive shell will be created. If there are multiple open
|
||||||
|
* interactive channels, a legacy behavior will apply in which channel selection prioritizes
|
||||||
|
* an active subsystem, the exec pty, and, lastly, the shell. If using multiple interactive
|
||||||
|
* channels, callers are discouraged from relying on this legacy behavior and should specify
|
||||||
|
* the intended channel.
|
||||||
|
*
|
||||||
|
* @param int|null $channel Channel id returned by self::getInteractiveChannelId()
|
||||||
* @throws RuntimeException on connection error
|
* @throws RuntimeException on connection error
|
||||||
|
* @throws InsufficientSetupException on unexpected channel status, possibly due to closure
|
||||||
* @see SSH2::read()
|
* @see SSH2::read()
|
||||||
*/
|
*/
|
||||||
public function write(string $cmd): void
|
public function write(string $cmd, int $channel = null): void
|
||||||
{
|
{
|
||||||
if (!$this->isAuthenticated()) {
|
if ($channel === null) {
|
||||||
throw new InsufficientSetupException('Operation disallowed prior to login()');
|
$channel = $this->get_interactive_channel();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!($this->bitmap & self::MASK_SHELL) && !$this->initShell()) {
|
if (!$this->isInteractiveChannelOpen($channel)) {
|
||||||
throw new RuntimeException('Unable to initiate an interactive shell session');
|
if ($channel != self::CHANNEL_SHELL) {
|
||||||
|
throw new InsufficientSetupException('Data is not available on channel');
|
||||||
|
} elseif (!$this->openShell()) {
|
||||||
|
throw new RuntimeException('Unable to initiate an interactive shell session');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->send_channel_packet($this->get_interactive_channel(), $cmd);
|
$this->send_channel_packet($channel, $cmd);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -2795,8 +2836,7 @@ class SSH2
|
|||||||
|
|
||||||
$this->channel_status[self::CHANNEL_SUBSYSTEM] = MessageType::CHANNEL_DATA;
|
$this->channel_status[self::CHANNEL_SUBSYSTEM] = MessageType::CHANNEL_DATA;
|
||||||
|
|
||||||
$this->bitmap |= self::MASK_SHELL;
|
$this->channel_id_last_interactive = self::CHANNEL_SUBSYSTEM;
|
||||||
$this->in_subsystem = true;
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -2808,8 +2848,9 @@ class SSH2
|
|||||||
*/
|
*/
|
||||||
public function stopSubsystem(): bool
|
public function stopSubsystem(): bool
|
||||||
{
|
{
|
||||||
$this->in_subsystem = false;
|
if ($this->isInteractiveChannelOpen(self::CHANNEL_SUBSYSTEM)) {
|
||||||
$this->close_channel(self::CHANNEL_SUBSYSTEM);
|
$this->close_channel(self::CHANNEL_SUBSYSTEM);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2817,10 +2858,23 @@ class SSH2
|
|||||||
* Closes a channel
|
* Closes a channel
|
||||||
*
|
*
|
||||||
* If read() timed out you might want to just close the channel and have it auto-restart on the next read() call
|
* If read() timed out you might want to just close the channel and have it auto-restart on the next read() call
|
||||||
|
*
|
||||||
|
* If not specifying a channel, an open interactive channel will be selected. If there are
|
||||||
|
* multiple open interactive channels, a legacy behavior will apply in which channel selection
|
||||||
|
* prioritizes an active subsystem, the exec pty, and, lastly, the shell. If using multiple
|
||||||
|
* interactive channels, callers are discouraged from relying on this legacy behavior and
|
||||||
|
* should specify the intended channel.
|
||||||
|
*
|
||||||
|
* @param int|null $channel Channel id returned by self::getInteractiveChannelId()
|
||||||
*/
|
*/
|
||||||
public function reset(): void
|
public function reset(int $channel = null): void
|
||||||
{
|
{
|
||||||
$this->close_channel($this->get_interactive_channel());
|
if ($channel === null) {
|
||||||
|
$channel = $this->get_interactive_channel();
|
||||||
|
}
|
||||||
|
if ($this->isInteractiveChannelOpen($channel)) {
|
||||||
|
$this->close_channel($channel);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -2872,6 +2926,43 @@ class SSH2
|
|||||||
return (bool) ($this->bitmap & self::MASK_LOGIN);
|
return (bool) ($this->bitmap & self::MASK_LOGIN);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is the interactive shell active?
|
||||||
|
*/
|
||||||
|
public function isShellOpen(): bool
|
||||||
|
{
|
||||||
|
return $this->isInteractiveChannelOpen(self::CHANNEL_SHELL);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is the exec pty active?
|
||||||
|
*/
|
||||||
|
public function isPTYOpen(): bool
|
||||||
|
{
|
||||||
|
return $this->isInteractiveChannelOpen(self::CHANNEL_EXEC);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is the given interactive channel active?
|
||||||
|
*
|
||||||
|
* @param int $channel Channel id returned by self::getInteractiveChannelId()
|
||||||
|
*/
|
||||||
|
public function isInteractiveChannelOpen(int $channel): bool
|
||||||
|
{
|
||||||
|
return $this->isAuthenticated() && $this->is_channel_status_data($channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a channel identifier, presently of the last interactive channel opened, regardless of current status.
|
||||||
|
* Returns 0 if no interactive channel has been opened.
|
||||||
|
*
|
||||||
|
* @see self::isInteractiveChannelOpen()
|
||||||
|
*/
|
||||||
|
public function getInteractiveChannelId(): int
|
||||||
|
{
|
||||||
|
return $this->channel_id_last_interactive;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pings a server connection, or tries to reconnect if the connection has gone down
|
* Pings a server connection, or tries to reconnect if the connection has gone down
|
||||||
*
|
*
|
||||||
@ -3448,9 +3539,8 @@ class SSH2
|
|||||||
*/
|
*/
|
||||||
public function disablePTY(): void
|
public function disablePTY(): void
|
||||||
{
|
{
|
||||||
if ($this->in_request_pty_exec) {
|
if ($this->isPTYOpen()) {
|
||||||
$this->close_channel(self::CHANNEL_EXEC);
|
$this->close_channel(self::CHANNEL_EXEC);
|
||||||
$this->in_request_pty_exec = false;
|
|
||||||
}
|
}
|
||||||
$this->request_pty = false;
|
$this->request_pty = false;
|
||||||
}
|
}
|
||||||
@ -3475,6 +3565,7 @@ class SSH2
|
|||||||
* - if the connection times out
|
* - if the connection times out
|
||||||
* - if the channel status is CHANNEL_OPEN and the response was CHANNEL_OPEN_CONFIRMATION
|
* - if the channel status is CHANNEL_OPEN and the response was CHANNEL_OPEN_CONFIRMATION
|
||||||
* - if the channel status is CHANNEL_REQUEST and the response was CHANNEL_SUCCESS
|
* - if the channel status is CHANNEL_REQUEST and the response was CHANNEL_SUCCESS
|
||||||
|
* - if the channel status is CHANNEL_CLOSE and the response was CHANNEL_CLOSE
|
||||||
*
|
*
|
||||||
* bool(false) is returned if:
|
* bool(false) is returned if:
|
||||||
*
|
*
|
||||||
@ -3639,7 +3730,10 @@ class SSH2
|
|||||||
throw new RuntimeException('Unable to fulfill channel request');
|
throw new RuntimeException('Unable to fulfill channel request');
|
||||||
}
|
}
|
||||||
case MessageType::CHANNEL_CLOSE:
|
case MessageType::CHANNEL_CLOSE:
|
||||||
return $type == MessageType::CHANNEL_CLOSE ? true : $this->get_channel_packet($client_channel, $skip_extended);
|
if ($client_channel == $channel && $type == MessageType::CHANNEL_CLOSE) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return $this->get_channel_packet($client_channel, $skip_extended);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3674,9 +3768,8 @@ class SSH2
|
|||||||
case MessageType::CHANNEL_CLOSE:
|
case MessageType::CHANNEL_CLOSE:
|
||||||
$this->curTimeout = 5;
|
$this->curTimeout = 5;
|
||||||
|
|
||||||
if ($this->bitmap & self::MASK_SHELL) {
|
$this->close_channel_bitmap($channel);
|
||||||
$this->bitmap &= ~self::MASK_SHELL;
|
|
||||||
}
|
|
||||||
if ($this->channel_status[$channel] != MessageType::CHANNEL_EOF) {
|
if ($this->channel_status[$channel] != MessageType::CHANNEL_EOF) {
|
||||||
$this->send_binary_packet(pack('CN', MessageType::CHANNEL_CLOSE, $this->server_channels[$channel]));
|
$this->send_binary_packet(pack('CN', MessageType::CHANNEL_CLOSE, $this->server_channels[$channel]));
|
||||||
}
|
}
|
||||||
@ -4002,16 +4095,26 @@ class SSH2
|
|||||||
while (!is_bool($this->get_channel_packet($client_channel))) {
|
while (!is_bool($this->get_channel_packet($client_channel))) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->is_timeout) {
|
|
||||||
$this->disconnect();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($want_reply) {
|
if ($want_reply) {
|
||||||
$this->send_binary_packet(pack('CN', MessageType::CHANNEL_CLOSE, $this->server_channels[$client_channel]));
|
$this->send_binary_packet(pack('CN', MessageType::CHANNEL_CLOSE, $this->server_channels[$client_channel]));
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->bitmap & self::MASK_SHELL) {
|
$this->close_channel_bitmap($client_channel);
|
||||||
$this->bitmap &= ~self::MASK_SHELL;
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maintains execution state bitmap in response to channel closure
|
||||||
|
*/
|
||||||
|
private function close_channel_bitmap(int $client_channel): void
|
||||||
|
{
|
||||||
|
switch ($client_channel) {
|
||||||
|
case self::CHANNEL_SHELL:
|
||||||
|
// Shell status has been maintained in the bitmap for backwards
|
||||||
|
// compatibility sake, but can be removed going forward
|
||||||
|
if ($this->bitmap & self::MASK_SHELL) {
|
||||||
|
$this->bitmap &= ~self::MASK_SHELL;
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,9 +15,28 @@ use phpseclib3\Tests\PhpseclibFunctionalTestCase;
|
|||||||
|
|
||||||
class SSH2Test extends PhpseclibFunctionalTestCase
|
class SSH2Test extends PhpseclibFunctionalTestCase
|
||||||
{
|
{
|
||||||
|
public function getSSH2(): SSH2
|
||||||
|
{
|
||||||
|
return new SSH2($this->getEnv('SSH_HOSTNAME'), 22);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSSH2Login(): SSH2
|
||||||
|
{
|
||||||
|
$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(): SSH2
|
public function testConstructor(): SSH2
|
||||||
{
|
{
|
||||||
$ssh = new SSH2($this->getEnv('SSH_HOSTNAME'));
|
$ssh = $this->getSSH2();
|
||||||
|
|
||||||
$this->assertIsObject(
|
$this->assertIsObject(
|
||||||
$ssh,
|
$ssh,
|
||||||
@ -44,6 +63,17 @@ class SSH2Test extends PhpseclibFunctionalTestCase
|
|||||||
'Failed asserting that SSH2 is not authenticated after construction.'
|
'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(
|
$this->assertNotEmpty(
|
||||||
$ssh->getServerPublicHostKey(),
|
$ssh->getServerPublicHostKey(),
|
||||||
'Failed asserting that a non-empty public host key was fetched.'
|
'Failed asserting that a non-empty public host key was fetched.'
|
||||||
@ -84,6 +114,11 @@ class SSH2Test extends PhpseclibFunctionalTestCase
|
|||||||
'Failed asserting that SSH2 is not authenticated after bad login attempt.'
|
'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;
|
return $ssh;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,6 +139,11 @@ class SSH2Test extends PhpseclibFunctionalTestCase
|
|||||||
'Failed asserting that SSH2 is authenticated after good login attempt.'
|
'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;
|
return $ssh;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -123,12 +163,28 @@ class SSH2Test extends PhpseclibFunctionalTestCase
|
|||||||
->will($this->returnValue(true));
|
->will($this->returnValue(true));
|
||||||
$ssh->exec('pwd', [$callbackObject, 'callbackMethod']);
|
$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;
|
return $ssh;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testGetServerPublicHostKey(): void
|
public function testGetServerPublicHostKey(): void
|
||||||
{
|
{
|
||||||
$ssh = new SSH2($this->getEnv('SSH_HOSTNAME'));
|
$ssh = $this->getSSH2();
|
||||||
|
|
||||||
$this->assertIsString($ssh->getServerPublicHostKey());
|
$this->assertIsString($ssh->getServerPublicHostKey());
|
||||||
}
|
}
|
||||||
@ -153,11 +209,65 @@ class SSH2Test extends PhpseclibFunctionalTestCase
|
|||||||
public function testDisablePTY(SSH2 $ssh): SSH2
|
public function testDisablePTY(SSH2 $ssh): SSH2
|
||||||
{
|
{
|
||||||
$ssh->enablePTY();
|
$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');
|
$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();
|
$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');
|
$ssh->exec('pwd');
|
||||||
|
|
||||||
$this->assertTrue(true);
|
$this->assertFalse(
|
||||||
|
$ssh->isPTYOpen(),
|
||||||
|
'Failed asserting that pty was not open after exec.'
|
||||||
|
);
|
||||||
|
|
||||||
return $ssh;
|
return $ssh;
|
||||||
}
|
}
|
||||||
@ -184,13 +294,248 @@ class SSH2Test extends PhpseclibFunctionalTestCase
|
|||||||
|
|
||||||
$ssh->write("ping 127.0.0.1\n");
|
$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();
|
$ssh->enablePTY();
|
||||||
$ssh->exec('bash');
|
|
||||||
|
$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->write("ls -latr\n");
|
||||||
|
|
||||||
$ssh->setTimeout(1);
|
$ssh->setTimeout(1);
|
||||||
|
|
||||||
$this->assertIsString($ssh->read());
|
$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(): SSH2
|
||||||
|
{
|
||||||
|
$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): void
|
||||||
|
{
|
||||||
|
$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(): void
|
||||||
|
{
|
||||||
|
$this->expectException(\RuntimeException::class);
|
||||||
|
$this->expectExceptionMessage('If you want to run multiple exec()\'s you will need to disable (and re-enable if appropriate) a PTY for each one.');
|
||||||
|
|
||||||
|
$ssh = $this->getSSH2Login();
|
||||||
|
|
||||||
|
$ssh->enablePTY();
|
||||||
|
|
||||||
|
$ssh->exec('bash');
|
||||||
|
|
||||||
|
$ssh->exec('bash');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testMultipleInteractiveChannels(): void
|
||||||
|
{
|
||||||
|
$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.'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace phpseclib3\Tests\Unit\Net;
|
namespace phpseclib3\Tests\Unit\Net;
|
||||||
|
|
||||||
|
use phpseclib3\Exception\InsufficientSetupException;
|
||||||
use phpseclib3\Net\SSH2;
|
use phpseclib3\Net\SSH2;
|
||||||
use phpseclib3\Tests\PhpseclibTestCase;
|
use phpseclib3\Tests\PhpseclibTestCase;
|
||||||
|
|
||||||
@ -143,6 +144,71 @@ class SSH2UnitTest extends PhpseclibTestCase
|
|||||||
$this->assertSame('{' . spl_object_hash($ssh) . '}', $ssh->getResourceId());
|
$this->assertSame('{' . spl_object_hash($ssh) . '}', $ssh->getResourceId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @requires PHPUnit < 10
|
||||||
|
*/
|
||||||
|
public function testReadUnauthenticated(): void
|
||||||
|
{
|
||||||
|
$this->expectException(InsufficientSetupException::class);
|
||||||
|
$this->expectExceptionMessage('Operation disallowed prior to login()');
|
||||||
|
|
||||||
|
$ssh = $this->createSSHMock();
|
||||||
|
|
||||||
|
$ssh->read();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @requires PHPUnit < 10
|
||||||
|
*/
|
||||||
|
public function testWriteUnauthenticated(): void
|
||||||
|
{
|
||||||
|
$this->expectException(InsufficientSetupException::class);
|
||||||
|
$this->expectExceptionMessage('Operation disallowed prior to login()');
|
||||||
|
|
||||||
|
$ssh = $this->createSSHMock();
|
||||||
|
|
||||||
|
$ssh->write('');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @requires PHPUnit < 10
|
||||||
|
*/
|
||||||
|
public function testWriteOpensShell(): void
|
||||||
|
{
|
||||||
|
$ssh = $this->getMockBuilder(SSH2::class)
|
||||||
|
->disableOriginalConstructor()
|
||||||
|
->setMethods(['__destruct', 'isAuthenticated', 'openShell', 'send_channel_packet'])
|
||||||
|
->getMock();
|
||||||
|
$ssh->expects($this->once())
|
||||||
|
->method('isAuthenticated')
|
||||||
|
->willReturn(true);
|
||||||
|
$ssh->expects($this->once())
|
||||||
|
->method('openShell')
|
||||||
|
->willReturn(true);
|
||||||
|
$ssh->expects($this->once())
|
||||||
|
->method('send_channel_packet')
|
||||||
|
->with(SSH2::CHANNEL_SHELL, 'hello');
|
||||||
|
|
||||||
|
$ssh->write('hello');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @requires PHPUnit < 10
|
||||||
|
*/
|
||||||
|
public function testOpenShellWhenOpen(): void
|
||||||
|
{
|
||||||
|
$ssh = $this->getMockBuilder(SSH2::class)
|
||||||
|
->disableOriginalConstructor()
|
||||||
|
->setMethods(['__destruct', 'isShellOpen'])
|
||||||
|
->getMock();
|
||||||
|
|
||||||
|
$ssh->expects($this->once())
|
||||||
|
->method('isShellOpen')
|
||||||
|
->willReturn(true);
|
||||||
|
|
||||||
|
$this->assertFalse($ssh->openShell());
|
||||||
|
}
|
||||||
|
|
||||||
public function testGetTimeout(): void
|
public function testGetTimeout(): void
|
||||||
{
|
{
|
||||||
$ssh = new SSH2('localhost');
|
$ssh = new SSH2('localhost');
|
||||||
|
Loading…
Reference in New Issue
Block a user