Inherited Variables
Inherited Methods
Class Details
Allow for remote execution of commands using SSH
The SExec class provides a number of facilities for remote command execution using SSH.
The name SExec comes after "rexec" (the remote execution library) and the "exec" facilities available under PHP. As a matter of fact, we try to mimic to some extent the execution facilities offered by PHP over SSH: thus you will find ssh_popen() akin to popen(), etc.
RATIONALE
The reason for this class is to allow executing code on a remote back-end avoiding MITM spoofs in your communications. This allows you to provide a web front-end (possibly redundant) and call a remote back-end to execute the job.
Furthermore, you may have fallback features where if execution on a remote back-end fails you can restart the command on a fallback remote host, increasing reliability.
DEPENDENCIES
The class relies on an underlying installation of SSH. It has been tested with OpenSSH on Linux, but should work on other systems with OpenSSH as well.
Further, the class in its current inception relies on OpenSSH version being greater than 3.8. If you have an older SSH, please use version 1.0 of this class instead.
DESIGN RATIONALE
The reasons for the choices taken are simple: we might have relied on an SSH library (like libSSH) and integrated it with PHP, but then, any weakness/bug/change on said library would require a recompilation of the library and PHP. This is a serious inconvenience. More to that, it would require the maintenance of two simultaneous SSH installations, viz. OpenSSH and the library, duplicating the work of tracking security/bug issues.
By using the underlying SSH commands, we become independent of them: if anything is discovered, you just have to update your system SSH, and nothing else. Otherwise you would have a dependency on SSH to remember, which is always forgotten. This way we avoid getting out of sync with the system's SSH.
Better yet: this easies development, making this class a lot simpler to write, understand, maintain and debug.
Finally, the dependency on SSH being OpenSSH 3.8 or greater is due to efficiency reasons. Establishing an SSH connection is costly in time. If you are going to make many, this would impose a heavy cost on your scripts. We routinely launch several thousand remote jobs, and authentication delays soon proved unacceptable.
OpenSSH 3.8 introduced the possibility of sharing a single SSH channel between many "connections". This means that only the first (or master) instance (which will provide the shared channel) needs to authenticate, hence saving significant time.
The constructor then creates a master channel, leaves it idle all the object's lifetime and closses it at the end. This channel might be used as well, but we felt it wasn't such a big loss to keep it idle, and furthermore, being the master, we didn't want to risk getting into any trouble that might close it prematurely. So it stands.
All other routines (which actually do the work) simply hijack on the master channel, hence avoiding the costly authentication step (and executing significantly faster). The only exception are the "COPY" routines, which can not hijack the master channel and hence must do authentication every time.
One more detail: some methods allow for interactive communication with the remote end. We have simply used a terminal-less connection for them, using regular files as the intermediate communication channels. A pipe implementation is also possible, and works as well, but we have found that dealing with pipes is tricky and error-prone, while using files is simple and intuitive, so we opted for using files.
The difference has to do with the way you communicate with the other end: using pipes you may block on read and/or write, and so may the other end. Since there may occur errors in the process, that implies that getting into a deadlock is trivial. Just picture this scenarios:
You send a command -> the remote ends starts the command and prompts for input on stdout, hangs reding on stdin -> you read the prompt and send the input -> the remote end wakes and processes it.
You send a command -> the remote end fails, logs an error on stderr, gets back the system prompt and hangs on reading stdin -> you notice the prompt and read stderr... since you can't predict the length of the error message you must empty the pipe... and when doing it you hang after reading the last char... -> deadlock
You send a command -> the remote end fails, logs an error on stderr, gets back the system prompt and hangs on reading stdin -> you don't read stderr to avoid hanging, so submit a new command... this goes on and on until the remote side's stderr buffer fills, then the remote side locks waiting for you to read stderr -> you can't know it hang, so you try to submit a new command, and hang on writing waiting for the other end to read your command -> deadlock
More scenarios are possible, and since you (or the other side) can't predict what's going to happen, it is very tricy to avoid them.
Now, using files, you don't have that problem: whenever you reach the current end-of-file, you get an EOF, no need to hang waiting for the other side to fill it in with data. The other side doesn't hang on writing unless your disk space fills up. It's a lot simpler.
Your problem with files is continuing reads after new data becomes available: the safest way is to call flush() before reading and seeking to the last position read to avoid having to re-read everything (which implies that after finishing reading you must ftell() your position.
See the included test script for examples.
CUSTOMIZATION
You must state to the class where your SSH executables (ssh and scp) are located. This allows you to have them placed anywhere, but also implies the responsability of using full pathnames to reduce hacking dangers. It also allows you to use/test a new SSH implementation installed in a non-standard place before switching to it, or even to keep various SSH installations on the system (e.g. if the system's SSH is not up-to-date, you may install one on your home and use it).
You may also indicate where to store temporary files. This must be a directory followed by a prefix to use when creating a temporal directory. The parent directory must be writeable by the user who runs the class (usually it will be run by apache, www or some such). Most commonly the parent directory will be /tmp or $DocumentRoot/tmp or something similar.
The directory+prefix you state will be used to create a unique temporary work directory for each object instantiated. Examples of a valid specifications are "/tmp/phpSsh-" or "/tmp/". When the object is instantiated, a random string will be appended to this value to create the actual temporary directory name.
The reason for allowing specifying a prefix is so that debugging may be easier by facilitating identification of temporaries generated by this class.
DEBUGGING
The class comes with extensive debugging aids. To enable them, just set a global variable called $debug to TRUE. This will output abundant debugging information and leave copies of communication log files for your reference.
Additionally, there is a sample demo script that shows how to use this class and may help you debug it. This script is included in the distribution (or should be) as 'ssh_debug.php'. See notes and comments within it for more details.
Tags:
- see - ssh(1), scp(1)
- since - File available since Release 1.0
- link - http://savannah.cern.ch/projects/GridGRAMM
- version - Release: @package_version@
- copyright - José R. Valverde <jrvalverde@es.embnet.org>
- license - doc/lic/
- author - José R. Valverde <mailto:jrvalverde@acm.org>
[ Top ]
Class Variables
$master =
[line 280]
handle to process controlling the master channel
Tags:
- access - private
Type: string
Overrides:
[ Top ]
$master_input =
[line 288]
stdin of process controlling the master channel
Tags:
- access - private
Type: string
Overrides:
[ Top ]
$mplex_socket = "/tmp/ssh.mplex"
[line 272]
name of multiplexing socket
Tags:
- access - private
Type: string
Overrides:
[ Top ]
$password =
[line 240]
remote password
Tags:
- access - private
Type: string
Overrides:
[ Top ]
$remote =
[line 232]
remote endpoint ([user@]host[:port])
Tags:
- access - private
Type: string
Overrides:
[ Top ]
$scp = "/usr/bin/scp"
[line 256]
location of scp program
Tags:
- access - private
Type: string
Overrides:
[ Top ]
$ssh = "/usr/bin/ssh"
[line 248]
location of ssh program
Tags:
- access - private
Type: string
Overrides:
[ Top ]
$version = "2.0"
[line 224]
The current version of the class
Tags:
- access - public
Type: string
Overrides:
[ Top ]
$workdir = "/tmp/phpSsh"
[line 264]
tmp. dir prefix specification
Tags:
- access - private
Type: string
Overrides:
[ Top ]
Class Methods
SExec
SExec SExec(
[string
$remote = "localhost"], [string
$password = "xxyzzy"])
[line 373]
Class constructor.
Generate a new instance of a remote execution environment. The object returned allows you to invoke commands to be executed remotely in a way similar to PHP exec commands (popen, proc_open...) over SSH (so that your communications can be secure).
You may specify a remote endpoint and a password, a remote endpoint alone, or nothing at all.
If you provide a remote endpoint and password they are used to drive the communications and execute your commands.
If no password is provided, then a default of "xxyzzy" (the canonical computer magic word) is used. Unless this is your password (not recommended), this means that the default password is useless unless you are working in a trusted environment where it is not needed and ignored. That may be the case if you enable trusting mechanisms with .shosts/.rhosts or passphraseless RSA/DSA authentication. Not that we endorse them either, but in these cases any password provided will be ignored and it doesn't make sense to provide a real one: xxyzzy can do as well as any other.
If no password and no remote end is provided, then "localhost" is used as the remote end, assuming no password is required (as described above). This is only useful if localhost is trusted, and you have reasons to use SSH internally... Some people does.
Regarding the remote end specification, it can be any valid single-string SSH remote end description: the basic format is
[username@]remote.host[:port]
- "username" is the remote user name to log in as. It is optional. If provided, it must be separated from the remote host by an "@" sign. If it is not provided, the remote username is assumed to be the same as the current local one.
- "remote.host" is a valid host specification, either a numeric IP address or a valid host name (which may require a full name or not depending on your settings).
- "port" is the remote port where SSH is listening and which we want to connect to. It is optional, and if provided, must follow the remote host specification separated from it by a colon ":". If not provided, the default port (22) is used.
Here is an example of how to use this constructor: <sample> require_once 'ssh.php'; $remote = "jruser@example.com"; $password = "PASSWORD";
$rmt = new SExec($remote, $password); if (! i$rmt) echo "Couldn't connect to $remote\n"; </sample>
Tags:
- return - a new connection object with the remote end or FALSE if the connection could not be established.
- since - Method available since Release 1.0
- access - public
Parameters:
- string $remote - remote The remote end to run the command, in the form 'user@host:port' (you may omit the 'user@' or ':port' parts if the default values [i.e. same user or standard port] are OK).
- string $password - password The remote password. Note that if direct RSA/DSA/.shosts/.rhosts login is enabled then the password will be ignored as SSH should not run the ASKPASS command).
[ Top ]
destruct
exit destruct(
)
[line 496]
Class destructor
Destroy all working processes and data streams and structures used by an instance of this class.
This method will send a termination message to the other end of the master channel, close the control stream of the master channel and terminate its controlling process, finally unsetting the object and setting the object handle to NULL.
If a global $debug is not set to TRUE, then it will also remove all communication traces of this object: i.e. all log files for interactive and master sessions, communications socket, etc...
If global $debug is set to TRUE, then a copy of all log files created during the lifetime of the object will be left on a temporary directory for your perusal and reference.
Tags:
- return - status of the mater channel control process.
Parameters:
[ Top ]
ssh_close
the ssh_close(
p
$p)
[line 1064]
Close an SSH interactive session
This method terminates a previously open interactive remote session. It will send a termination notification to the remote end, close the connection with control and communication streams, and terminate the local control process.
Copies of the log files that contain the output and error of the communication are left out for later reference and local peruse. If you don't need them any longer, you may delete them or just leave them around until the class destructor is called (which will remove all session traces),
Tags:
- return - exit status of the remote interactive session.
Parameters:
- p $p - an associative array with the description of the interactive session control process, obtained by a previous call to one of the interactive session creation methods ssh_open_shell() or ssh_open_command().
[ Top ]
ssh_copy
status ssh_copy(
origin
$origin, destination
$destination, password
$password)
[line 569]
Copy a file or directory from one source to a destination
This function copies source to dest, where one of them is a local filespec and the other a remote filespec of the form [user@]host:path
If the original source is a directory, it will be copied recursively to destination (hence easing file transfers).
The function returns TRUE on success or FALSE on failure.
EFFICIENCY NOTE:
The copy routines use 'scp' to do their actual work. Since scp seems to be unable to hitchhike on the master channel, we must do authentication for each copy operation (subroutine call). These routines are hence a lot more time-expensive than all the other ones.
You may want to consider whether you can group several copies into one single call to reduce authentication overheads.
Tags:
- return - TRUE if all went well, or FALSE on failure.
- see - scp(1) <b>NOTE</b> DEPRECATED (inconsistent with the class)
Parameters:
- origin $origin - The origin path, of the form [user@][host][:port]path You may omit the optional sections if the default values (local username, local host, standard SSH port) are OK
- destination $destination - The destination path, of the form [user@][host][:port:]path You may omit the optional sections if the default values (local username, local host, standard SSH port) are OK
- password $password - The password to use to connect to the remote end of the copy (be it the origin or the destination, it's all the same). If connection is automatic by some means (.shosts or RSA/DSA authentication) then it should be ignored and any password should do.
[ Top ]
ssh_copy_from
status ssh_copy_from(
remotepath
$remotepath, localpath
$localpath)
[line 692]
Copy a file or directory from a remote source to a local destination
This function copies source to dest, where first of them is a remote filespec and then comes a local filespec, both specified as normal system paths.
Both, local and remote paths may be absolute or relative.
If the original source is a directory, it will be copied recursively to destination (hence easing file transfers).
The function returns TRUE on success or FALSE on failure.
EFFICIENCY NOTE:
The copy routines use 'scp' to do their actual work. Since scp seems to be unable to hitchhike on the master channel, we must do authetication for each copy operation (subroutine call). These routines are hence a lot more time-expensive than all the other ones.
You may want to consider whether you can group several copies into one single call to reduce authentication overheads.
Tags:
- return - TRUE if all went well, or FALSE on failure.
- see - scp(1)
Parameters:
- remotepath $remotepath - The origin remote path, either absolute or relative to the login home. If it denotes a directory, the copy will be recursive.
- localpath $localpath - The local destination path, either absolute or relative to the current working directory.
[ Top ]
ssh_copy_to
status ssh_copy_to(
localpath
$localpath, remotepath
$remotepath)
[line 631]
Copy a file or directory from a local source to a remote destination
This function copies source to dest, where first of them is a local filespec and then comes a remote filespec as a normal system path.
Both, local and remote paths may be absolute or relative.
If the original source is a directory, it will be copied recursively to destination (hence easing file transfers).
The function returns TRUE on success or FALSE on failure.
EFFICIENCY NOTE:
The copy routines use 'scp' to do their actual work. Since scp seems to be unable to hitchhike on the master channel, we must do authetication for each copy operation (subroutine call). These routines are hence a lot more time-expensive than all the other ones.
You may want to consider whether you can group several copies into one single call to reduce authentication overheads.
Tags:
- return - TRUE if all went well, or FALSE on failure.
- see - scp(1)
Parameters:
- localpath $localpath - The origin local path, either absolute or relative to the current working directory. If it denotes a directory, the copy will be recursive.
- remotepath $remotepath - The destination path, either absolute or relative to the login home.
[ Top ]
ssh_exec
status ssh_exec(
command
$command, output
&$out)
[line 796]
Execute a remote command using SSH
This function sort of mimics rexec(3) using SSH as the transport protocol.
The function returns the exit status of the remote command, and appends the remote job output to an optional argument.
This function is intended to be used as a one-time all-at-once non-interactive execution mechanism which will run the command remotely and return its output.
If you try to issue an interactive command using this function, all you will get is unneccessary trouble. So don't!
Tags:
- return - will hold the termination status of SSH after invocation, which should be the exit status of the remote command or 255 if an error occurred
Parameters:
- command $command - The command to execute on the remote end NOTE: if you want to use redirection, the entire remote command line should be enclosed in additional quotes!
- output &$out - Optional, the collated (stdout+stderr) output of the remote command.
[ Top ]
ssh_open_command
a ssh_open_command(
command
$command)
[line 951]
Open an SSH connection to run an interactive command on a remote site
Connects to a remote host and runs an interactive command with NO controlling terminal.
This routine creates communication streams with the remote shell, and stores all output (standard and error) of the connection into two separate local log files (one for stdout and one for stderr).
Returns a process_control array which contains the process resource ID and an the standard file descriptors which the caller may use to interact with the remote shell.
The process control array contains:
'process' -- the process resource for the newly created connection
'std_in' -- handle to the standard input of the new connection
'std_out' -- handle to standard output of the new connection
'std_err' -- handle to standard error of the new connection
'stdout_file' -- actual filename of the local log file for the new connection standard output
'stderr_file' -- actual filename of the local log file for the new connection standard error
Tags:
- return - process control associative array.
Parameters:
- command $command - to be executed interactively on the remote end
[ Top ]
ssh_open_shell
a ssh_open_shell(
)
[line 843]
Open an SSH connection to a remote site with a shell to run interactive commands
Connects to a remote host and opens an interactive shell session with NO controlling terminal.
This routine creates communication streams with the remote shell, and stores all output (standard and error) of the connection into two separate local log files (one for stdout and one for stderr).
Returns a process_control array which contains the process resource ID and an the standard file descriptors which the caller may use to interact with the remote shell.
The process control array contains:
'process' -- the process resource for the newly created connection
'std_in' -- handle to the standard input of the new connection
'std_out' -- handle to standard output of the new connection
'std_err' -- handle to standard error of the new connection
'stdout_file' -- actual filename of the local log file for the new connection standard output
'stderr_file' -- actual filename of the local log file for the new connection standard error
Tags:
- return - process control associative array.
Parameters:
[ Top ]
ssh_out_expect
void ssh_out_expect(
mixed
$p, [mixed
$expr = "^# "])
[line 1032]
Get output until we reach a given regular expression
NOTE EXPERIMENTAL, requires more thought and experience.
Parameters:
[ Top ]
ssh_passthru
void ssh_passthru(
command
$command, status
&$status)
[line 751]
Execute a single command remotely using ssh and display its output, optionally returning its exit status (like passthru)
This function is intended to be used as a one-time all-at-once non-interactive execution mechanism which will run the command remotely and display its output.
If you try to issue an interactive command using this function, all you will get is unneccessary trouble. So don't!
This might be done as well using a pipe on /tmp and making the command 'cat' the pipe: when ssh runs, it runs the command 'cat' on the pipe and hangs on read. Then we just need a thread to open the pipe, put the password and close the pipe.
This other way the password is never wirtten down. But, OTOH, the file life is so ephemeral that most of the time it will only exist in the internal system cache, so this approach is not that bad either.
Tags:
- see - passthru()
Parameters:
- command $command - The command to execute on the remote end NOTE: if you want to use redirection, the entire remote command line should be enclosed in additional quotes!
- status &$status - Optional, this will hold the termination status of SSH after invocation, which should be the exit status of the remote command or 255 if an error occurred
[ Top ]
ssh_pclose
the ssh_pclose(
f
$f)
[line 1137]
Close a piped remote execution command control pipe.
This routine accepts as input the handle for the control stream of a remote command and closes it, terminating the command as well. The handle must be valid and obtained through a call to ssh_popen().
Tags:
- return - termination status of the command that was run.
Parameters:
- f $f - is the file handle associated with the pipe control stream
[ Top ]
ssh_popen
a ssh_popen(
command
$command, mode
$mode)
[line 1118]
Execute a remote command and keep an unidirectional stream contact with it.
This routine mimics 'popen()' but uses ssh to connect to a remote host and run the requested command: in other words, it opens a pipe to a remotely executed command. This pipe is unidirectional, with the communications direction controlled by a method parameter.
Tags:
- return - handle to the unidirectional communication stream, similar to that returned by fopen(), or FALSE on failure. This handle must be closed with ssh_pclose().
- see - popen() for more details.
Parameters:
- command $command - is the command to execute on the remote end
- mode $mode - specifies the communications direction for the pipe: if set to "r" (read), then we will be able to collect command output only; if set to "w" (write) then we may only send input to the remote command.
[ Top ]