Chapter 5. Networking

Table of Contents

5.1. Net_SSH
5.1.1. Dependencies
5.1.2. Net_SSH2 Examples
5.1.3. Host Key Verification
5.1.4. read() / write() vs. exec()
5.1.5. sudo with read() / write()
5.1.6. SSH-1's exec() vs. SSH-2's exec()
5.1.7. Successive calls to SSH-2's exec()
5.1.8. Debugging SSH-2
5.2. Net_SFTP
5.2.1. Introduction
5.2.2. Dependencies
5.2.3. Net_SFTP Example
5.2.4. put($remote_file, $data [, $mode])
5.2.5. get($remote_file [, $local_file])
5.2.6. pwd(), chdir(), mkdir() and rmdir()
5.2.7. chmod() and size()
5.2.8. nlist() and rawlist()
5.2.9. stat() and lstat()
5.2.10. delete() and rename()
5.2.11. Debugging SFTP

5.1. Net_SSH

The Net_SSH1 and Net_SSH2 libraries have, for the most part, an identical API. Some functions, however, do behave differently.

5.1.1. Dependencies

Net_SSH1/2 require, minimally, Math/BigInteger.php, Crypt/*.php, and PHP/Compat/Function/*.php. Net_SSH1 requires PHP 4.0.0. Net_SSH2 requires PHP 4.3.0 due to it's use of sha1().

5.1.2. Net_SSH2 Examples

<?php
include('Net/SSH2.php');

$ssh = new Net_SSH2('www.domain.tld');
if (!$ssh->login('username', 'password')) {
    exit('Login Failed');
}

echo $ssh->exec('pwd');
echo $ssh->exec('ls -la');
?>
<?php
include('Net/SSH2.php');

$key = new Crypt_RSA();
//$key->setPassword('whatever');
$key->loadKey(file_get_contents('privatekey'));

$ssh = new Net_SSH2('www.domain.tld');
if (!$ssh->login('username', $key)) {
    exit('Login Failed');
}

echo $ssh->read('username@username:~$');
$ssh->write("ls -la\n");
echo $ssh->read('username@username:~$');
?>

5.1.3. Host Key Verification

SSH protects itself against active eavesdroppers by providing a host key. The first time you connect the host key is supposed to be cached in some manner. On subsequent connections, the host key being used for this connection should be checked against the cached host key. If they match, it's the same server. If not, it's a different one.

In SSH-1, getHostKeyPublicModulus() and getHostKeyPublicExponent() will provide you with the host key. In SSH-2, getServerPublicHostKey() gets you the key.

The Net_SSH1 and Net_SSH2 examples omit the key verification stage for brevity. Also, depending on the context in which this library is used, it may even be unnecessary. For example, if you're connecting to www.example.com:22 from www.example.com:80, eavesdroppers are not something you need to worry about.

5.1.4. read() / write() vs. exec()

read() works a lot like expect in that it'll read from a stream until the pattern, as specified by the first parameter, is found. If the second parameter is set to NET_SSH2_READ_REGEX then the first parameter will be treated as a regular expression.

write() sends input - such as a command - to the server. Normally, operating systems don't process commands until the Enter key is hit, so to mimic this, you'll need to send a "\n", a "\r\n" or a "\r" depending on the operating system the server you're connecting to is using. Note that although the input will most likely be a command it doesn't have to be. What you send will be parsed by whatever is expecting user-entered text. So if you type in "passwd\n" the input - until "\n" is entered - will be parsed by passwd as a new or existing password - not as a command to be ran by the shell.

exec() sends a command to the shell to be ran. After the command has run it's course you'll get it's output and if the prompt - or whatever it is that you might alternatively read up to with read() - occurs anywhere in the output this is not a problem.

For the most part, exec() is probably the function you want to be calling. read() includes ANSI escape codes, the prompt, the command you're trying to run, the servers banner message, etc, whereas exec() returns the output of the command and that's it. Further, read() will never 100% work, whereas exec() will. Like say you're doing $ssh->write("sudo cat filename.ext") and that filename.txt includes the prompt within it. At that point, read() will stop in the middle of filename.txt and won't continue on to the actual prompt.

5.1.5. sudo with read() / write()

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. The following code sample demonstrates how to get around this:

<?php
include('Net/SSH2.php');

$sftp = new Net_SSH2('www.domain.tld');
$sftp->login('username', 'password');

echo $sftp->read('username@username:~$');
$sftp->write("sudo ls -la\n");
$output = $sftp->read('#Password:|username@username:~\$#', NET_SSH2_READ_REGEX);
echo $output;
if (preg_match('#Password:#', $lines)) {
    $ssh->write("password\n");
    echo $sftp->read('username@username:~$');
}
>

5.1.6. SSH-1's exec() vs. SSH-2's exec()

exec() works by creating a channel, issuing a command, and then subsequently destroying that channel. Since SSH-1 only allows one channel, exec() can only be called once. SSH-2, in contrast, allows an unlimited number of channels, and as such, you can perform as many exec()'s as you see fit.

5.1.7. Successive calls to SSH-2's exec()

Successive calls to SSH-2's exec() may not work as expected. Consider the following:

<?php
include('Net/SSH2.php');

$ssh = new Net_SSH2('www.domain.tld');
if (!$ssh->login('username', 'password')) {
    exit('Login Failed');
}

echo $ssh->exec('pwd');
echo $ssh->exec('cd /');
echo $ssh->exec('pwd');
?>

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. As such, if you want to support cd in your program, it'd be best to just handle that internally and rewrite all commands, before they're passed to exec() such that the relative paths are expanded to the absolute paths. Alternatively, one could always run a shell script, however, that may not always be an option.

5.1.8. Debugging SSH-2

To log output, the NET_SSH2_LOGGING constant will need to be defined. If you want full logs, you'll need to do define('NET_SSH2_LOGGING', NET_SSH2_LOG_COMPLEX). $ssh->getLog() will then return a string containing the unencrypted packets in hex and ASCII. If you want to just record the packet types that are being sent to and fro, you'll need to do define('NET_SSH2_LOGGING', NET_SSH2_LOG_SIMPLE). $ssh->getLog() will then return an array. Both log types include the amount of time it took to send the packet in question. The former is useful for general diagnostics and the latter is more useful for profiling. An example follows:

<?php
include('Net/SSH2.php');
define('NET_SSH2_LOGGING', NET_SSH2_LOG_COMPLEX);

$ssh = new Net_SSH2('www.domain.tld');
if (!$ssh->login('username', 'password')) {
    exit('Login Failed');
}

echo $ssh->exec('pwd');
echo $ssh->getLog();
?>

Depending on the problem, it may be more effective to just look at the output of $ssh->getLastError() (which returns a string) and $ssh->getErrors() (which returns an array) than to sift through the logs.

5.2. Net_SFTP

5.2.1. Introduction

Net_SFTP currently only supports SFTPv3, which, according to wikipedia.org, "is the most widely used version, implemented by the popular OpenSSH SFTP server".

5.2.2. Dependencies

Net_SFTP requires, minimally, PHP 4.3.0 and Net/SSH2.php, Math/BigInteger.php, Crypt/*.php, and PHP/Compat/Function/*.php.

5.2.3. Net_SFTP Example

<?php
include('Net/SFTP.php');

$sftp = new Net_SFTP('www.domain.tld');
if (!$sftp->login('username', 'password')) {
    exit('Login Failed');
}

echo $sftp->pwd() . "\r\n";
$sftp->put('filename.ext', 'hello, world!');
print_r($sftp->nlist());
?>

5.2.4. put($remote_file, $data [, $mode])

By default, put() does not read from the local filesystem. $data is dumped directly into $remote_file. So, for example, if you set $data to 'filename.ext' and then do get(), you will get a file, twelve bytes long, containing 'filename.ext' as its contents.

Setting $mode to NET_SFTP_LOCAL_FILE will change the above behavior. With NET_SFTP_LOCAL_FILE, $remote_file will contain as many bytes as filename.ext does on your local filesystem. If your filename.ext is 1MB then that is how large $remote_file will be, as well.

Currently, only binary mode is supported. As such, if the line endings need to be adjusted, you will need to take care of that, yourself.

5.2.5. get($remote_file [, $local_file])

Returns a string containing the contents of $remote_file if $local_file is left undefined or a boolean false if the operation was unsuccessful. If $local_file is defined, returns true or false depending on the success of the operation

5.2.6. pwd(), chdir(), mkdir() and rmdir()

pwd() returns the current directory, chdir() changes directories, mkdir() creates direcotires, and rmdir() removes directories. In the event of failure, they all return false. chdir(), mkdir(), and rmdir() return true on successful completion of the operation.

5.2.7. chmod() and size()

chmod() sets the permissions on a file and returns the new file permissions on success or false on error. Permissions are expected to be in octal so to set a file to 777 do $sftp->chmod(0777, $filename)

size() returns the size, in bytes, of an arbitrary file.

5.2.8. nlist() and rawlist()

nlist($dir = '.') returns the contents of the current directory as a numerically indexed array and rawlist() returns an associate array where the filenames are the array keys and the array values are, themselves, arrays containing the file attributes. The directory can be changed with the first parameter.

One of rawlist()'s parameters is 'type'. It's a field that may not be present on all servers but for those for those that it is it's value corresponds to one of several named constants: NET_SFTP_TYPE_REGULAR, NET_SFTP_TYPE_DIRECTORY, NET_SFTP_TYPE_SYMLINK or NET_SFTP_TYPE_SPECIAL.

5.2.9. stat() and lstat()

stat() returns information about a specific file whereas lstat() returns information about a file or symbolic link.

5.2.10. delete() and rename()

The purpose of both functions should be easy enough to glean - delete() deletes files or directories and rename() renames them. Both return true on success and false on failure.

5.2.11. Debugging SFTP

To log output, the NET_SFTP_LOGGING constant will need to be defined. If you want full logs, you'll need to do define('NET_SFTP_LOGGING', NET_SFTP_LOG_COMPLEX). $ssh->getSFTPLog() will then return a string containing the unencrypted packets in hex and ASCII. If you want to just record the packet types that are being sent to and fro, you'll need to do define('NET_SFTP_LOGGING', NET_SFTP_LOG_SIMPLE). $ssh->getLog() will then return an array. Both log types include the amount of time it took to send the packet in question. The former is useful for general diagnostics and the latter is more useful for profiling. An example follows:

<?php
include('Net/SFTP.php');
define('NET_SFTP_LOGGING', NET_SFTP_LOG_COMPLEX); // or NET_SFTP_LOG_SIMPLE

$sftp = new Net_SFTP('www.domain.tld');
if (!$sftp->login('username', 'password')) {
    exit('Login Failed');
}

print_r($sftp->nlist());
echo $ssh->getSFTPLog();
?>

Depending on the problem, it may be more effective to just look at the output of $ssh->getLastSFTPError() (which returns a string) and $ssh->getSFTPErrors() (which returns an array) than to sift through the logs.