Main Page | Namespace List | File List | File Members

ssh-files.php

Go to the documentation of this file.
00001 <?php
00002 
00003 /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
00004 
00005 /**
00006  * ssh.php -- a class to execute remote commands over SSH
00007  *
00008  *      This file contains the files-based implementation of the SExec
00009  * class. This implementation relies on the usage of regular temporary
00010  * files to communicate with the remote end, thus avoiding several
00011  * drawbacks (mainly deadlocks) associated with pipes.
00012  *
00013  *      The SExec class provides methods to launch and control jobs and
00014  * transfer files over SSH.
00015  *
00016  * PHP versions 4 and 5
00017  *
00018  * LICENSE:
00019  * This library is free software; you can redistribute it and/or
00020  * modify it under the terms of the GNU Lesser General Public
00021  * License as published by the Free Software Foundation; either
00022  * version 2.1 of the License, or (at your option) any later version.
00023  * 
00024  * This library is distributed in the hope that it will be useful,
00025  * but WITHOUT ANY WARRANTY; without even the implied warranty of
00026  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
00027  * Lesser General Public License for more details.
00028  * 
00029  * You should have received a copy of the GNU Lesser General Public
00030  * License along with this library; if not, write to the Free Software
00031  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
00032  *
00033  * @category    Net
00034  * @package     SExec
00035  * @author      José R. Valverde <jrvalverde@acm.org>
00036  * @copyright   José R. Valverde <jrvalverde@es.embnet.org>
00037  * @license     doc/lic/
00038  * @version     CVS: $Id: ssh-files.php,v 1.6 2005/05/25 16:02:40 netadmin Exp $
00039  * @link        http://savannah.cern.ch/projects/GridGRAMM
00040  * @see         ssh(1), scp(1)
00041  * @since       File available since Release 1.0
00042  */
00043 
00044 
00045 /** 
00046  * Allow for remote execution of commands using SSH
00047  *
00048  *      The SExec class provides a number of facilities for remote
00049  * command execution using SSH.
00050  *
00051  *      The name SExec comes after "rexec" (the remote execution library)
00052  * and the "exec" facilities available under PHP. As a matter of fact,
00053  * we try to mimic to some extent the execution facilities offered by
00054  * PHP over SSH: thus you will find ssh_popen() akin to popen(), etc.
00055  *
00056  * <b>RATIONALE</b>
00057  *
00058  *      The reason for this class is to allow executing code on a remote
00059  * back-end avoiding MITM spoofs in your communications. This allows you
00060  * to provide a web front-end (possibly redundant) and call a remote
00061  * back-end to execute the job.
00062  *
00063  *      Furthermore, you may have fallback features where if execution
00064  * on a remote back-end fails you can restart the command on a fallback
00065  * remote host, increasing reliability.
00066  *
00067  * <b>DEPENDENCIES</b>
00068  *
00069  *      The class relies on an underlying installation of SSH. It has
00070  * been tested with OpenSSH on Linux, but should work on other systems
00071  * with OpenSSH as well.
00072  *
00073  *      Further, the class in its current inception relies on OpenSSH
00074  * version being greater than 3.8. If you have an older SSH, please use
00075  * version 1.0 of this class instead.
00076  *
00077  * <b>DESIGN RATIONALE</b>
00078  *
00079  *      The reasons for the choices taken are simple: we might have
00080  * relied on an SSH library (like libSSH) and integrated it with PHP,
00081  * but then, any weakness/bug/change on said library would require a
00082  * recompilation of the library and PHP. This is a serious inconvenience.
00083  * More to that, it would require the maintenance of two simultaneous
00084  * SSH installations, viz. OpenSSH and the library, duplicating the work
00085  * of tracking security/bug issues.
00086  *
00087  *      By using the underlying SSH commands, we become independent of
00088  * them: if anything is discovered, you just have to update your system
00089  * SSH, and nothing else. Otherwise you would have a dependency on SSH
00090  * to remember, which is always forgotten. This way we avoid getting out
00091  * of sync with the system's SSH.
00092  *
00093  *      Better yet: this easies development, making this class a lot
00094  * simpler to write, understand, maintain and debug.
00095  *
00096  *      Finally, the dependency on SSH being OpenSSH 3.8 or greater is
00097  * due to efficiency reasons. Establishing an SSH connection is costly
00098  * in time. If you are going to make many, this would impose a heavy
00099  * cost on your scripts. We routinely launch several thousand remote 
00100  * jobs, and authentication delays soon proved unacceptable.
00101  *
00102  *      OpenSSH 3.8 introduced the possibility of sharing a single SSH
00103  * channel between many "connections". This means that only the first
00104  * (or master) instance (which will provide the shared channel) needs 
00105  * to authenticate, hence saving significant time.
00106  *
00107  *      The constructor then creates a master channel, leaves it idle
00108  * all the object's lifetime and closses it at the end. This channel
00109  * might be used as well, but we felt it wasn't such a big loss to keep
00110  * it idle, and furthermore, being the master, we didn't want to risk
00111  * getting into any trouble that might close it prematurely. So it stands.
00112  *
00113  *      All other routines (which actually do the work) simply hijack on
00114  * the master channel, hence avoiding the costly authentication step (and
00115  * executing significantly faster). The only exception are the "COPY" 
00116  * routines, which can not hijack the master channel and hence must do
00117  * authentication every time.
00118  *
00119  *      One more detail: some methods allow for interactive communication
00120  * with the remote end. We have simply used a terminal-less connection
00121  * for them, using regular files as the intermediate communication channels.
00122  * A pipe implementation is also possible, and works as well, but we have
00123  * found that dealing with pipes is tricky and error-prone, while using
00124  * files is simple and intuitive, so we opted for using files.
00125  *
00126  *      The difference has to do with the way you communicate with the
00127  * other end: using pipes you may block on read and/or write, and so
00128  * may the other end. Since there may occur errors in the process, that
00129  * implies that getting into a deadlock is trivial. Just picture this
00130  * scenarios:
00131  *
00132  *      You send a command -> the remote ends starts the command and
00133  * prompts for input on stdout, hangs reding on stdin -> you read the 
00134  * prompt and send the input -> the remote end wakes and processes it.
00135  *
00136  *      You send a command -> the remote end fails, logs an error on
00137  * stderr, gets back the system prompt and hangs on reading stdin -> you
00138  * notice the prompt and read stderr... since you can't predict the 
00139  * length of the error message you must empty the pipe... and when doing it
00140  * you hang after reading the last char... -> deadlock
00141  *
00142  *      You send a command -> the remote end fails, logs an error on stderr,
00143  * gets back the system prompt and hangs on reading stdin -> you don't read
00144  * stderr to avoid hanging, so submit a new command... this goes on and on
00145  * until the remote side's stderr buffer fills, then the remote side locks
00146  * waiting for you to read stderr -> you can't know it hang, so you try
00147  * to submit a new command, and hang on writing waiting for the other end
00148  * to read your command -> deadlock
00149  *
00150  *      More scenarios are possible, and since you (or the other side)
00151  * can't predict what's going to happen, it is very tricy to avoid them.
00152  *
00153  *      Now, using files, you don't have that problem: whenever you reach
00154  * the current end-of-file, you get an EOF, no need to hang waiting for
00155  * the other side to fill it in with data. The other side doesn't hang on
00156  * writing unless your disk space fills up. It's a lot simpler. 
00157  *
00158  *      Your problem with files is continuing reads after new data becomes
00159  * available: the safest way is to call flush() before reading and seeking
00160  * to the last position read to avoid having to re-read everything (which
00161  * implies that after finishing reading you must ftell() your position.
00162  *
00163  *      See the included test script for examples.
00164  *
00165  * <b>CUSTOMIZATION</b>
00166  *
00167  *      You <i>must</i> state to the class where your SSH executables (ssh and
00168  * scp) are located. This allows you to have them placed anywhere, but
00169  * also implies the responsability of using full pathnames to reduce
00170  * hacking dangers. It also allows you to use/test a new SSH implementation
00171  * installed in a non-standard place before switching to it, or even to
00172  * keep various SSH installations on the system (e.g. if the system's
00173  * SSH is not up-to-date, you may install one on your home and use it).
00174  *
00175  *      You may also indicate where to store temporary files. This must 
00176  * be a directory followed by a prefix to use when creating a temporal
00177  * directory. The parent directory must be writeable by the user who runs 
00178  * the class (usually it will be run by apache, www or some such). Most commonly 
00179  * the parent directory will be /tmp or $DocumentRoot/tmp or something similar.
00180  *
00181  *      The directory+prefix you state will be used to create a unique
00182  * temporary work directory for each object instantiated. Examples of
00183  * a valid specifications are "/tmp/phpSsh-" or "/tmp/". When the object is 
00184  * instantiated, a random string will be appended to this value to create
00185  * the actual temporary directory name.
00186  *      
00187  *      The reason for allowing specifying a prefix is so that debugging
00188  * may be easier by facilitating identification of temporaries generated
00189  * by this class.
00190  *
00191  * <b>DEBUGGING</b>
00192  *
00193  *      The class comes with extensive debugging aids. To enable them,
00194  * just set a global variable called $debug to TRUE. This will output
00195  * abundant debugging information and leave copies of communication log
00196  * files for your reference.
00197  *
00198  *      Additionally, there is a sample demo script that shows how to
00199  * use this class and may help you debug it. This script is included 
00200  * in the distribution (or should be) as 'ssh_debug.php'. See notes
00201  * and comments within it for more details.
00202  *
00203  * @category    Net
00204  * @package     SExec
00205  * @author      José R. Valverde <jrvalverde@acm.org>
00206  * @copyright   José R. Valverde <jrvalverde@es.embnet.org>
00207  * @license     doc/lic/
00208  * @version     Release: @package_version@
00209  * @link        http://savannah.cern.ch/projects/GridGRAMM
00210  * @see         ssh(1), scp(1)
00211  * @since       File available since Release 1.0
00212 
00213  */
00214 class SExec {
00215 
00216     // {{{ properties
00217 
00218     /**
00219      * The current version of the class
00220      *
00221      * @var string
00222      * @access public
00223      */
00224     var $version="2.0";
00225     
00226     /**
00227      * remote endpoint ([user@]host[:port])
00228      *
00229      * @var string
00230      * @access private
00231      */
00232     var $remote;
00233     
00234     /**
00235      * remote password 
00236      *
00237      * @var string
00238      * @access private
00239      */
00240     var $password;
00241 
00242     /**
00243      * location of ssh program
00244      *
00245      * @var string
00246      * @access private
00247      */
00248     var $ssh = "/usr/bin/ssh";
00249     
00250     /**
00251      * location of scp program
00252      *
00253      * @var string
00254      * @access private
00255      */
00256     var $scp = "/usr/bin/scp";
00257     
00258     /**
00259      * tmp. dir prefix specification
00260      *
00261      * @var string
00262      * @access private
00263      */
00264     var $workdir = "/tmp/phpSsh";
00265     
00266     /**
00267      * name of multiplexing socket
00268      *
00269      * @var string
00270      * @access private
00271      */
00272     var $mplex_socket = "/tmp/ssh.mplex";
00273     
00274     /**
00275      * handle to process controlling the master channel
00276      *
00277      * @var string
00278      * @access private
00279      */
00280     var $master;
00281     
00282     /**
00283      * stdin of process controlling the master channel
00284      *
00285      * @var string
00286      * @access private
00287      */
00288     var $master_input;
00289 
00290     //}}}
00291     
00292     //{{{   instantiation
00293     
00294     /** Class constructor.
00295      *
00296      *  Generate a new instance of a remote execution environment.
00297      * The object returned allows you to invoke commands to be executed
00298      * remotely in a way similar to PHP exec commands (popen, proc_open...)
00299      * over SSH (so that your communications can be secure).
00300      *
00301      *  You may specify a remote endpoint and a password, a remote endpoint
00302      * alone, or nothing at all.
00303      *
00304      *  If you provide a remote endpoint and password they are used to drive
00305      * the communications and execute your commands.
00306      *
00307      *  If no password is provided, then a default of "xxyzzy" (the canonical
00308      * computer magic word) is used. Unless this is your password (not 
00309      * recommended), this means that the default password is useless unless
00310      * you are working in a trusted environment where it is not needed and
00311      * ignored. That may be the case if you enable trusting mechanisms with
00312      * .shosts/.rhosts or passphraseless RSA/DSA authentication. Not that
00313      * we endorse them either, but in these cases any password provided will
00314      * be ignored and it doesn't make sense to provide a real one: xxyzzy
00315      * can do as well as any other.
00316      *
00317      *  If no password and no remote end is provided, then "localhost" is
00318      * used as the remote end, assuming no password is required (as described
00319      * above). This is only useful if localhost is trusted, and you have reasons
00320      * to use SSH internally... Some people does.
00321      *
00322      *  Regarding the remote end specification, it can be any valid single-string
00323      * SSH remote end description: the basic format is
00324      *
00325      *  [username@]remote.host[:port]
00326      *
00327      *  - "username" is the remote user name to log in as. It is optional. If provided, 
00328      *    it must be separated from the remote host by an "@" sign. If it is not 
00329      *    provided, the remote username is assumed to be the same as the current local
00330      *    one.
00331      *
00332      *  - "remote.host" is a valid host specification, either a numeric IP address
00333      *    or a valid host name (which may require a full name or not depending on
00334      *    your settings).
00335      *
00336      *  - "port" is the remote port where SSH is listening and which we want to
00337      *    connect to. It is optional, and if provided, must follow the remote host
00338      *    specification separated from it by a colon ":". If not provided, the
00339      *    default port (22) is used.
00340      *
00341      *  Examples of remote host specifications are "user@host.example.net:22",
00342      * "someone@host:22", "host.example.net:22", "host:22", 
00343      * "somebody@host.example.net", "user@host", "host.example.net", "host".
00344      *
00345      * Here is an example of how to use this constructor:
00346      * <code>
00347      *  require_once 'ssh.php';
00348      *
00349      *  $remote = "jruser@example.com";
00350      *  $password = "PASSWORD";
00351      *
00352      *  $rmt = new SExec($remote, $password);
00353      *   if (! $rmt)
00354      *          echo "Couldn't connect to $remote\n";
00355      * </code>
00356      *
00357      *  @param string remote   The remote end to run the command, in
00358      *                      the form 'user@host:port' (you may
00359      *                      omit the 'user@' or ':port' parts
00360      *                      if the default values [i.e. same user
00361      *                      or standard port] are OK).
00362      *
00363      *  @param string password The remote password. Note that if direct
00364      *                      RSA/DSA/.shosts/.rhosts login is enabled
00365      *                      then the password will be ignored as
00366      *                      SSH should not run the ASKPASS command).
00367      *
00368      *  @return SExec|false a new connection object with the remote end or
00369      *                      FALSE if the connection could not be established.
00370      *
00371      *  @access public
00372      *  @since Method available since Release 1.0
00373      */
00374     function SExec($remote="localhost", $password="xxyzzy")
00375     {
00376         global $debug;
00377         
00378         if ($debug) echo "\nCreating a new SExec\n";
00379         $this->remote = $remote;
00380         $this->password = "$password";
00381         umask(0077);
00382         /* DESIGN
00383          * In order to increase efficiency, we will create a master channel
00384          * on class instantiation. The master channel should be closed by a
00385          * corresponding class destructor!
00386          *
00387          * Creating a master channel has the advantage that subsequent SSH
00388          * connections will use it and avoid repeating the slow authentication
00389          * process: in other words, they will go much, much faster.
00390          */
00391          
00392         // first we must generate a unique UNIX socket address or we'll fail
00393         // We use a tricky trick: generate two random numbers and use them;
00394         // this is tricky since there might be a problem, but with very low
00395         // probability. BUT IT MAY STILL FAIL: there's a race condition between
00396         // the end of the while and the subsequent if.
00397         do {
00398             mt_srand((double)microtime()*1000000 ) .
00399             $this->workdir = "/tmp/phpSsh-" . mt_rand() .".". mt_rand();
00400             if ($debug) echo "\nSExec: trying $this->workdir/ ...";
00401             // CAUTION: this is potentially an endless loop (albeit with very
00402             // low probability) if every possible file did exist.
00403         }
00404         while (file_exists($this->workdir));
00405         if (mkdir($this->workdir) == FALSE) {
00406             // we can't continue. How can we cancel this?
00407             // try these and then check what happens
00408             unset($this);
00409             $this = NULL;
00410             return FALSE;
00411         }
00412         else 
00413             if ($debug) echo " OK\n";
00414         // Now we have a place to put the socket... Mmm...
00415         // Come to think of it, we have a place to put ANY temporary
00416         // for the class... 
00417         // XXX Maybe we can change everywhere else to use this?
00418         $this->mplex_socket = $this->workdir."/mplex_socket";
00419         
00420         // Finally we can call SSH -M
00421         // Create SSH_ASKPASS script to provide the password
00422         $tmpfname = tempnam($this->workdir, 'SExec-');
00423         chmod($tmpfname, 0700);
00424         putenv("DISPLAY=none:0.");
00425         putenv("SSH_ASKPASS=$tmpfname");
00426         $fp = fopen($tmpfname, "w");
00427         fputs($fp, "#!/bin/sh\necho $this->password\n");
00428         if (!$debug) 
00429             fputs($fp, "/bin/touch $tmpfname.called\n");
00430         else
00431             fputs($fp, "/bin/rm -f $tmpfname\n");
00432         fclose($fp);
00433         
00434         // OK, we are ready. Now let's open a master shell
00435         $child_stdout = tempnam($this->workdir, "open_sh-O-");
00436         $child_stderr = tempnam($this->workdir, "open_sh-E-");
00437         $descriptorspec = array(
00438             0 => array("pipe", "r"),  // connect child's stdin to the read end of a pipe
00439             1 => array("file", $child_stdout, "a"),  // connect child's stdout to the write end of a pipe
00440             2 => array("file", $child_stderr, "a")   // stderr is a pipe to read from
00441         );
00442 
00443         if ($debug) echo "$this->ssh -x -t -t ".
00444                      "-M -S $this->mplex_socket " .
00445                      "$this->remote\n";
00446         $this->master = proc_open("$this->ssh -x -t -t ".
00447                      "-M -S $this->mplex_socket " .
00448                      "$this->remote",
00449                      $descriptorspec,
00450                      $pipes);
00451         if ((! is_resource($this->master)) || ($this->master == FALSE)) {
00452                 putenv("SSH_ASKPASS=dummy");
00453                 unset($this);
00454                 $this = NULL;
00455                 return FALSE;
00456         }
00457         // we do not need to worry about the output log files, just the
00458         // input pipe for logout
00459         $this->master_input = $pipes[0];
00460         
00461         // Before going ahead, we need to ensure the control shell 
00462         // has started: wait for the socket to become available
00463         // note: there should be a timeout here to avoid a possibly
00464         // infinite loop XXX
00465         do {
00466             if ($debug) echo "waiting 0.1 sec\n";
00467             usleep(100000);     // wait 0.1 seconds
00468         } while (! file_exists($this->mplex_socket));
00469 
00470         // and now we must register a destructor for the class
00471         // that will close the connection.
00472         //register_shutdown_function($this->destruct());
00473         
00474         return $this;
00475     }
00476     
00477     /** Class destructor
00478      *
00479      *  Destroy all working processes and data streams and structures
00480      * used by an instance of this class.
00481      *
00482      *  This method will send a termination message to the other end
00483      * of the master channel, close the control stream of the master
00484      * channel and terminate its controlling process, finally unsetting
00485      * the object and setting the object handle to NULL.
00486      *
00487      *  If a global $debug is not set to TRUE, then it will also remove
00488      * all communication traces of this object: i.e. all log files for
00489      * interactive and master sessions, communications socket, etc...
00490      *
00491      *  If global $debug is set to TRUE, then a copy of all log files
00492      * created during the lifetime of the object will be left on a
00493      * temporary directory for your perusal and reference.
00494      *
00495      *  @return integer exit status of the master channel control process.
00496      *
00497      *  @access public
00498      *  @since Method available since Release 1.0
00499      */
00500     function destruct()
00501     {
00502         global $debug;
00503         
00504         if ($debug) echo "\nDestroying SExec master\n";
00505         if ($debug) print_r($this);
00506         if ($debug) echo "sending logout\n";
00507         // log out master process
00508         fputs($this->master_input, "\n\nlogout\n\n");
00509         // close master stdin
00510         fclose($this->master_input);
00511         // close master process
00512         $ret = proc_close($this->master);
00513         // remove temporaries
00514         if (! $debug) system("/bin/rm -rf $this->workdir");
00515         // utterly destroy this instance
00516         unset($this);
00517         $this = NULL;
00518         return $ret;
00519     }    
00520     
00521     //}}}
00522     
00523     //{{{ methods
00524     /**
00525      *  Copy a file or directory from one source to a destination
00526      *
00527      *  This function copies source to dest, where one of them is a
00528      * local filespec and the other a remote filespec of the form
00529      * [user@]host:path
00530      *
00531      *  If the original source is a directory, it will be copied
00532      * recursively to destination (hence easing file transfers).
00533      *
00534      *  The function returns TRUE on success or FALSE on failure.
00535      *
00536      * <b>EFFICIENCY NOTICE:</b>
00537      *
00538      *  The copy routines use 'scp' to do their actual work. Since
00539      * scp seems to be unable to hitchhike on the master channel,
00540      * we must do authentication for each copy operation (subroutine 
00541      * call). These routines are hence a lot more time-expensive 
00542      * than all the other ones.
00543      *
00544      *  You may want to consider whether you can group several
00545      * copies into one single call to reduce authentication 
00546      * overheads.
00547      *
00548      *  @note DEPRECATED (inconsistent with the class)
00549      *
00550      *  @see scp(1)
00551      *
00552      *  @param string origin    The origin path, of the form
00553      *                  [user@][host][:port]path
00554      *                  You may omit the optional sections if
00555      *                  the default values (local username, local
00556      *                  host, standard SSH port) are OK
00557      *
00558      *  @param string destination       The destination path, of the form
00559      *                  [user@][host][:port:]path
00560      *                  You may omit the optional sections if
00561      *                  the default values (local username, local
00562      *                  host, standard SSH port) are OK
00563      *
00564      *  @param string password  The password to use to connect to the remote
00565      *                  end of the copy (be it the origin or the
00566      *                  destination, it's all the same). If connection
00567      *                  is automatic by some means (.shosts or RSA/DSA
00568      *                  authentication) then it should be ignored and
00569      *                  any password should do.
00570      *
00571      *  @return bool TRUE if all went well, or FALSE on failure.
00572      *
00573      *  @access public
00574      *  @since Method available since Release 1.0
00575      *  @deprecated Method deprecated as of Release 2.1
00576      */
00577     function ssh_copy($origin, $destination, $password)
00578     {
00579         global $debug;
00580 
00581         umask(0077);
00582         $tmpfname = tempnam($this->workdir, "copy-");
00583         chmod($tmpfname, 0700);
00584         putenv("DISPLAY=none:0.");
00585         putenv("SSH_ASKPASS=$tmpfname");
00586         $fp = fopen($tmpfname, "w");
00587         fputs($fp, "#!/bin/sh\necho $password\n");
00588         if (! $debug)  
00589             fputs($fp, "/bin/touch $tmpfname.called\n");
00590         else
00591             fputs($fp, "/bin/rm $tmpfname\n");
00592         fclose($fp);
00593         exec("$this->scp -pqrC $origin $destination", $out, $status);
00594         if ($status == 0)
00595             return TRUE;
00596         else
00597             return FALSE;
00598     }
00599 
00600 
00601     /**
00602      *  Copy a file or directory from a local source to a remote destination
00603      *
00604      *  This function copies source to dest, where first of them is a
00605      * local filespec and then comes a remote filespec as a normal
00606      * system path.
00607      *
00608      *  Both, local and remote paths may be absolute or relative.
00609      *
00610      *  If the original source is a directory, it will be copied
00611      * recursively to destination (hence easing file transfers).
00612      *
00613      *  The function returns TRUE on success or FALSE on failure.
00614      *
00615      * <b>EFFICIENCY NOTICE:</b>
00616      *
00617      *  The copy routines use 'scp' to do their actual work. Since
00618      * scp seems to be unable to hitchhike on the master channel,
00619      * we must do authetication for each copy operation (subroutine 
00620      * call). These routines are hence a lot more time-expensive 
00621      * than all the other ones.
00622      *
00623      *  You may want to consider whether you can group several
00624      * copies into one single call to reduce authentication 
00625      * overheads.
00626      *
00627      *  @see scp(1)
00628      *
00629      *  @param string localpath    The origin local path, either absolute or
00630      *                  relative to the current working directory. 
00631      *                  If it denotes a directory, the copy will 
00632      *                  be recursive.
00633      *
00634      *  @param string remotepath   The destination path, either
00635      *                  absolute or relative to the login home.
00636      *
00637      *  @return bool TRUE if all went well, or FALSE on failure.
00638      *
00639      *  @access public
00640      *  @since Method available since Release 2.1
00641      */
00642     function ssh_copy_to($localpath, $remotepath)
00643     {
00644         global $debug;
00645 
00646         umask(0077);
00647         $tmpfname = tempnam($this->workdir, "copy-to-");
00648         chmod($tmpfname, 0700);
00649         putenv("DISPLAY=none:0.");
00650         putenv("SSH_ASKPASS=$tmpfname");
00651         $fp = fopen($tmpfname, "w");
00652         fputs($fp, "#!/bin/sh\necho $this->password\n");
00653         if (! $debug)  
00654             fputs($fp, "/bin/touch $tmpfname.called\n");
00655         else
00656             fputs($fp, "/bin/rm $tmpfname\n");
00657         fclose($fp);
00658         exec("$this->scp -pqrC $localpath $this->remote:$remotepath", $out, $status);
00659         if ($status == 0)
00660             return TRUE;
00661         else
00662             return FALSE;
00663     }
00664     
00665     /**
00666      *  Copy a file or directory from a remote source to a local destination
00667      *
00668      *  This function copies source to dest, where first of them is a
00669      * remote filespec and then comes a local filespec, both specified 
00670      * as normal system paths.
00671      *
00672      *  Both, local and remote paths may be absolute or relative.
00673      *
00674      *  If the original source is a directory, it will be copied
00675      * recursively to destination (hence easing file transfers).
00676      *
00677      *  The function returns TRUE on success or FALSE on failure.
00678      *
00679      * EFFICIENCY NOTICE:
00680      *
00681      *  The copy routines use 'scp' to do their actual work. Since
00682      * scp seems to be unable to hitchhike on the master channel,
00683      * we must do authetication for each copy operation (subroutine 
00684      * call). These routines are hence a lot more time-expensive 
00685      * than all the other ones.
00686      *
00687      *  You may want to consider whether you can group several
00688      * copies into one single call to reduce authentication 
00689      * overheads.
00690      *
00691      *  @see scp(1)
00692      *
00693      *  @param string remotepath   The origin remote path, either absolute or
00694      *                  relative to the login home. If it denotes a 
00695      *                  directory, the copy will be recursive.
00696      *
00697      *  @param string localpath    The local destination path, either
00698      *                  absolute or relative to the current working
00699      *                  directory.
00700      *
00701      *  @return bool TRUE if all went well, or FALSE on failure.
00702      *
00703      *  @access public
00704      *  @since Method available since Release 1.0
00705      */
00706     function ssh_copy_from($remotepath, $localpath)
00707     {
00708         global $debug;
00709 
00710         umask(0077);
00711         $tmpfname = tempnam($this->workdir, "copy-from-");
00712         chmod($tmpfname, 0700);
00713         putenv("DISPLAY=none:0.");
00714         putenv("SSH_ASKPASS=$tmpfname");
00715         $fp = fopen($tmpfname, "w");
00716         fputs($fp, "#!/bin/sh\necho $this->password\n");
00717         if (! $debug)  
00718             fputs($fp, "/bin/touch $tmpfname.called\n");
00719         else
00720             fputs($fp, "/bin/rm $tmpfname\n");
00721         fclose($fp);
00722         exec("$this->scp -pqrC $this->remote:$remotepath $localpath", $out, $status);
00723         if ($status == 0)
00724             return TRUE;
00725         else
00726             return FALSE;
00727     }
00728 
00729     /**
00730      *  Execute a single command remotely
00731      *
00732      *  Execute a single command remotely using ssh and 
00733      * display its output, optionally returning its exit 
00734      * status (like passthru)
00735      *
00736      *  This function is intended to be used as a one-time
00737      * all-at-once non-interactive execution mechanism which
00738      * will run the command remotely and display its output.
00739      *
00740      *  If you try to issue an interactive command using this
00741      * function, all you will get is unneccessary trouble. So
00742      * don't!
00743      *
00744      *  This might be done as well using a pipe on /tmp and
00745      * making the command 'cat' the pipe: when ssh runs, it
00746      * runs the command 'cat' on the pipe and hangs on read.
00747      *  Then we just need a thread to open the pipe, put the
00748      * password and close the pipe.
00749      *
00750      *  This other way the password is never wirtten down.
00751      * But, OTOH, the file life is so ephemeral that most
00752      * of the time it will only exist in the internal system
00753      * cache, so this approach is not that bad either.
00754      *
00755      *  @see passthru()
00756      *
00757      *  @param string command  The command to execute on the remote end
00758      *                      NOTE: if you want to use redirection, the
00759      *                      entire remote command line should be 
00760      *                      enclosed in additional quotes!
00761      *  @param integer status   Optional, this will hold the termination
00762      *                      status of SSH after invocation, which
00763      *                      should be the exit status of the remote
00764      *                      command or 255 if an error occurred
00765      *  @return void
00766      *
00767      *  @access public
00768      *  @since Method available since Release 1.0
00769      */
00770     function ssh_passthru($command, &$status)
00771     {
00772         global $debug;
00773 
00774         if ($debug) echo "status = $status\n";
00775         // go
00776         if (isset($status)) {
00777             if ($debug) echo "st: $this->ssh -x -t -t -S $this->mplex_socket $this->remote \"$command\"\n";
00778             passthru("$this->ssh -x -t -t -S $this->mplex_socket $this->remote \"$command\"", $status);
00779         }
00780         else {
00781             if ($debug) echo "~st: $this->ssh -x -t -t  -S $this->mplex_socket $this->remote \"$command\"\n";
00782             passthru("$this->ssh -x -t -t  -S $this->mplex_socket $this->remote \"$command\"");
00783         }
00784     }
00785     
00786     
00787     /**
00788      *  Execute a remote command using SSH
00789      *
00790      *  This function sort of mimics rexec(3) using SSH as the transport
00791      * protocol.
00792      *
00793      *  The function returns the exit status of the remote command, and
00794      * appends the remote job output to an optional argument.
00795      *
00796      *  This function is intended to be used as a one-time
00797      * all-at-once non-interactive execution mechanism which
00798      * will run the command remotely and return its output.
00799      *
00800      *  If you try to issue an interactive command using this
00801      * function, all you will get is unneccessary trouble. So
00802      * don't!
00803      *
00804      *  @param string command  The command to execute on the remote end
00805      *                      NOTE: if you want to use redirection, the
00806      *                      entire remote command line should be 
00807      *                      enclosed in additional quotes!
00808      *  @param string output   Optional, the collated (stdout+stderr) output 
00809      *                      of the remote command.
00810      *  @return integer status  will hold the termination
00811      *                      status of SSH after invocation, which
00812      *                      should be the exit status of the remote
00813      *                      command or 255 if an error occurred
00814      *
00815      *  @access public
00816      *  @since Method available since Release 1.0
00817      */
00818     function ssh_exec($command, &$out)
00819     {
00820         global $debug;
00821 
00822         umask(0077);
00823         $tmpfname = tempnam($this->workdir, 'exec');
00824         chmod($tmpfname, 0700);
00825         if ($debug) echo $tmpfname . "\n";
00826 
00827         exec("$this->ssh -x -t -t -S $this->mplex_socket $this->remote \"$command\"", $out, $retval);
00828         return $retval;
00829 
00830     }
00831     
00832     /**
00833      *  Open an SSH connection to a remote site with a shell to run 
00834      * interactive commands
00835      *
00836      *  Connects to a remote host and opens an interactive shell session
00837      * with NO controlling terminal.
00838      *
00839      *  This routine creates communication streams with the remote shell,
00840      * and stores all output (standard and error) of the connection into
00841      * two separate local log files (one for stdout and one for stderr).
00842      *
00843      *  Returns a process_control array which contains the process resource
00844      * ID and an the standard file descriptors which the caller may use to
00845      * interact with the remote shell.
00846      *
00847      * The process control array contains:
00848      *
00849      *  'process' -- the process resource for the newly created connection
00850      *
00851      *  'std_in' -- handle to the standard input of the new connection
00852      *
00853      *  'std_out' -- handle to standard output of the new connection
00854      *
00855      *  'std_err' -- handle to standard error of the new connection
00856      *
00857      *  'stdout_file' -- actual filename of the local log file for the
00858      *          new connection standard output
00859      *
00860      *  'stderr_file' -- actual filename of the local log file for the
00861      *          new connection standard error
00862      *
00863      *  @return mixed a process control associative array.
00864      *
00865      *  @access public
00866      *  @since Method available since Release 1.0
00867      */
00868     function ssh_open_shell()
00869     {   
00870         global $debug;
00871 
00872         // Open a child process with the 'proc_open' function. 
00873         //
00874         // Some tricks: we must open the connection using '-x' to disable
00875         // X11 forwarding, and use '-t -t' to avoid SSH generating an error
00876         // because we are not connected to any terminal.
00877         //
00878         // NOTE: if the web server is trusted remotely (i.e. it's SSH public 
00879         // key is accepted in ~user@host:.ssh/authorized_keys) then any 
00880         // password will do.
00881 
00882         // Prepare I/O
00883         umask(0077);
00884         if ($debug) {
00885             $child_stdout = tempnam($this->workdir, "open_sh-".getmypid()."-O-");
00886             $child_stderr = tempnam($this->workdir, "open_sh-".getmypid()."-E-");
00887         } else {
00888             $child_stdout = tempnam($this->workdir, "open_sh-");
00889             $child_stderr = tempnam($this->workdir, "open_sh-");
00890         }
00891         $descriptorspec = array(
00892             0 => array("pipe", "r"),  // connect child's stdin to the read end of a pipe
00893             1 => array("file", $child_stdout, "a"),  // connect child's stdout to the write end of a pipe
00894             2 => array("file", $child_stderr, "a")   // stderr is a pipe to read from
00895         );
00896         if ($debug) echo "$this->ssh -x -t -t -S $this->mplex_socket $this->remote<br />\n";
00897         $process = proc_open("$this->ssh -x -t -t -S $this->mplex_socket $this->remote", 
00898                          $descriptorspec,
00899                          $pipes);
00900         
00901         // check status
00902         if (!is_resource($process)) 
00903         {
00904             letal("SSH::connect", "cannot connect to the remote host");
00905             return;
00906         }
00907         if ($debug) echo "proc_open done<br />\n";
00908 
00909         // $pipes now looks like this:
00910         //   0 => writeable handle connected to child stdin
00911         
00912         // Open child's stdin and stdout
00913         $pipes[1] = fopen($child_stdout, "r");
00914         $pipes[2] = fopen($child_stderr, "r");
00915         
00916         // Should we leave this to the user?
00917         // set to non-blocking and avoid having to call fflush
00918         //stream_set_blocking($pipes[0], FALSE);
00919         //stream_set_blocking($pipes[1], FALSE);
00920         //stream_set_blocking($pipes[2], FALSE);
00921         stream_set_write_buffer($pipes[0], 0);
00922         stream_set_write_buffer($pipes[1], 0);
00923         stream_set_write_buffer($pipes[2], 0);
00924 
00925         // We now have a connection to the remote SSH
00926         // Server which we may use to send commands/receive output
00927         $p = array('process' => $process
00928                     ,'std_in' => $pipes[0]
00929                     ,'std_out' => $pipes[1]
00930                     ,'std_err' => $pipes[2] 
00931                     ,'stdout_file' => $child_stdout
00932                     ,'stderr_file' => $child_stderr
00933                    );
00934         if ($debug)  {
00935             echo "process descriptor array is \n";
00936             print_r($p);
00937         }
00938         return $p;
00939     }
00940     
00941     /**
00942      *  Open an SSH connection to run an interactive command on a remote
00943      * site
00944      *
00945      *  Connects to a remote host and runs an interactive command
00946      * with NO controlling terminal.
00947      *
00948      *  This routine creates communication streams with the remote shell,
00949      * and stores all output (standard and error) of the connection into
00950      * two separate local log files (one for stdout and one for stderr).
00951      *
00952      *  Returns a process_control array which contains the process resource
00953      * ID and an the standard file descriptors which the caller may use to
00954      * interact with the remote shell.
00955      *
00956      * The process control array contains:
00957      *
00958      *  'process' -- the process resource for the newly created connection
00959      *
00960      *  'std_in' -- handle to the standard input of the new connection
00961      *
00962      *  'std_out' -- handle to standard output of the new connection
00963      *
00964      *  'std_err' -- handle to standard error of the new connection
00965      *
00966      *  'stdout_file' -- actual filename of the local log file for the
00967      *          new connection standard output
00968      *
00969      *  'stderr_file' -- actual filename of the local log file for the
00970      *          new connection standard error
00971      *
00972      *  @param  string command to be executed interactively on the remote end
00973      *
00974      *  @return mixed a process control associative array.
00975      *
00976      *  @access public
00977      *  @since Method available since Release 1.0
00978      */
00979     function ssh_open_command($command)
00980     {   
00981         global $debug;
00982 
00983         // Open a child process with the 'proc_open' function. 
00984         //
00985         // Some tricks: we must open the connection using '-x' to disable
00986         // X11 forwarding, and use '-t -t' to avoid SSH generating an error
00987         // because we are not connected to any terminal.
00988         //
00989         // NOTE: if the web server is trusted remotely (i.e. it's SSH public 
00990         // key is accepted in ~user@host:.ssh/authorized_keys) then any 
00991         // password will do.
00992 
00993         // Prepare I/O
00994         umask(0077);
00995         if ($debug) {
00996             $child_stdout = tempnam($this->workdir, "open_cmd-".getmypid()."-1-");
00997             $child_stderr = tempnam($this->workdir, "open_cmd-".getmypid()."-2-");
00998         } else {
00999             $child_stdout = tempnam($this->workdir, "open_cmd-");
01000             $child_stderr = tempnam($this->workdir, "open_cmd-");
01001         }
01002         $descriptorspec = array(
01003             0 => array("pipe", "r"),  // connect child's stdin to the read end of a pipe
01004             #1 => array("pipe", "a"),  // connect child's stdout to the write end of a pipe
01005             #2 => array("pipe", "a")   // stderr is a pipe to read from
01006             1 => array("file", $child_stdout, "a"),
01007             2 => array("file", $child_stderr, "a")
01008         );
01009 
01010         if ($debug) echo "$this->ssh -x -t -t -S $this->mplex_socket $this->remote $command<br />\n";
01011         $process = proc_open("$this->ssh -x -t -t -S $this->mplex_socket $this->remote \"$command\"", 
01012                          $descriptorspec,
01013                          $pipes);
01014         
01015         // check status
01016         if (!is_resource($process)) 
01017         {
01018             letal("SSH::connect", "cannot connect to the remote host");
01019             return;
01020         }
01021         if ($debug) echo "proc_open done<br />\n";
01022 
01023         // $pipes now looks like this:
01024         //   0 => writeable handle connected to child stdin
01025         
01026         // Open child's stdin and stdout
01027         $pipes[1] = fopen($child_stdout, "r");
01028         $pipes[2] = fopen($child_stderr, "r");
01029         
01030         // Should we leave this to the user?
01031         // set to non-blocking and avoid having to call fflush
01032         #stream_set_blocking($pipes[0], FALSE);
01033         #stream_set_blocking($pipes[1], FALSE);
01034         #stream_set_blocking($pipes[2], FALSE);
01035         stream_set_write_buffer($pipes[0], 0);
01036         stream_set_write_buffer($pipes[1], 0);
01037         stream_set_write_buffer($pipes[2], 0);
01038 
01039         // We now have a connection to the remote SSH
01040         // Server which we may use to send commands/receive output
01041         $p = array('process' => $process
01042                     ,'std_in' => $pipes[0]
01043                     ,'std_out' => $pipes[1]
01044                     ,'std_err' => $pipes[2] 
01045                     ,'stdout_file' => $child_stdout
01046                     ,'stderr_file' => $child_stderr
01047                    );
01048         if ($debug)  {
01049             echo "process descriptor array is \n";
01050             print_r($p);
01051         }
01052         return $p;
01053     }
01054     
01055     /**
01056      * Get output until we reach a given regular expression
01057      *
01058      *  @note EXPERIMENTAL, requires more thought and experience.
01059      */
01060     function ssh_out_expect($p, $expr="^# ")
01061     {
01062         do {
01063                 flush();
01064                 fseek($p["std_out"], $last);
01065                 $line = fgets($p["std_out"], 1024);
01066                  #echo ">> ".$line;
01067                 $last = ftell($p["std_out"]);
01068         } while ((! feof($p["std_out"]) ) || (! ereg($expr, $line)));
01069     }
01070 
01071     /**
01072      * Close an SSH interactive session
01073      *
01074      *  This method terminates a previously open interactive remote 
01075      * session. It will send a termination notification to the
01076      * remote end, close the connection with control and communication
01077      * streams, and terminate the local control process.
01078      *
01079      *  Copies of the log files that contain the output and error
01080      * of the communication are left out for later reference and 
01081      * local peruse. If you don't need them any longer, you may
01082      * delete them or just leave them around until the class destructor
01083      * is called (which will remove all session traces),
01084      *
01085      *  @param mixed p an associative array with the description of the interactive
01086      *          session control process, obtained by a previous call to one
01087      *          of the interactive session creation methods ssh_open_shell()
01088      *          or ssh_open_command().
01089      *
01090      *  @return integer the exit status of the remote interactive session.
01091      *
01092      *  @access public
01093      *  @since Method available since Release 1.0
01094      */
01095     function ssh_close($p)
01096     {
01097         global $debug;
01098         
01099             fwrite($p['std_in'], "\n");
01100             fwrite($p['std_in'], "logout\n");
01101             fflush($p['std_in']);
01102             fclose($p['std_in']); fclose($p['std_out']); fclose($p['std_err']);
01103             if ($debug) echo "pipes/files closed\n";
01104             // XXX we should delete the log files here...
01105             return proc_close($p['process']);
01106     }
01107     
01108 #    if ($php_version >= 5)
01109 #    {
01110 #       /**
01111 #        * send a signal to a running ssh_open_* process
01112 #        */
01113 #       function ssh_signal($p, $signal)
01114 #       {
01115 #           return proc_terminate($p['process'], $signal);
01116 #       }
01117 #       /**
01118 #        * get info about a running ssh_open_* process
01119 #        */
01120 #       function ssh_get_status($p)
01121 #       {
01122 #           return proc_get_status($p['process']);
01123 #       }
01124 #    }
01125     
01126     /**
01127      *  Execute a remote command and keep an unidirectional stream
01128      * contact with it.
01129      *
01130      *  This routine mimics 'popen()' but uses ssh to connect to
01131      * a remote host and run the requested command: in other words,
01132      * it opens a pipe to a remotely executed command. This pipe is
01133      * unidirectional, with the communications direction controlled
01134      * by a method parameter.
01135      *
01136      *  @see popen() for more details.
01137      *
01138      *  @param string command is the command to execute on the remote end
01139      *
01140      *  @param string mode specifies the communications direction for the 
01141      *          pipe: if set to "r" (read), then we will be able to
01142      *          collect command output only; if set to "w" (write)
01143      *          then we may only send input to the remote command.
01144      *
01145      *  @return resource a handle to the unidirectional communication stream,
01146      *          similar to that returned by fopen(), or FALSE on
01147      *          failure. This handle must be closed with ssh_pclose().
01148      *
01149      *  @access public
01150      *  @since Method available since Release 1.0
01151      */
01152     function ssh_popen($command, $mode)
01153     {
01154         global $debug;
01155 
01156         // go
01157         return popen("$this->ssh -x -t -t -S $this->mplex_socket $this->remote \"$command\"", $mode);
01158     }
01159     
01160     /**
01161      * Close a piped remote execution command control pipe.
01162      *
01163      *  This routine accepts as input the handle for the control stream
01164      * of a remote command and closes it, terminating the command as well.
01165      * The handle must be valid and obtained through a call to ssh_popen().
01166      *
01167      *  @param resource f is the file handle associated with the pipe control stream
01168      *
01169      *  @return integer the termination status of the command that was run.
01170      *
01171      *  @access public
01172      *  @since Method available since Release 1.0
01173      */
01174     function ssh_pclose($f)
01175     {
01176         return pclose($f);
01177     }
01178 
01179     //}}}
01180 }
01181 
01182 /*
01183  * Local variables:
01184  * tab-width: 4
01185  * c-basic-offset: 4
01186  * c-hanging-comment-ender-p: nil
01187  * End:
01188  */
01189 
01190 ?>

Generated on Wed May 25 19:14:05 2005 for php::ssh by doxygen 1.3.6