553,7 → 553,7 |
* @var array |
* @access private |
*/ |
private $message_numbers = []; |
private static $message_numbers = []; |
|
/** |
* Disconnection Message 'reason codes' defined in RFC4253 |
562,7 → 562,7 |
* @var array |
* @access private |
*/ |
private $disconnect_reasons = []; |
private static $disconnect_reasons = []; |
|
/** |
* SSH_MSG_CHANNEL_OPEN_FAILURE 'reason codes', defined in RFC4254 |
571,7 → 571,7 |
* @var array |
* @access private |
*/ |
private $channel_open_failure_reasons = []; |
private static $channel_open_failure_reasons = []; |
|
/** |
* Terminal Modes |
581,7 → 581,7 |
* @var array |
* @access private |
*/ |
private $terminal_modes = []; |
private static $terminal_modes = []; |
|
/** |
* SSH_MSG_CHANNEL_EXTENDED_DATA's data_type_codes |
591,7 → 591,7 |
* @var array |
* @access private |
*/ |
private $channel_extended_data_type_codes = []; |
private static $channel_extended_data_type_codes = []; |
|
/** |
* Send Sequence Number |
647,6 → 647,14 |
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 |
* |
* Maximum packet size indexed by channel |
838,20 → 846,6 |
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 |
* |
* @var string |
1105,7 → 1099,8 |
*/ |
public function __construct($host, $port = 22, $timeout = 10) |
{ |
$this->message_numbers = [ |
if (empty(self::$message_numbers)) { |
self::$message_numbers = [ |
1 => 'NET_SSH2_MSG_DISCONNECT', |
2 => 'NET_SSH2_MSG_IGNORE', |
3 => 'NET_SSH2_MSG_UNIMPLEMENTED', |
1136,7 → 1131,7 |
99 => 'NET_SSH2_MSG_CHANNEL_SUCCESS', |
100 => 'NET_SSH2_MSG_CHANNEL_FAILURE' |
]; |
$this->disconnect_reasons = [ |
self::$disconnect_reasons = [ |
1 => 'NET_SSH2_DISCONNECT_HOST_NOT_ALLOWED_TO_CONNECT', |
2 => 'NET_SSH2_DISCONNECT_PROTOCOL_ERROR', |
3 => 'NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED', |
1153,22 → 1148,22 |
14 => 'NET_SSH2_DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE', |
15 => 'NET_SSH2_DISCONNECT_ILLEGAL_USER_NAME' |
]; |
$this->channel_open_failure_reasons = [ |
self::$channel_open_failure_reasons = [ |
1 => 'NET_SSH2_OPEN_ADMINISTRATIVELY_PROHIBITED' |
]; |
$this->terminal_modes = [ |
self::$terminal_modes = [ |
0 => 'NET_SSH2_TTY_OP_END' |
]; |
$this->channel_extended_data_type_codes = [ |
self::$channel_extended_data_type_codes = [ |
1 => 'NET_SSH2_EXTENDED_DATA_STDERR' |
]; |
|
$this->define_array( |
$this->message_numbers, |
$this->disconnect_reasons, |
$this->channel_open_failure_reasons, |
$this->terminal_modes, |
$this->channel_extended_data_type_codes, |
self::define_array( |
self::$message_numbers, |
self::$disconnect_reasons, |
self::$channel_open_failure_reasons, |
self::$terminal_modes, |
self::$channel_extended_data_type_codes, |
[60 => 'NET_SSH2_MSG_USERAUTH_PASSWD_CHANGEREQ'], |
[60 => 'NET_SSH2_MSG_USERAUTH_PK_OK'], |
[60 => 'NET_SSH2_MSG_USERAUTH_INFO_REQUEST', |
1183,6 → 1178,7 |
[30 => 'NET_SSH2_MSG_KEX_ECDH_INIT', |
31 => 'NET_SSH2_MSG_KEX_ECDH_REPLY'] |
); |
} |
|
/** |
* Typehint is required due to a bug in Psalm: https://github.com/vimeo/psalm/issues/7508 |
2729,7 → 2725,7 |
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.'); |
} |
|
2779,8 → 2775,6 |
$this->disconnect_helper(NET_SSH2_DISCONNECT_BY_APPLICATION); |
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 |
2810,7 → 2804,8 |
|
$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; |
} |
|
2838,18 → 2833,27 |
/** |
* 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::write() |
* @return bool |
* @throws InsufficientSetupException if not authenticated |
* @throws \UnexpectedValueException on receipt of unexpected packets |
* @throws \RuntimeException on other errors |
*/ |
private function initShell() |
public function openShell() |
{ |
if ($this->in_request_pty_exec === true) { |
return true; |
if ($this->isShellOpen()) { |
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; |
$packet_size = 0x4000; |
|
2907,6 → 2911,8 |
|
$this->channel_status[self::CHANNEL_SHELL] = NET_SSH2_MSG_CHANNEL_DATA; |
|
$this->channel_id_last_interactive = self::CHANNEL_SHELL; |
|
$this->bitmap |= self::MASK_SHELL; |
|
return true; |
2913,8 → 2919,10 |
} |
|
/** |
* 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::write() |
* @return int |
2922,9 → 2930,9 |
private function get_interactive_channel() |
{ |
switch (true) { |
case $this->in_subsystem: |
case $this->is_channel_status_data(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; |
default: |
return self::CHANNEL_SHELL; |
2932,6 → 2940,17 |
} |
|
/** |
* 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 int |
2987,27 → 3006,38 |
* 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 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() |
* @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 |
* @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->is_timeout = false; |
|
if (!$this->isAuthenticated()) { |
throw new InsufficientSetupException('Operation disallowed prior to login()'); |
if ($channel === null) { |
$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'); |
} |
} |
|
$channel = $this->get_interactive_channel(); |
|
if ($mode == self::READ_NEXT) { |
return $this->get_channel_packet($channel); |
} |
3024,7 → 3054,6 |
} |
$response = $this->get_channel_packet($channel); |
if ($response === true) { |
$this->in_request_pty_exec = false; |
return Strings::shift($this->interactiveBuffer, strlen($this->interactiveBuffer)); |
} |
|
3035,22 → 3064,35 |
/** |
* 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() |
* @param string $cmd |
* @param int|null $channel Channel id returned by self::getInteractiveChannelId() |
* @return void |
* @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()) { |
throw new InsufficientSetupException('Operation disallowed prior to login()'); |
if ($channel === null) { |
$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'); |
} |
} |
|
$this->send_channel_packet($this->get_interactive_channel(), $cmd); |
$this->send_channel_packet($channel, $cmd); |
} |
|
/** |
3103,8 → 3145,7 |
|
$this->channel_status[self::CHANNEL_SUBSYSTEM] = NET_SSH2_MSG_CHANNEL_DATA; |
|
$this->bitmap |= self::MASK_SHELL; |
$this->in_subsystem = true; |
$this->channel_id_last_interactive = self::CHANNEL_SUBSYSTEM; |
|
return true; |
} |
3117,8 → 3158,9 |
*/ |
public function stopSubsystem() |
{ |
$this->in_subsystem = false; |
if ($this->isInteractiveChannelOpen(self::CHANNEL_SUBSYSTEM)) { |
$this->close_channel(self::CHANNEL_SUBSYSTEM); |
} |
return true; |
} |
|
3127,11 → 3169,24 |
* |
* 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); |
} |
} |
|
/** |
* Is timeout? |
3190,6 → 3245,49 |
} |
|
/** |
* 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 |
* |
* Inspired by http://php.net/manual/en/mysqli.ping.php |
3504,7 → 3602,7 |
|
if (defined('NET_SSH2_LOGGING')) { |
$current = microtime(true); |
$message_number = isset($this->message_numbers[ord($payload[0])]) ? $this->message_numbers[ord($payload[0])] : 'UNKNOWN (' . ord($payload[0]) . ')'; |
$message_number = isset(self::$message_numbers[ord($payload[0])]) ? self::$message_numbers[ord($payload[0])] : 'UNKNOWN (' . ord($payload[0]) . ')'; |
$message_number = '<- ' . $message_number . |
' (since last: ' . round($current - $this->last_packet, 4) . ', network: ' . round($stop - $start, 4) . 's)'; |
$this->append_log($message_number, $payload); |
3586,7 → 3684,7 |
case NET_SSH2_MSG_DISCONNECT: |
Strings::shift($payload, 1); |
list($reason_code, $message) = Strings::unpackSSH2('Ns', $payload); |
$this->errors[] = 'SSH_MSG_DISCONNECT: ' . $this->disconnect_reasons[$reason_code] . "\r\n$message"; |
$this->errors[] = 'SSH_MSG_DISCONNECT: ' . static::$disconnect_reasons[$reason_code] . "\r\n$message"; |
$this->bitmap = 0; |
return false; |
case NET_SSH2_MSG_IGNORE: |
3773,9 → 3871,8 |
*/ |
public function disablePTY() |
{ |
if ($this->in_request_pty_exec) { |
if ($this->isPTYOpen()) { |
$this->close_channel(self::CHANNEL_EXEC); |
$this->in_request_pty_exec = false; |
} |
$this->request_pty = false; |
} |
3801,6 → 3898,7 |
* - 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_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: |
* |
3968,9 → 4066,12 |
throw new \RuntimeException('Unable to fulfill channel request'); |
} |
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); |
} |
} |
|
// ie. $this->channel_status[$channel] == NET_SSH2_MSG_CHANNEL_DATA |
|
4003,9 → 4104,8 |
case NET_SSH2_MSG_CHANNEL_CLOSE: |
$this->curTimeout = 5; |
|
if ($this->bitmap & self::MASK_SHELL) { |
$this->bitmap &= ~self::MASK_SHELL; |
} |
$this->close_channel_bitmap($channel); |
|
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])); |
} |
4157,7 → 4257,7 |
|
if (defined('NET_SSH2_LOGGING')) { |
$current = microtime(true); |
$message_number = isset($this->message_numbers[ord($logged[0])]) ? $this->message_numbers[ord($logged[0])] : 'UNKNOWN (' . ord($logged[0]) . ')'; |
$message_number = isset(self::$message_numbers[ord($logged[0])]) ? self::$message_numbers[ord($logged[0])] : 'UNKNOWN (' . ord($logged[0]) . ')'; |
$message_number = '-> ' . $message_number . |
' (since last: ' . round($current - $this->last_packet, 4) . ', network: ' . round($stop - $start, 4) . 's)'; |
$this->append_log($message_number, $logged); |
4348,18 → 4448,31 |
while (!is_bool($this->get_channel_packet($client_channel))) { |
} |
|
if ($this->is_timeout) { |
$this->disconnect(); |
} |
|
if ($want_reply) { |
$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) { |
$this->bitmap &= ~self::MASK_SHELL; |
} |
break; |
} |
} |
|
/** |
* Disconnect |
4395,7 → 4508,7 |
* @param mixed[] ...$args |
* @access protected |
*/ |
protected function define_array(...$args) |
protected static function define_array(...$args) |
{ |
foreach ($args as $arg) { |
foreach ($arg as $key => $value) { |