mirror of
https://github.com/danog/phpseclib.git
synced 2024-12-12 09:09:39 +01:00
Merge pull request #1897 from rposky/3.0
SSH2: Better support for multiple interactive channels & expose shell functions: 3.0 Backport
This commit is contained in:
commit
0f8bc61538
@ -646,6 +646,14 @@ class SSH2
|
|||||||
*/
|
*/
|
||||||
protected $channel_status = [];
|
protected $channel_status = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The identifier of the interactive channel which was opened most recently
|
||||||
|
*
|
||||||
|
* @see self::getInteractiveChannelId()
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
private $channel_id_last_interactive = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Packet Size
|
* Packet Size
|
||||||
*
|
*
|
||||||
@ -837,20 +845,6 @@ class SSH2
|
|||||||
*/
|
*/
|
||||||
private $request_pty = false;
|
private $request_pty = false;
|
||||||
|
|
||||||
/**
|
|
||||||
* Flag set while exec() is running when using enablePTY()
|
|
||||||
*
|
|
||||||
* @var bool
|
|
||||||
*/
|
|
||||||
private $in_request_pty_exec = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Flag set after startSubsystem() is called
|
|
||||||
*
|
|
||||||
* @var bool
|
|
||||||
*/
|
|
||||||
private $in_subsystem;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Contents of stdError
|
* Contents of stdError
|
||||||
*
|
*
|
||||||
@ -2729,7 +2723,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.');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2779,8 +2773,6 @@ class SSH2
|
|||||||
$this->disconnect_helper(NET_SSH2_DISCONNECT_BY_APPLICATION);
|
$this->disconnect_helper(NET_SSH2_DISCONNECT_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
|
||||||
@ -2810,7 +2802,8 @@ class SSH2
|
|||||||
|
|
||||||
$this->channel_status[self::CHANNEL_EXEC] = NET_SSH2_MSG_CHANNEL_DATA;
|
$this->channel_status[self::CHANNEL_EXEC] = NET_SSH2_MSG_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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2838,16 +2831,25 @@ 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.
|
||||||
|
*
|
||||||
|
* @see self::isShellOpen()
|
||||||
* @see self::read()
|
* @see self::read()
|
||||||
* @see self::write()
|
* @see self::write()
|
||||||
* @return bool
|
* @return bool
|
||||||
|
* @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
|
||||||
*/
|
*/
|
||||||
private function initShell()
|
public function openShell()
|
||||||
{
|
{
|
||||||
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;
|
||||||
@ -2907,14 +2909,18 @@ class SSH2
|
|||||||
|
|
||||||
$this->channel_status[self::CHANNEL_SHELL] = NET_SSH2_MSG_CHANNEL_DATA;
|
$this->channel_status[self::CHANNEL_SHELL] = NET_SSH2_MSG_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()
|
||||||
* @return int
|
* @return int
|
||||||
@ -2922,15 +2928,26 @@ class SSH2
|
|||||||
private function get_interactive_channel()
|
private function get_interactive_channel()
|
||||||
{
|
{
|
||||||
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
|
||||||
|
*
|
||||||
|
* @param int $channel The channel number to evaluate
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
private function is_channel_status_data($channel)
|
||||||
|
{
|
||||||
|
return isset($this->channel_status[$channel]) && $this->channel_status[$channel] == NET_SSH2_MSG_CHANNEL_DATA;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return an available open channel
|
* Return an available open channel
|
||||||
*
|
*
|
||||||
@ -2987,26 +3004,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.
|
||||||
|
*
|
||||||
* @see self::write()
|
* @see self::write()
|
||||||
* @param string $expect
|
* @param string $expect
|
||||||
* @param int $mode
|
* @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
|
||||||
*/
|
*/
|
||||||
public function read($expect = '', $mode = self::READ_SIMPLE)
|
public function read($expect = '', $mode = self::READ_SIMPLE, $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)) {
|
||||||
|
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');
|
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);
|
||||||
@ -3024,7 +3052,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));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3035,22 +3062,35 @@ 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.
|
||||||
|
*
|
||||||
* @see SSH2::read()
|
* @see SSH2::read()
|
||||||
* @param string $cmd
|
* @param string $cmd
|
||||||
|
* @param int|null $channel Channel id returned by self::getInteractiveChannelId()
|
||||||
* @return void
|
* @return void
|
||||||
* @throws \RuntimeException on connection error
|
* @throws \RuntimeException on connection error
|
||||||
|
* @throws InsufficientSetupException on unexpected channel status, possibly due to closure
|
||||||
*/
|
*/
|
||||||
public function write($cmd)
|
public function write($cmd, $channel = null)
|
||||||
{
|
{
|
||||||
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)) {
|
||||||
|
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');
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -3103,8 +3143,7 @@ class SSH2
|
|||||||
|
|
||||||
$this->channel_status[self::CHANNEL_SUBSYSTEM] = NET_SSH2_MSG_CHANNEL_DATA;
|
$this->channel_status[self::CHANNEL_SUBSYSTEM] = NET_SSH2_MSG_CHANNEL_DATA;
|
||||||
|
|
||||||
$this->bitmap |= self::MASK_SHELL;
|
$this->channel_id_last_interactive = self::CHANNEL_SUBSYSTEM;
|
||||||
$this->in_subsystem = true;
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -3117,8 +3156,9 @@ class SSH2
|
|||||||
*/
|
*/
|
||||||
public function stopSubsystem()
|
public function stopSubsystem()
|
||||||
{
|
{
|
||||||
$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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3127,10 +3167,23 @@ class SSH2
|
|||||||
*
|
*
|
||||||
* 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()
|
||||||
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function reset()
|
public function reset($channel = null)
|
||||||
{
|
{
|
||||||
$this->close_channel($this->get_interactive_channel());
|
if ($channel === null) {
|
||||||
|
$channel = $this->get_interactive_channel();
|
||||||
|
}
|
||||||
|
if ($this->isInteractiveChannelOpen($channel)) {
|
||||||
|
$this->close_channel($channel);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -3189,6 +3242,49 @@ class SSH2
|
|||||||
return (bool) ($this->bitmap & self::MASK_LOGIN);
|
return (bool) ($this->bitmap & self::MASK_LOGIN);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is the interactive shell active?
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isShellOpen()
|
||||||
|
{
|
||||||
|
return $this->isInteractiveChannelOpen(self::CHANNEL_SHELL);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is the exec pty active?
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isPTYOpen()
|
||||||
|
{
|
||||||
|
return $this->isInteractiveChannelOpen(self::CHANNEL_EXEC);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is the given interactive channel active?
|
||||||
|
*
|
||||||
|
* @param int $channel Channel id returned by self::getInteractiveChannelId()
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isInteractiveChannelOpen($channel)
|
||||||
|
{
|
||||||
|
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()
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function getInteractiveChannelId()
|
||||||
|
{
|
||||||
|
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
|
||||||
*
|
*
|
||||||
@ -3773,9 +3869,8 @@ class SSH2
|
|||||||
*/
|
*/
|
||||||
public function disablePTY()
|
public function disablePTY()
|
||||||
{
|
{
|
||||||
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;
|
||||||
}
|
}
|
||||||
@ -3801,6 +3896,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:
|
||||||
*
|
*
|
||||||
@ -3968,7 +4064,10 @@ class SSH2
|
|||||||
throw new \RuntimeException('Unable to fulfill channel request');
|
throw new \RuntimeException('Unable to fulfill channel request');
|
||||||
}
|
}
|
||||||
case NET_SSH2_MSG_CHANNEL_CLOSE:
|
case NET_SSH2_MSG_CHANNEL_CLOSE:
|
||||||
return $type == NET_SSH2_MSG_CHANNEL_CLOSE ? true : $this->get_channel_packet($client_channel, $skip_extended);
|
if ($client_channel == $channel && $type == NET_SSH2_MSG_CHANNEL_CLOSE) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return $this->get_channel_packet($client_channel, $skip_extended);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -4003,9 +4102,8 @@ class SSH2
|
|||||||
case NET_SSH2_MSG_CHANNEL_CLOSE:
|
case NET_SSH2_MSG_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] != NET_SSH2_MSG_CHANNEL_EOF) {
|
if ($this->channel_status[$channel] != NET_SSH2_MSG_CHANNEL_EOF) {
|
||||||
$this->send_binary_packet(pack('CN', NET_SSH2_MSG_CHANNEL_CLOSE, $this->server_channels[$channel]));
|
$this->send_binary_packet(pack('CN', NET_SSH2_MSG_CHANNEL_CLOSE, $this->server_channels[$channel]));
|
||||||
}
|
}
|
||||||
@ -4348,17 +4446,30 @@ 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', NET_SSH2_MSG_CHANNEL_CLOSE, $this->server_channels[$client_channel]));
|
$this->send_binary_packet(pack('CN', NET_SSH2_MSG_CHANNEL_CLOSE, $this->server_channels[$client_channel]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->close_channel_bitmap($client_channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maintains execution state bitmap in response to channel closure
|
||||||
|
*
|
||||||
|
* @param int $client_channel The channel number to maintain closure status of
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function close_channel_bitmap($client_channel)
|
||||||
|
{
|
||||||
|
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) {
|
if ($this->bitmap & self::MASK_SHELL) {
|
||||||
$this->bitmap &= ~self::MASK_SHELL;
|
$this->bitmap &= ~self::MASK_SHELL;
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -13,9 +13,34 @@ use phpseclib3\Tests\PhpseclibFunctionalTestCase;
|
|||||||
|
|
||||||
class SSH2Test extends 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()
|
public function testConstructor()
|
||||||
{
|
{
|
||||||
$ssh = new SSH2($this->getEnv('SSH_HOSTNAME'));
|
$ssh = $this->getSSH2();
|
||||||
|
|
||||||
$this->assertIsObject(
|
$this->assertIsObject(
|
||||||
$ssh,
|
$ssh,
|
||||||
@ -42,6 +67,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.'
|
||||||
@ -82,6 +118,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,6 +143,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -121,12 +167,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()
|
public function testGetServerPublicHostKey()
|
||||||
{
|
{
|
||||||
$ssh = new SSH2($this->getEnv('SSH_HOSTNAME'));
|
$ssh = $this->getSSH2();
|
||||||
|
|
||||||
$this->assertIsString($ssh->getServerPublicHostKey());
|
$this->assertIsString($ssh->getServerPublicHostKey());
|
||||||
}
|
}
|
||||||
@ -151,11 +213,65 @@ class SSH2Test extends PhpseclibFunctionalTestCase
|
|||||||
public function testDisablePTY(SSH2 $ssh)
|
public function testDisablePTY(SSH2 $ssh)
|
||||||
{
|
{
|
||||||
$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;
|
||||||
}
|
}
|
||||||
@ -182,13 +298,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()
|
||||||
|
{
|
||||||
|
$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('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()
|
||||||
|
{
|
||||||
|
$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.'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
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;
|
||||||
|
|
||||||
@ -146,6 +147,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()
|
||||||
|
{
|
||||||
|
$this->expectException(InsufficientSetupException::class);
|
||||||
|
$this->expectExceptionMessage('Operation disallowed prior to login()');
|
||||||
|
|
||||||
|
$ssh = $this->createSSHMock();
|
||||||
|
|
||||||
|
$ssh->read();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @requires PHPUnit < 10
|
||||||
|
*/
|
||||||
|
public function testWriteUnauthenticated()
|
||||||
|
{
|
||||||
|
$this->expectException(InsufficientSetupException::class);
|
||||||
|
$this->expectExceptionMessage('Operation disallowed prior to login()');
|
||||||
|
|
||||||
|
$ssh = $this->createSSHMock();
|
||||||
|
|
||||||
|
$ssh->write('');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @requires PHPUnit < 10
|
||||||
|
*/
|
||||||
|
public function testWriteOpensShell()
|
||||||
|
{
|
||||||
|
$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()
|
||||||
|
{
|
||||||
|
$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()
|
public function testGetTimeout()
|
||||||
{
|
{
|
||||||
$ssh = new SSH2('localhost');
|
$ssh = new SSH2('localhost');
|
||||||
|
Loading…
Reference in New Issue
Block a user