Running Commands
One-off commands
echo $ssh->exec('pwd');
echo $ssh->exec('ls -la');
By default $ssh->exec()
returns both stdout and stderr. To suppress stderr you can call $ssh->enableQuietMode()
. To re-enable it call $ssh->disableQuietMode()
.
To get stderr separately from stdout you'll need to call $ssh->enableQuietMode()
and then call $ssh->getStdError()
. These functions do not work with $ssh->read()
(ie. when a PTY is enabled) for reasons described here.
To get the exit status you can call $ssh->getExitStatus()
.
To see whether or not "quiet mode" is enabled do $ssh->isQuietModeEnabled()
.
Callbacks
$ssh->exec('ping 127.0.0.1', function($str) {
echo $str;
//if (strpos($str, 'icmp_seq=5') !== false) {
// return true;
//}
});
This example is best run via the CLI. Run it via the webbrowser and you may need to flush the output buffer and even then YMMV.
The commented out code shows how you could use a callback method to return early based on whatever your criteria might be. ie. if the callback method returns bool(true)
then exec()
will return early.
Gotcha: Successive calls to exec()
echo $ssh->exec('pwd'); // outputs /home/username
$ssh->exec('cd /');
echo $ssh->exec('pwd'); // (despite the previous command) outputs /home/username
If done on an interactive shell, the output you'd receive for the first pwd would (depending on how your system is setup) be different than the output of the second pwd. The above code snippet, however, will yield two identical lines.
The reason for this is that any "state changes" you make to the one-time shell are gone once the exec() has been ran and the channel has been deleted.
You can workaround this on Linux by doing $ssh->exec('cd /; pwd')
or $ssh->exec('cd / && pwd')
.
Interactive Shell
echo $ssh->read('username@username:~$');
$ssh->write("ls -la\n"); // note the "\n"
echo $ssh->read('username@username:~$');
On a terminal you normally don't just type the command and expect to get the output. You type the command and then hit enter. To simulate that you'll need to add "\n"
to the commands you send via write()
Gotcha: Writing without first reading
You should always $ssh->read()
the prompt before $ssh->write()
'ing the command. Consider the following:
$ssh = new SSH2('localhost');
$ssh->login('user', 'pass');
$ssh->write("ping -c 5 127.0.0.1\n");
echo $ssh->read('username@username:~$');
In this example the initial $ssh->read('username@username:~$')
is being skipped. Superficially, this might seem reasonable enough, but in practice, it won't work.
Here's the output of the code, as written:
ping -c 5 127.0.0.1
Last login: Thu Feb 6 06:51:26 2020 from 10.0.2.2
username@username:~$
Here's the output if $ssh->read('username@username:~$');
had been done before:
ping -c 5 127.0.0.1
PING 127.0.0.1 (127.0.0.1) 56(84) bytes of data.
64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.015 ms
64 bytes from 127.0.0.1: icmp_seq=2 ttl=64 time=0.038 ms
64 bytes from 127.0.0.1: icmp_seq=3 ttl=64 time=0.074 ms
64 bytes from 127.0.0.1: icmp_seq=4 ttl=64 time=0.038 ms
64 bytes from 127.0.0.1: icmp_seq=5 ttl=64 time=0.044 ms
--- 127.0.0.1 ping statistics ---
5 packets transmitted, 5 received, 0% packet loss, time 4093ms
rtt min/avg/max/mdev = 0.015/0.041/0.074/0.020 ms
username@username:~$
Gotcha: echo'ing out stdin
Let's say you wanted to "stream" a bunch of bytes into a file via stdin. Superficially one might think this would work:
$ssh->exec('cat > filename.ext');
$ssh->write("asdfasdf\n");
For non-binary data it does but it does so with a few caveats.
- Any data you send to it via
$ssh->write()
will be echo'd out, so you'll also want to do$ssh->read('asdfasdf')
. This makes sense when you consider that there are times when Linux does not echo out the keys you type. eg. when you're in normal mode in vim. If you hit i or a you'll enter into insert mode without those characters being echo'd out. Whether or not the keystrokes you enter are echo'd out to you depends on the context. - This doesn't work well with binary data. The PuTTYPuTTYPuTTY question in the PuTTY FAQ addresses this.
read() with regular expressions: sudo
Technically, sudo is as easy as doing echo 'password' | sudo -S command
but sudo also provides a good example of how to use regular expressions with $ssh->read()
:
echo $ssh->read('username@username:~$');
$ssh->write("sudo ls -la\n");
$output = $ssh->read('#[pP]assword[^:]*:|username@username:~\$#', SSH2::READ_REGEX);
echo $output;
if (preg_match('#[pP]assword[^:]*:#', $output)) {
$ssh->write("password\n");
echo $ssh->read('username@username:~$');
}
By default, sudo caches passwords for 5 minutes after they've been entered. So while $ssh->read('Password:')
will work the first time you try it, it won't work if you try it within a five minutes after having initially ran it.
setTimeout()
By default, phpseclib should timeout if the command you're running takes more than 10s. You can change this behavior thusly:
$ssh->setTimeout(1);
echo $ssh->read();
After each $ssh->read()
the timeout resets to what you set it to last. By default the "channel" remains open so if you do $ssh->write("ping 127.0.0.1\n")
and do two successive $ssh->read()
you'll get ping output in both of those calls. If you don't want it to do that you can either send Ctrl + C via $ssh->write("\x03")
or you can reset the whole channel by calling $ssh->reset()
.
$ssh->isTimeout()
will return true if the result of the last $ssh->read()
or $ssh->exec()
was due to a timeout. Otherwise it will return false.
$ssh->setTimeout()
works with $ssh->read()
and $ssh->exec()
. If an $ssh->exec()
call times out and you want to run another $ssh->exec()
call after the timed out one, do $ssh->reset()
.
setKeepAlive()
In some cases it may be necessary to send a "keepalive" to the server every x seconds to prevent the SSH connection from being closed by the server during long running commands. eg. if, in the target servers sshd_config, ClientAliveCountMax is 0 and ClientAliveInterval is 3, then echo $ssh->exec('sleep 10; ls -latr')
will timeout and you'll get a "Connection closed prematurely" ConnectionClosedException. setKeepAlive(...)
is the answer to this problem.
Note that setKeepAlive()
will not keep a connection alive if you're doing a time consuming operation outside of the SSH instance. eg. doing sleep(10); echo $ssh->exec('ls -latr');
on a server with the aforementioned sshd_config will still result in a "Connection closed prematurely" ConnectionClosedException because, at the end of the day, PHP is still a synchronous language and unless phpseclib has control then the "keepalive" packets won't be sent out.
Determining what to read(): passwd
Let's say you want to change your password. Technically, SSH has built-in OS-independent functionality to accomodate this but phpseclib doesn't (currently) support it so we'll just use passwd. So how would that work? Normally you're presented with a series of prompts but what are those prompts? Let's find out.
$ssh->enablePTY();
$ssh->setTimeout(3);
$ssh->exec('passwd');
echo $ssh->read();
This outputs (current) UNIX password
so we'll wait for password:
to appear. But what comes after that?
$ssh->enablePTY();
$ssh->setTimeout(3);
$ssh->exec('passwd');
$ssh->setTimeout(3);
$ssh->read('password:');
$ssh->write("oldpassword\n");
echo $ssh->read();
This outputs Enter new UNIX password:
so we'll, once again, wait for password:
to appear. But what about after that?
$ssh->enablePTY();
$ssh->setTimeout(3);
$ssh->exec('passwd');
$ssh->setTimeout(3);
$ssh->read('password:');
$ssh->write("oldpassword\n");
$ssh->read('password:');
$ssh->write("newpassword\n");
echo $ssh->read();
This outputs Retype new UNIX password:
so we'll, once again, wait for password:
to appear. But what about after that?
$ssh->enablePTY();
$ssh->setTimeout(3);
$ssh->exec('passwd');
$ssh->setTimeout(3);
$ssh->read('password:');
$ssh->write("oldpassword\n");
$ssh->read('password:');
$ssh->write("newpassword\n");
$ssh->read('password:');
$ssh->write("newpassword\n");
echo $ssh->read();
This outputs passwd: password updated successfully
so I guess we're done! At this point $ssh->setTimeout(3)
is unneccessary. The last $ssh->read()
, in my testing, returns immediately.
ANSI Escape Codes
use phpseclib3\File\ANSI;
$ansi = new ANSI;
$ansi->appendString($ssh->read('username@username:~$'));
$ssh->write("top\n");
$ssh->setTimeout(5);
$ansi->appendString($ssh->read());
echo $ansi->getScreen(); // outputs HTML
Some commands issued to a terminal may yield ANSI escape codes. eg. ^[[H
. These provide the terminal with information on the formating of the characters and their positioning.
Since \phpseclib3\Net\SSH2 uses vt100 as the "TERM environment variable value" a VT100 terminal emulator is needed to properly handle the ANSI escape codes. \phpseclib3\File\ANSI aims to be such an emulator. The default screen size is 80x24.
$ansi->getScreen()
returns what'd be seen on the current screen. In the case of top this is desirable as it'll produce output like this:
top - 23:39:24 up 77 days, 1:13, 1 user, load average: 0.00, 0.00, 0.00
Tasks: 45 total, 2 running, 43 sleeping, 0 stopped, 0 zombie
Cpu(s): 0.0%us, 0.0%sy, 0.0%ni,100.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Mem: 1740956k total, 1079288k used, 661668k free, 221240k buffers
Swap: 0k total, 0k used, 0k free, 399940k cached
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
1 root 16 0 2128 696 600 S 0.0 0.0 0:01.10 init
2 root RT 0 0 0 0 S 0.0 0.0 0:00.00 migration/0
3 root 34 19 0 0 0 S 0.0 0.0 0:00.05 ksoftirqd/0
4 root RT 0 0 0 0 S 0.0 0.0 0:00.01 watchdog/0
5 root 10 -5 0 0 0 S 0.0 0.0 0:00.25 events/0
6 root 11 -5 0 0 0 S 0.0 0.0 0:00.63 khelper
7 root 10 -5 0 0 0 S 0.0 0.0 0:00.00 kthread
8 root 14 -5 0 0 0 S 0.0 0.0 0:00.00 xenwatch
9 root 10 -5 0 0 0 S 0.0 0.0 0:00.00 xenbus
17 root 10 -5 0 0 0 S 0.0 0.0 0:00.00 kblockd/0
46 root 20 -5 0 0 0 S 0.0 0.0 0:00.00 aio/0
45 root 15 0 0 0 0 S 0.0 0.0 0:00.64 kswapd0
562 root 20 -5 0 0 0 S 0.0 0.0 0:00.00 kseriod
657 root 15 0 0 0 0 S 0.0 0.0 0:04.23 kjournald
718 root 13 -4 2360 656 424 S 0.0 0.0 0:00.18 udevd
1592 root 14 -2 2396 848 560 S 0.0 0.0 0:00.03 dhclient
1647 root 15 0 0 0 0 S 0.0 0.0 0:00.16 kjournald
In the case of ls, however, it is less desirable. For commands like ls it may be preferable to do $ansi->getHistory()
. For top, that'd return the following:
__| __|_ ) Fedora 8
_| ( / 32-bit
___|\___|___|
Welcome to an EC2 Public Image
:-)
Base
--[ see /etc/ec2/release-notes ]--
[username@username:~$]$top
top - 23:51:56 up 77 days, 1:25, 1 user, load average: 0.00, 0.00, 0.00
Tasks: 45 total, 2 running, 43 sleeping, 0 stopped, 0 zombie
Cpu(s): 0.0%us, 0.0%sy, 0.0%ni,100.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Mem: 1740956k total, 1079256k used, 661700k free, 221240k buffers
Swap: 0k total, 0k used, 0k free, 399940k cached
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
1 root 16 0 2128 696 600 S 0.0 0.0 0:01.10 init
2 root RT 0 0 0 0 S 0.0 0.0 0:00.00 migration/0
3 root 34 19 0 0 0 S 0.0 0.0 0:00.05 ksoftirqd/0
4 root RT 0 0 0 0 S 0.0 0.0 0:00.01 watchdog/0
5 root 10 -5 0 0 0 S 0.0 0.0 0:00.25 events/0
6 root 11 -5 0 0 0 S 0.0 0.0 0:00.63 khelper
7 root 10 -5 0 0 0 S 0.0 0.0 0:00.00 kthread
8 root 14 -5 0 0 0 S 0.0 0.0 0:00.00 xenwatch
9 root 10 -5 0 0 0 S 0.0 0.0 0:00.00 xenbus
17 root 10 -5 0 0 0 S 0.0 0.0 0:00.00 kblockd/0
46 root 20 -5 0 0 0 S 0.0 0.0 0:00.00 aio/0
45 root 15 0 0 0 0 S 0.0 0.0 0:00.64 kswapd0
562 root 20 -5 0 0 0 S 0.0 0.0 0:00.00 kseriod
657 root 15 0 0 0 0 S 0.0 0.0 0:04.23 kjournald
718 root 13 -4 2360 656 424 S 0.0 0.0 0:00.18 udevd
1592 root 14 -2 2396 848 560 S 0.0 0.0 0:00.03 dhclient
1647 root 15 0 0 0 0 S 0.0 0.0 0:00.16 kjournald
The history, by default, stores 200 lines (not including the current screen). This can be changed by doing $ansi->setHistory(100)
or whatever.
Both functions return HTML with the various formatting properties specified by HTML. If you don't want HTML and want just the raw text of the terminal without any formatting do htmlspecialchars_decode(strip_tags($ansi->getScreen()))
.
Sending Special Characters
use phpseclib3\File\ANSI;
$ssh->setTimeout(2);
$ssh->read();
$ssh->write("vim\n");
$ssh->read();
$ssh->write("\x1BOP"); // send F1
$ansi = new ANSI;
$ansi->appendString($ssh->read());
A table of special characters and the keys they correspond to can be found at SSH2 Special Characters. The output of the above program is as follows:
*help.txt* For Vim version 7.3. Last change: 2010 Jul 20 VIM - main help file k Move around: Use the cursor keys, or "h" to go left, h l "j" to go down, "k" to go up, "l" to go right. j Close this window: Use ":q<Enter>". Get out of Vim: Use ":qa!<Enter>" (careful, all changes are lost!). Jump to a subject: Position the cursor on a tag (e.g. |bars|) and hit CTRL-]. With the mouse: ":set mouse=a" to enable the mouse (in xterm or GUI). Double-click the left mouse button on a tag, e.g. |bars|. Jump back: Type CTRL-T or CTRL-O (repeat to go further back). Get specific help: It is possible to go directly to whatever you want help on, by giving an argument to the |:help| command. It is possible to further specify the context: *help-context* WHAT PREPEND EXAMPLE ~ Normal mode command (nothing) :help x help.txt [Help][RO] 1,1 Top [No Name] 0,0-1 All "help.txt" [readonly] 221L, 8239C
The output may look different than vim when ran through, for example, PuTTY, because PuTTY uses xterm
as it's shell whereas phpseclib uses vt100
.
exec() with and without a PTY vs Interactive Shells
passwd
Some commands require a PTY to work correctly with exec()
. Examples follow:
exec():
echo $ssh->exec('passwd');
Stalls. If you use a callback function or setTimeout() you'll see that it's outputting (current) UNIX password:
and waiting for input that can't ever come.
exec() with PTY:
$ssh->enablePTY();
$ssh->exec('passwd');
echo $ssh->read('password:');
$ssh->write("badpw\n");
echo $ssh->read('password unchanged');
Runs as one might expect. There may be a three second delay before the "password unchanged" dialog that's implemented by Linux to protect against brute force attacks (per "man pam_unix").
read() / write():
echo $ssh->read('username@username:~$');
$ssh->write("passwd\n");
echo $ssh->read('password:');
$ssh->write("badpw\n");
echo $ssh->read('password unchanged');
Pretty much the same output as exec() with PTY.
top
exec():
echo $ssh->exec('top');
Outputs TERM environment variable not set
.
exec() with PTY:
$ssh->enablePTY();
$ssh->exec('top');
$ssh->setTimeout(5);
$ansi->appendString($ssh->read());
echo $ansi->getScreen();
See ANSI Escape Codes for the output.
read() / write():
$ansi->appendString($ssh->read('username@username:~$'));
$ssh->write("top\n");
$ssh->setTimeout(5);
$ansi->appendString($ssh->read());
echo $ansi->getScreen();
Pretty much the same output as exec() with PTY.
Window Size
The default window size is 80x24 (80 columns, 24 rows).
The number of rows can be adjusted by calling $ssh->setWindowRows()
. The number of columns can be adjusted by calling $ssh->setWindowColumns()
. Both can be adjusted at the same time by calling $ssh->setWindowSize(80, 24)
.
The number of rows and columns can be determined by calling $ssh->getWindowRows()
and $ssh->getWindowColumns()
, respectively.
Multiple Channels
Let's say you ran a command with $ssh->exec()
with a PTY open but that you also wanted to, simultaniously, run a command on an interactive shell. Nothing presented in the documentation, up to this point, really enables that, however, as of phpseclib v3.0.20, an additional parameter - $channel
- has been added to read()
, write()
and reset()
, so that now you can do this:
$ssh->enablePTY();
$ssh->exec('sudo ls -latr');
$ssh->write("sudo ls -latr\n", SSH2::CHANNEL_SHELL);
$ssh->read('#[pP]assword[^:]*:|username@username:~\$#', SSH2::READ_REGEX, SSH2::CHANNEL_EXEC);
You can also, as of phpseclib v3.0.20, see the status of a channel by calling any of the following:
isShellOpen()
isPTYOpen()
isInteractiveChannelOpen($channel)
Note that phpseclib does not currently let you have multiple shells or exec's with PTY or subsystems open.