[ Index ]

PHP Cross Reference of phpSSH

/ -> ssh-files.php (source)

   1  <?php
   2  
   3  /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
   4  
   5  /**
   6   * ssh.php -- a class to execute remote commands over SSH
   7   *
   8   *    This file contains the files-based implementation of the SExec
   9   * class. This implementation relies on the usage of regular temporary
  10   * files to communicate with the remote end, thus avoiding several
  11   * drawbacks (mainly deadlocks) associated with pipes.
  12   *
  13   *    The SExec class provides methods to launch and control jobs and
  14   * transfer files over SSH.
  15   *
  16   * PHP versions 4 and 5
  17   *
  18   * LICENSE:
  19   * This library is free software; you can redistribute it and/or
  20   * modify it under the terms of the GNU Lesser General Public
  21   * License as published by the Free Software Foundation; either
  22   * version 2.1 of the License, or (at your option) any later version.
  23   * 
  24   * This library is distributed in the hope that it will be useful,
  25   * but WITHOUT ANY WARRANTY; without even the implied warranty of
  26   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  27   * Lesser General Public License for more details.
  28   * 
  29   * You should have received a copy of the GNU Lesser General Public
  30   * License along with this library; if not, write to the Free Software
  31   * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  32   *
  33   * @category    Net
  34   * @package    SExec
  35   * @author     José R. Valverde <jrvalverde@acm.org>
  36   * @copyright     José R. Valverde <jrvalverde@es.embnet.org>
  37   * @license    doc/lic/
  38   * @version    CVS: $Id: ssh-files.php,v 1.6 2005/05/25 16:02:40 netadmin Exp $
  39   * @link    http://savannah.cern.ch/projects/GridGRAMM
  40   * @see        ssh(1), scp(1)
  41   * @since    File available since Release 1.0
  42   */
  43  
  44  
  45  /** 
  46   * Allow for remote execution of commands using SSH
  47   *
  48   *    The SExec class provides a number of facilities for remote
  49   * command execution using SSH.
  50   *
  51   *    The name SExec comes after "rexec" (the remote execution library)
  52   * and the "exec" facilities available under PHP. As a matter of fact,
  53   * we try to mimic to some extent the execution facilities offered by
  54   * PHP over SSH: thus you will find ssh_popen() akin to popen(), etc.
  55   *
  56   * <b>RATIONALE</b>
  57   *
  58   *    The reason for this class is to allow executing code on a remote
  59   * back-end avoiding MITM spoofs in your communications. This allows you
  60   * to provide a web front-end (possibly redundant) and call a remote
  61   * back-end to execute the job.
  62   *
  63   *    Furthermore, you may have fallback features where if execution
  64   * on a remote back-end fails you can restart the command on a fallback
  65   * remote host, increasing reliability.
  66   *
  67   * <b>DEPENDENCIES</b>
  68   *
  69   *    The class relies on an underlying installation of SSH. It has
  70   * been tested with OpenSSH on Linux, but should work on other systems
  71   * with OpenSSH as well.
  72   *
  73   *    Further, the class in its current inception relies on OpenSSH
  74   * version being greater than 3.8. If you have an older SSH, please use
  75   * version 1.0 of this class instead.
  76   *
  77   * <b>DESIGN RATIONALE</b>
  78   *
  79   *    The reasons for the choices taken are simple: we might have
  80   * relied on an SSH library (like libSSH) and integrated it with PHP,
  81   * but then, any weakness/bug/change on said library would require a
  82   * recompilation of the library and PHP. This is a serious inconvenience.
  83   * More to that, it would require the maintenance of two simultaneous
  84   * SSH installations, viz. OpenSSH and the library, duplicating the work
  85   * of tracking security/bug issues.
  86   *
  87   *    By using the underlying SSH commands, we become independent of
  88   * them: if anything is discovered, you just have to update your system
  89   * SSH, and nothing else. Otherwise you would have a dependency on SSH
  90   * to remember, which is always forgotten. This way we avoid getting out
  91   * of sync with the system's SSH.
  92   *
  93   *    Better yet: this easies development, making this class a lot
  94   * simpler to write, understand, maintain and debug.
  95   *
  96   *    Finally, the dependency on SSH being OpenSSH 3.8 or greater is
  97   * due to efficiency reasons. Establishing an SSH connection is costly
  98   * in time. If you are going to make many, this would impose a heavy
  99   * cost on your scripts. We routinely launch several thousand remote 
 100   * jobs, and authentication delays soon proved unacceptable.
 101   *
 102   *    OpenSSH 3.8 introduced the possibility of sharing a single SSH
 103   * channel between many "connections". This means that only the first
 104   * (or master) instance (which will provide the shared channel) needs 
 105   * to authenticate, hence saving significant time.
 106   *
 107   *    The constructor then creates a master channel, leaves it idle
 108   * all the object's lifetime and closses it at the end. This channel
 109   * might be used as well, but we felt it wasn't such a big loss to keep
 110   * it idle, and furthermore, being the master, we didn't want to risk
 111   * getting into any trouble that might close it prematurely. So it stands.
 112   *
 113   *    All other routines (which actually do the work) simply hijack on
 114   * the master channel, hence avoiding the costly authentication step (and
 115   * executing significantly faster). The only exception are the "COPY" 
 116   * routines, which can not hijack the master channel and hence must do
 117   * authentication every time.
 118   *
 119   *    One more detail: some methods allow for interactive communication
 120   * with the remote end. We have simply used a terminal-less connection
 121   * for them, using regular files as the intermediate communication channels.
 122   * A pipe implementation is also possible, and works as well, but we have
 123   * found that dealing with pipes is tricky and error-prone, while using
 124   * files is simple and intuitive, so we opted for using files.
 125   *
 126   *    The difference has to do with the way you communicate with the
 127   * other end: using pipes you may block on read and/or write, and so
 128   * may the other end. Since there may occur errors in the process, that
 129   * implies that getting into a deadlock is trivial. Just picture this
 130   * scenarios:
 131   *
 132   *     You send a command -> the remote ends starts the command and
 133   * prompts for input on stdout, hangs reding on stdin -> you read the 
 134   * prompt and send the input -> the remote end wakes and processes it.
 135   *
 136   *    You send a command -> the remote end fails, logs an error on
 137   * stderr, gets back the system prompt and hangs on reading stdin -> you
 138   * notice the prompt and read stderr... since you can't predict the 
 139   * length of the error message you must empty the pipe... and when doing it
 140   * you hang after reading the last char... -> deadlock
 141   *
 142   *    You send a command -> the remote end fails, logs an error on stderr,
 143   * gets back the system prompt and hangs on reading stdin -> you don't read
 144   * stderr to avoid hanging, so submit a new command... this goes on and on
 145   * until the remote side's stderr buffer fills, then the remote side locks
 146   * waiting for you to read stderr -> you can't know it hang, so you try
 147   * to submit a new command, and hang on writing waiting for the other end
 148   * to read your command -> deadlock
 149   *
 150   *     More scenarios are possible, and since you (or the other side)
 151   * can't predict what's going to happen, it is very tricy to avoid them.
 152   *
 153   *    Now, using files, you don't have that problem: whenever you reach
 154   * the current end-of-file, you get an EOF, no need to hang waiting for
 155   * the other side to fill it in with data. The other side doesn't hang on
 156   * writing unless your disk space fills up. It's a lot simpler. 
 157   *
 158   *    Your problem with files is continuing reads after new data becomes
 159   * available: the safest way is to call flush() before reading and seeking
 160   * to the last position read to avoid having to re-read everything (which
 161   * implies that after finishing reading you must ftell() your position.
 162   *
 163   *    See the included test script for examples.
 164   *
 165   * <b>CUSTOMIZATION</b>
 166   *
 167   *    You <i>must</i> state to the class where your SSH executables (ssh and
 168   * scp) are located. This allows you to have them placed anywhere, but
 169   * also implies the responsability of using full pathnames to reduce
 170   * hacking dangers. It also allows you to use/test a new SSH implementation
 171   * installed in a non-standard place before switching to it, or even to
 172   * keep various SSH installations on the system (e.g. if the system's
 173   * SSH is not up-to-date, you may install one on your home and use it).
 174   *
 175   *    You may also indicate where to store temporary files. This must 
 176   * be a directory followed by a prefix to use when creating a temporal
 177   * directory. The parent directory must be writeable by the user who runs 
 178   * the class (usually it will be run by apache, www or some such). Most commonly 
 179   * the parent directory will be /tmp or $DocumentRoot/tmp or something similar.
 180   *
 181   *    The directory+prefix you state will be used to create a unique
 182   * temporary work directory for each object instantiated. Examples of
 183   * a valid specifications are "/tmp/phpSsh-" or "/tmp/". When the object is 
 184   * instantiated, a random string will be appended to this value to create
 185   * the actual temporary directory name.
 186   *    
 187   *    The reason for allowing specifying a prefix is so that debugging
 188   * may be easier by facilitating identification of temporaries generated
 189   * by this class.
 190   *
 191   * <b>DEBUGGING</b>
 192   *
 193   *    The class comes with extensive debugging aids. To enable them,
 194   * just set a global variable called $debug to TRUE. This will output
 195   * abundant debugging information and leave copies of communication log
 196   * files for your reference.
 197   *
 198   *    Additionally, there is a sample demo script that shows how to
 199   * use this class and may help you debug it. This script is included 
 200   * in the distribution (or should be) as 'ssh_debug.php'. See notes
 201   * and comments within it for more details.
 202   *
 203   * @category       Net
 204   * @package    SExec
 205   * @author     José R. Valverde <jrvalverde@acm.org>
 206   * @copyright     José R. Valverde <jrvalverde@es.embnet.org>
 207   * @license    doc/lic/
 208   * @version        Release: @package_version@
 209   * @link    http://savannah.cern.ch/projects/GridGRAMM
 210   * @see        ssh(1), scp(1)
 211   * @since    File available since Release 1.0
 212  
 213   */
 214  class SExec {
 215  
 216      // {{{ properties
 217  
 218      /**
 219       * The current version of the class
 220       *
 221       * @var string
 222       * @access public
 223       */
 224      var $version="2.0";
 225      
 226      /**
 227       * remote endpoint ([user@]host[:port])
 228       *
 229       * @var string
 230       * @access private
 231       */
 232      var $remote;
 233      
 234      /**
 235       * remote password 
 236       *
 237       * @var string
 238       * @access private
 239       */
 240      var $password;
 241  
 242      /**
 243       * location of ssh program
 244       *
 245       * @var string
 246       * @access private
 247       */
 248      var $ssh = "/usr/bin/ssh";
 249      
 250      /**
 251       * location of scp program
 252       *
 253       * @var string
 254       * @access private
 255       */
 256      var $scp = "/usr/bin/scp";
 257      
 258      /**
 259       * tmp. dir prefix specification
 260       *
 261       * @var string
 262       * @access private
 263       */
 264      var $workdir = "/tmp/phpSsh";
 265      
 266      /**
 267       * name of multiplexing socket
 268       *
 269       * @var string
 270       * @access private
 271       */
 272      var $mplex_socket = "/tmp/ssh.mplex";
 273      
 274      /**
 275       * handle to process controlling the master channel
 276       *
 277       * @var string
 278       * @access private
 279       */
 280      var $master;
 281      
 282      /**
 283       * stdin of process controlling the master channel
 284       *
 285       * @var string
 286       * @access private
 287       */
 288      var $master_input;
 289  
 290      //}}}
 291      
 292      //{{{   instantiation
 293      
 294      /** Class constructor.
 295       *
 296       *    Generate a new instance of a remote execution environment.
 297       * The object returned allows you to invoke commands to be executed
 298       * remotely in a way similar to PHP exec commands (popen, proc_open...)
 299       * over SSH (so that your communications can be secure).
 300       *
 301       *    You may specify a remote endpoint and a password, a remote endpoint
 302       * alone, or nothing at all.
 303       *
 304       *    If you provide a remote endpoint and password they are used to drive
 305       * the communications and execute your commands.
 306       *
 307       *    If no password is provided, then a default of "xxyzzy" (the canonical
 308       * computer magic word) is used. Unless this is your password (not 
 309       * recommended), this means that the default password is useless unless
 310       * you are working in a trusted environment where it is not needed and
 311       * ignored. That may be the case if you enable trusting mechanisms with
 312       * .shosts/.rhosts or passphraseless RSA/DSA authentication. Not that
 313       * we endorse them either, but in these cases any password provided will
 314       * be ignored and it doesn't make sense to provide a real one: xxyzzy
 315       * can do as well as any other.
 316       *
 317       *    If no password and no remote end is provided, then "localhost" is
 318       * used as the remote end, assuming no password is required (as described
 319       * above). This is only useful if localhost is trusted, and you have reasons
 320       * to use SSH internally... Some people does.
 321       *
 322       *    Regarding the remote end specification, it can be any valid single-string
 323       * SSH remote end description: the basic format is
 324       *
 325       *    [username@]remote.host[:port]
 326       *
 327       *    - "username" is the remote user name to log in as. It is optional. If provided, 
 328       *       it must be separated from the remote host by an "@" sign. If it is not 
 329       *    provided, the remote username is assumed to be the same as the current local
 330       *    one.
 331       *
 332       *    - "remote.host" is a valid host specification, either a numeric IP address
 333       *    or a valid host name (which may require a full name or not depending on
 334       *    your settings).
 335       *
 336       *     - "port" is the remote port where SSH is listening and which we want to
 337       *    connect to. It is optional, and if provided, must follow the remote host
 338       *    specification separated from it by a colon ":". If not provided, the
 339       *    default port (22) is used.
 340       *
 341       *    Examples of remote host specifications are "user@host.example.net:22",
 342       * "someone@host:22", "host.example.net:22", "host:22", 
 343       * "somebody@host.example.net", "user@host", "host.example.net", "host".
 344       *
 345       * Here is an example of how to use this constructor:
 346       * <code>
 347       *  require_once 'ssh.php';
 348       *
 349       *  $remote = "jruser@example.com";
 350       *  $password = "PASSWORD";
 351       *
 352       *  $rmt = new SExec($remote, $password);
 353       *   if (! $rmt)
 354       *      echo "Couldn't connect to $remote\n";
 355       * </code>
 356       *
 357       *  @param string remote   The remote end to run the command, in
 358       *                  the form 'user@host:port' (you may
 359       *                        omit the 'user@' or ':port' parts
 360       *                        if the default values [i.e. same user
 361       *                        or standard port] are OK).
 362       *
 363       *  @param string password The remote password. Note that if direct
 364       *                  RSA/DSA/.shosts/.rhosts login is enabled
 365       *                  then the password will be ignored as
 366       *                  SSH should not run the ASKPASS command).
 367       *
 368       *    @return SExec|false a new connection object with the remote end or
 369       *                        FALSE if the connection could not be established.
 370       *
 371       *    @access public
 372       *  @since Method available since Release 1.0
 373       */
 374      function SExec($remote="localhost", $password="xxyzzy")
 375      {
 376          global $debug;
 377      
 378      if ($debug) echo "\nCreating a new SExec\n";
 379          $this->remote = $remote;
 380      $this->password = "$password";
 381      umask(0077);
 382      /* DESIGN
 383       * In order to increase efficiency, we will create a master channel
 384       * on class instantiation. The master channel should be closed by a
 385       * corresponding class destructor!
 386       *
 387       * Creating a master channel has the advantage that subsequent SSH
 388       * connections will use it and avoid repeating the slow authentication
 389       * process: in other words, they will go much, much faster.
 390       */
 391       
 392      // first we must generate a unique UNIX socket address or we'll fail
 393      // We use a tricky trick: generate two random numbers and use them;
 394      // this is tricky since there might be a problem, but with very low
 395      // probability. BUT IT MAY STILL FAIL: there's a race condition between
 396      // the end of the while and the subsequent if.
 397      do {
 398          mt_srand((double)microtime()*1000000 ) .
 399          $this->workdir = "/tmp/phpSsh-" . mt_rand() .".". mt_rand();
 400          if ($debug) echo "\nSExec: trying $this->workdir/ ...";
 401          // CAUTION: this is potentially an endless loop (albeit with very
 402          // low probability) if every possible file did exist.
 403      }
 404      while (file_exists($this->workdir));
 405      if (mkdir($this->workdir) == FALSE) {
 406          // we can't continue. How can we cancel this?
 407          // try these and then check what happens
 408          unset($this);
 409          $this = NULL;
 410          return FALSE;
 411      }
 412      else 
 413          if ($debug) echo " OK\n";
 414      // Now we have a place to put the socket... Mmm...
 415      // Come to think of it, we have a place to put ANY temporary
 416      // for the class... 
 417      // XXX Maybe we can change everywhere else to use this?
 418      $this->mplex_socket = $this->workdir."/mplex_socket";
 419      
 420      // Finally we can call SSH -M
 421      // Create SSH_ASKPASS script to provide the password
 422      $tmpfname = tempnam($this->workdir, 'SExec-');
 423      chmod($tmpfname, 0700);
 424      putenv("DISPLAY=none:0.");
 425      putenv("SSH_ASKPASS=$tmpfname");
 426      $fp = fopen($tmpfname, "w");
 427      fputs($fp, "#!/bin/sh\necho $this->password\n");
 428      if (!$debug) 
 429          fputs($fp, "/bin/touch $tmpfname.called\n");
 430      else
 431          fputs($fp, "/bin/rm -f $tmpfname\n");
 432      fclose($fp);
 433      
 434      // OK, we are ready. Now let's open a master shell
 435      $child_stdout = tempnam($this->workdir, "open_sh-O-");
 436      $child_stderr = tempnam($this->workdir, "open_sh-E-");
 437      $descriptorspec = array(
 438          0 => array("pipe", "r"),  // connect child's stdin to the read end of a pipe
 439          1 => array("file", $child_stdout, "a"),  // connect child's stdout to the write end of a pipe
 440          2 => array("file", $child_stderr, "a")   // stderr is a pipe to read from
 441      );
 442  
 443      if ($debug) echo "$this->ssh -x -t -t ".
 444               "-M -S $this->mplex_socket " .
 445               "$this->remote\n";
 446      $this->master = proc_open("$this->ssh -x -t -t ".
 447               "-M -S $this->mplex_socket " .
 448               "$this->remote",
 449               $descriptorspec,
 450               $pipes);
 451      if ((! is_resource($this->master)) || ($this->master == FALSE)) {
 452          putenv("SSH_ASKPASS=dummy");
 453          unset($this);
 454          $this = NULL;
 455          return FALSE;
 456      }
 457      // we do not need to worry about the output log files, just the
 458      // input pipe for logout
 459      $this->master_input = $pipes[0];
 460      
 461      // Before going ahead, we need to ensure the control shell 
 462      // has started: wait for the socket to become available
 463      // note: there should be a timeout here to avoid a possibly
 464      // infinite loop XXX
 465      do {
 466          if ($debug) echo "waiting 0.1 sec\n";
 467          usleep(100000);    // wait 0.1 seconds
 468      } while (! file_exists($this->mplex_socket));
 469  
 470      // and now we must register a destructor for the class
 471      // that will close the connection.
 472      //register_shutdown_function($this->destruct());
 473      
 474      return $this;
 475      }
 476      
 477      /** Class destructor
 478       *
 479       *    Destroy all working processes and data streams and structures
 480       * used by an instance of this class.
 481       *
 482       *    This method will send a termination message to the other end
 483       * of the master channel, close the control stream of the master
 484       * channel and terminate its controlling process, finally unsetting
 485       * the object and setting the object handle to NULL.
 486       *
 487       *    If a global $debug is not set to TRUE, then it will also remove
 488       * all communication traces of this object: i.e. all log files for
 489       * interactive and master sessions, communications socket, etc...
 490       *
 491       *    If global $debug is set to TRUE, then a copy of all log files
 492       * created during the lifetime of the object will be left on a
 493       * temporary directory for your perusal and reference.
 494       *
 495       *    @return integer exit status of the master channel control process.
 496       *
 497       *    @access public
 498       *  @since Method available since Release 1.0
 499       */
 500      function destruct()
 501      {
 502          global $debug;
 503      
 504      if ($debug) echo "\nDestroying SExec master\n";
 505      if ($debug) print_r($this);
 506      if ($debug) echo "sending logout\n";
 507      // log out master process
 508      fputs($this->master_input, "\n\nlogout\n\n");
 509      // close master stdin
 510      fclose($this->master_input);
 511      // close master process
 512      $ret = proc_close($this->master);
 513      // remove temporaries
 514          if (! $debug) system("/bin/rm -rf $this->workdir");
 515      // utterly destroy this instance
 516      unset($this);
 517      $this = NULL;
 518      return $ret;
 519      }    
 520      
 521      //}}}
 522      
 523      //{{{ methods
 524      /**
 525       *  Copy a file or directory from one source to a destination
 526       *
 527       *  This function copies source to dest, where one of them is a
 528       * local filespec and the other a remote filespec of the form
 529       * [user@]host:path
 530       *
 531       *  If the original source is a directory, it will be copied
 532       * recursively to destination (hence easing file transfers).
 533       *
 534       *  The function returns TRUE on success or FALSE on failure.
 535       *
 536       * <b>EFFICIENCY NOTICE:</b>
 537       *
 538       *    The copy routines use 'scp' to do their actual work. Since
 539       * scp seems to be unable to hitchhike on the master channel,
 540       * we must do authentication for each copy operation (subroutine 
 541       * call). These routines are hence a lot more time-expensive 
 542       * than all the other ones.
 543       *
 544       *    You may want to consider whether you can group several
 545       * copies into one single call to reduce authentication 
 546       * overheads.
 547       *
 548       *    @note DEPRECATED (inconsistent with the class)
 549       *
 550       *    @see scp(1)
 551       *
 552       *    @param string origin    The origin path, of the form
 553       *                    [user@][host][:port]path
 554       *                    You may omit the optional sections if
 555       *                    the default values (local username, local
 556       *                    host, standard SSH port) are OK
 557       *
 558       *    @param string destination    The destination path, of the form
 559       *                    [user@][host][:port:]path
 560       *                    You may omit the optional sections if
 561       *                    the default values (local username, local
 562       *                    host, standard SSH port) are OK
 563       *
 564       *    @param string password    The password to use to connect to the remote
 565       *                    end of the copy (be it the origin or the
 566       *                    destination, it's all the same). If connection
 567       *                    is automatic by some means (.shosts or RSA/DSA
 568       *                    authentication) then it should be ignored and
 569       *                    any password should do.
 570       *
 571       *    @return bool TRUE if all went well, or FALSE on failure.
 572       *
 573       *    @access public
 574       *  @since Method available since Release 1.0
 575       *    @deprecated Method deprecated as of Release 2.1
 576       */
 577      function ssh_copy($origin, $destination, $password)
 578      {
 579          global $debug;
 580  
 581      umask(0077);
 582      $tmpfname = tempnam($this->workdir, "copy-");
 583      chmod($tmpfname, 0700);
 584      putenv("DISPLAY=none:0.");
 585      putenv("SSH_ASKPASS=$tmpfname");
 586      $fp = fopen($tmpfname, "w");
 587      fputs($fp, "#!/bin/sh\necho $password\n");
 588      if (! $debug)  
 589          fputs($fp, "/bin/touch $tmpfname.called\n");
 590      else
 591          fputs($fp, "/bin/rm $tmpfname\n");
 592      fclose($fp);
 593      exec("$this->scp -pqrC $origin $destination", $out, $status);
 594      if ($status == 0)
 595          return TRUE;
 596      else
 597          return FALSE;
 598      }
 599  
 600  
 601      /**
 602       *  Copy a file or directory from a local source to a remote destination
 603       *
 604       *  This function copies source to dest, where first of them is a
 605       * local filespec and then comes a remote filespec as a normal
 606       * system path.
 607       *
 608       *    Both, local and remote paths may be absolute or relative.
 609       *
 610       *  If the original source is a directory, it will be copied
 611       * recursively to destination (hence easing file transfers).
 612       *
 613       *  The function returns TRUE on success or FALSE on failure.
 614       *
 615       * <b>EFFICIENCY NOTICE:</b>
 616       *
 617       *    The copy routines use 'scp' to do their actual work. Since
 618       * scp seems to be unable to hitchhike on the master channel,
 619       * we must do authetication for each copy operation (subroutine 
 620       * call). These routines are hence a lot more time-expensive 
 621       * than all the other ones.
 622       *
 623       *    You may want to consider whether you can group several
 624       * copies into one single call to reduce authentication 
 625       * overheads.
 626       *
 627       *    @see scp(1)
 628       *
 629       *    @param string localpath    The origin local path, either absolute or
 630       *            relative to the current working directory. 
 631       *            If it denotes a directory, the copy will 
 632       *            be recursive.
 633       *
 634       *    @param string remotepath   The destination path, either
 635       *            absolute or relative to the login home.
 636       *
 637       *    @return bool TRUE if all went well, or FALSE on failure.
 638       *
 639       *    @access public
 640       *  @since Method available since Release 2.1
 641       */
 642      function ssh_copy_to($localpath, $remotepath)
 643      {
 644          global $debug;
 645  
 646      umask(0077);
 647      $tmpfname = tempnam($this->workdir, "copy-to-");
 648      chmod($tmpfname, 0700);
 649      putenv("DISPLAY=none:0.");
 650      putenv("SSH_ASKPASS=$tmpfname");
 651      $fp = fopen($tmpfname, "w");
 652      fputs($fp, "#!/bin/sh\necho $this->password\n");
 653      if (! $debug)  
 654          fputs($fp, "/bin/touch $tmpfname.called\n");
 655      else
 656          fputs($fp, "/bin/rm $tmpfname\n");
 657      fclose($fp);
 658      exec("$this->scp -pqrC $localpath $this->remote:$remotepath", $out, $status);
 659      if ($status == 0)
 660          return TRUE;
 661      else
 662          return FALSE;
 663      }
 664      
 665      /**
 666       *  Copy a file or directory from a remote source to a local destination
 667       *
 668       *  This function copies source to dest, where first of them is a
 669       * remote filespec and then comes a local filespec, both specified 
 670       * as normal system paths.
 671       *
 672       *    Both, local and remote paths may be absolute or relative.
 673       *
 674       *  If the original source is a directory, it will be copied
 675       * recursively to destination (hence easing file transfers).
 676       *
 677       *  The function returns TRUE on success or FALSE on failure.
 678       *
 679       * EFFICIENCY NOTICE:
 680       *
 681       *    The copy routines use 'scp' to do their actual work. Since
 682       * scp seems to be unable to hitchhike on the master channel,
 683       * we must do authetication for each copy operation (subroutine 
 684       * call). These routines are hence a lot more time-expensive 
 685       * than all the other ones.
 686       *
 687       *    You may want to consider whether you can group several
 688       * copies into one single call to reduce authentication 
 689       * overheads.
 690       *
 691       *    @see scp(1)
 692       *
 693       *    @param string remotepath   The origin remote path, either absolute or
 694       *            relative to the login home. If it denotes a 
 695       *            directory, the copy will be recursive.
 696       *
 697       *    @param string localpath    The local destination path, either
 698       *            absolute or relative to the current working
 699       *            directory.
 700       *
 701       *    @return bool TRUE if all went well, or FALSE on failure.
 702       *
 703       *    @access public
 704       *  @since Method available since Release 1.0
 705       */
 706      function ssh_copy_from($remotepath, $localpath)
 707      {
 708          global $debug;
 709  
 710      umask(0077);
 711      $tmpfname = tempnam($this->workdir, "copy-from-");
 712      chmod($tmpfname, 0700);
 713      putenv("DISPLAY=none:0.");
 714      putenv("SSH_ASKPASS=$tmpfname");
 715      $fp = fopen($tmpfname, "w");
 716      fputs($fp, "#!/bin/sh\necho $this->password\n");
 717      if (! $debug)  
 718          fputs($fp, "/bin/touch $tmpfname.called\n");
 719      else
 720          fputs($fp, "/bin/rm $tmpfname\n");
 721      fclose($fp);
 722      exec("$this->scp -pqrC $this->remote:$remotepath $localpath", $out, $status);
 723      if ($status == 0)
 724          return TRUE;
 725      else
 726          return FALSE;
 727      }
 728  
 729      /**
 730       *    Execute a single command remotely
 731       *
 732       *  Execute a single command remotely using ssh and 
 733       * display its output, optionally returning its exit 
 734       * status (like passthru)
 735       *
 736       *    This function is intended to be used as a one-time
 737       * all-at-once non-interactive execution mechanism which
 738       * will run the command remotely and display its output.
 739       *
 740       *    If you try to issue an interactive command using this
 741       * function, all you will get is unneccessary trouble. So
 742       * don't!
 743       *
 744       *  This might be done as well using a pipe on /tmp and
 745       * making the command 'cat' the pipe: when ssh runs, it
 746       * runs the command 'cat' on the pipe and hangs on read.
 747       *  Then we just need a thread to open the pipe, put the
 748       * password and close the pipe.
 749       *
 750       *  This other way the password is never wirtten down.
 751       * But, OTOH, the file life is so ephemeral that most
 752       * of the time it will only exist in the internal system
 753       * cache, so this approach is not that bad either.
 754       *
 755       *    @see passthru()
 756       *
 757       *  @param string command  The command to execute on the remote end
 758       *                  NOTE: if you want to use redirection, the
 759       *                  entire remote command line should be 
 760       *                  enclosed in additional quotes!
 761       *  @param integer status   Optional, this will hold the termination
 762       *                  status of SSH after invocation, which
 763       *                  should be the exit status of the remote
 764       *                  command or 255 if an error occurred
 765       *  @return void
 766       *
 767       *    @access public
 768       *  @since Method available since Release 1.0
 769       */
 770      function ssh_passthru($command, &$status)
 771      {
 772          global $debug;
 773  
 774          if ($debug) echo "status = $status\n";
 775      // go
 776      if (isset($status)) {
 777          if ($debug) echo "st: $this->ssh -x -t -t -S $this->mplex_socket $this->remote \"$command\"\n";
 778          passthru("$this->ssh -x -t -t -S $this->mplex_socket $this->remote \"$command\"", $status);
 779      }
 780      else {
 781          if ($debug) echo "~st: $this->ssh -x -t -t  -S $this->mplex_socket $this->remote \"$command\"\n";
 782          passthru("$this->ssh -x -t -t  -S $this->mplex_socket $this->remote \"$command\"");
 783          }
 784      }
 785      
 786      
 787      /**
 788       *    Execute a remote command using SSH
 789       *
 790       *    This function sort of mimics rexec(3) using SSH as the transport
 791       * protocol.
 792       *
 793       *    The function returns the exit status of the remote command, and
 794       * appends the remote job output to an optional argument.
 795       *
 796       *    This function is intended to be used as a one-time
 797       * all-at-once non-interactive execution mechanism which
 798       * will run the command remotely and return its output.
 799       *
 800       *    If you try to issue an interactive command using this
 801       * function, all you will get is unneccessary trouble. So
 802       * don't!
 803       *
 804       *  @param string command  The command to execute on the remote end
 805       *                  NOTE: if you want to use redirection, the
 806       *                  entire remote command line should be 
 807       *                  enclosed in additional quotes!
 808       *  @param string output   Optional, the collated (stdout+stderr) output 
 809       *                        of the remote command.
 810       *  @return integer status  will hold the termination
 811       *                  status of SSH after invocation, which
 812       *                  should be the exit status of the remote
 813       *                  command or 255 if an error occurred
 814       *
 815       *    @access public
 816       *  @since Method available since Release 1.0
 817       */
 818      function ssh_exec($command, &$out)
 819      {
 820          global $debug;
 821  
 822      umask(0077);
 823      $tmpfname = tempnam($this->workdir, 'exec');
 824      chmod($tmpfname, 0700);
 825      if ($debug) echo $tmpfname . "\n";
 826  
 827      exec("$this->ssh -x -t -t -S $this->mplex_socket $this->remote \"$command\"", $out, $retval);
 828      return $retval;
 829  
 830      }
 831      
 832      /**
 833       *    Open an SSH connection to a remote site with a shell to run 
 834       * interactive commands
 835       *
 836       *    Connects to a remote host and opens an interactive shell session
 837       * with NO controlling terminal.
 838       *
 839       *    This routine creates communication streams with the remote shell,
 840       * and stores all output (standard and error) of the connection into
 841       * two separate local log files (one for stdout and one for stderr).
 842       *
 843       *    Returns a process_control array which contains the process resource
 844       * ID and an the standard file descriptors which the caller may use to
 845       * interact with the remote shell.
 846       *
 847       * The process control array contains:
 848       *
 849       *    'process' -- the process resource for the newly created connection
 850       *
 851       *    'std_in' -- handle to the standard input of the new connection
 852       *
 853       *    'std_out' -- handle to standard output of the new connection
 854       *
 855       *    'std_err' -- handle to standard error of the new connection
 856       *
 857       *    'stdout_file' -- actual filename of the local log file for the
 858       *        new connection standard output
 859       *
 860       *    'stderr_file' -- actual filename of the local log file for the
 861       *        new connection standard error
 862       *
 863       *    @return mixed a process control associative array.
 864       *
 865       *    @access public
 866       *  @since Method available since Release 1.0
 867       */
 868      function ssh_open_shell()
 869      {    
 870      global $debug;
 871  
 872      // Open a child process with the 'proc_open' function. 
 873      //
 874      // Some tricks: we must open the connection using '-x' to disable
 875      // X11 forwarding, and use '-t -t' to avoid SSH generating an error
 876      // because we are not connected to any terminal.
 877      //
 878      // NOTE: if the web server is trusted remotely (i.e. it's SSH public 
 879      // key is accepted in ~user@host:.ssh/authorized_keys) then any 
 880      // password will do.
 881  
 882      // Prepare I/O
 883      umask(0077);
 884      if ($debug) {
 885          $child_stdout = tempnam($this->workdir, "open_sh-".getmypid()."-O-");
 886          $child_stderr = tempnam($this->workdir, "open_sh-".getmypid()."-E-");
 887      } else {
 888          $child_stdout = tempnam($this->workdir, "open_sh-");
 889          $child_stderr = tempnam($this->workdir, "open_sh-");
 890      }
 891      $descriptorspec = array(
 892              0 => array("pipe", "r"),  // connect child's stdin to the read end of a pipe
 893              1 => array("file", $child_stdout, "a"),  // connect child's stdout to the write end of a pipe
 894              2 => array("file", $child_stderr, "a")   // stderr is a pipe to read from
 895      );
 896      if ($debug) echo "$this->ssh -x -t -t -S $this->mplex_socket $this->remote<br />\n";
 897      $process = proc_open("$this->ssh -x -t -t -S $this->mplex_socket $this->remote", 
 898                   $descriptorspec,
 899               $pipes);
 900      
 901      // check status
 902      if (!is_resource($process)) 
 903      {
 904          letal("SSH::connect", "cannot connect to the remote host");
 905          return;
 906      }
 907      if ($debug) echo "proc_open done<br />\n";
 908  
 909      // $pipes now looks like this:
 910      //   0 => writeable handle connected to child stdin
 911      
 912      // Open child's stdin and stdout
 913      $pipes[1] = fopen($child_stdout, "r");
 914      $pipes[2] = fopen($child_stderr, "r");
 915      
 916      // Should we leave this to the user?
 917      // set to non-blocking and avoid having to call fflush
 918      //stream_set_blocking($pipes[0], FALSE);
 919      //stream_set_blocking($pipes[1], FALSE);
 920      //stream_set_blocking($pipes[2], FALSE);
 921      stream_set_write_buffer($pipes[0], 0);
 922      stream_set_write_buffer($pipes[1], 0);
 923      stream_set_write_buffer($pipes[2], 0);
 924  
 925      // We now have a connection to the remote SSH
 926      // Server which we may use to send commands/receive output
 927      $p = array('process' => $process
 928                  ,'std_in' => $pipes[0]
 929                      ,'std_out' => $pipes[1]
 930              ,'std_err' => $pipes[2] 
 931              ,'stdout_file' => $child_stdout
 932              ,'stderr_file' => $child_stderr
 933             );
 934      if ($debug)  {
 935          echo "process descriptor array is \n";
 936          print_r($p);
 937      }
 938      return $p;
 939      }
 940      
 941      /**
 942       *    Open an SSH connection to run an interactive command on a remote
 943       * site
 944       *
 945       *    Connects to a remote host and runs an interactive command
 946       * with NO controlling terminal.
 947       *
 948       *    This routine creates communication streams with the remote shell,
 949       * and stores all output (standard and error) of the connection into
 950       * two separate local log files (one for stdout and one for stderr).
 951       *
 952       *    Returns a process_control array which contains the process resource
 953       * ID and an the standard file descriptors which the caller may use to
 954       * interact with the remote shell.
 955       *
 956       * The process control array contains:
 957       *
 958       *    'process' -- the process resource for the newly created connection
 959       *
 960       *    'std_in' -- handle to the standard input of the new connection
 961       *
 962       *    'std_out' -- handle to standard output of the new connection
 963       *
 964       *    'std_err' -- handle to standard error of the new connection
 965       *
 966       *    'stdout_file' -- actual filename of the local log file for the
 967       *        new connection standard output
 968       *
 969       *    'stderr_file' -- actual filename of the local log file for the
 970       *        new connection standard error
 971       *
 972       *    @param    string command to be executed interactively on the remote end
 973       *
 974       *    @return mixed a process control associative array.
 975       *
 976       *    @access public
 977       *  @since Method available since Release 1.0
 978       */
 979      function ssh_open_command($command)
 980      {    
 981      global $debug;
 982  
 983      // Open a child process with the 'proc_open' function. 
 984      //
 985      // Some tricks: we must open the connection using '-x' to disable
 986      // X11 forwarding, and use '-t -t' to avoid SSH generating an error
 987      // because we are not connected to any terminal.
 988      //
 989      // NOTE: if the web server is trusted remotely (i.e. it's SSH public 
 990      // key is accepted in ~user@host:.ssh/authorized_keys) then any 
 991      // password will do.
 992  
 993      // Prepare I/O
 994      umask(0077);
 995      if ($debug) {
 996          $child_stdout = tempnam($this->workdir, "open_cmd-".getmypid()."-1-");
 997          $child_stderr = tempnam($this->workdir, "open_cmd-".getmypid()."-2-");
 998      } else {
 999          $child_stdout = tempnam($this->workdir, "open_cmd-");
1000          $child_stderr = tempnam($this->workdir, "open_cmd-");
1001      }
1002      $descriptorspec = array(
1003              0 => array("pipe", "r"),  // connect child's stdin to the read end of a pipe
1004              #1 => array("pipe", "a"),  // connect child's stdout to the write end of a pipe
1005              #2 => array("pipe", "a")   // stderr is a pipe to read from
1006          1 => array("file", $child_stdout, "a"),
1007          2 => array("file", $child_stderr, "a")
1008      );
1009  
1010      if ($debug) echo "$this->ssh -x -t -t -S $this->mplex_socket $this->remote $command<br />\n";
1011      $process = proc_open("$this->ssh -x -t -t -S $this->mplex_socket $this->remote \"$command\"", 
1012                   $descriptorspec,
1013               $pipes);
1014      
1015      // check status
1016      if (!is_resource($process)) 
1017      {
1018          letal("SSH::connect", "cannot connect to the remote host");
1019          return;
1020      }
1021      if ($debug) echo "proc_open done<br />\n";
1022  
1023      // $pipes now looks like this:
1024      //   0 => writeable handle connected to child stdin
1025      
1026      // Open child's stdin and stdout
1027      $pipes[1] = fopen($child_stdout, "r");
1028      $pipes[2] = fopen($child_stderr, "r");
1029      
1030      // Should we leave this to the user?
1031      // set to non-blocking and avoid having to call fflush
1032      #stream_set_blocking($pipes[0], FALSE);
1033      #stream_set_blocking($pipes[1], FALSE);
1034      #stream_set_blocking($pipes[2], FALSE);
1035      stream_set_write_buffer($pipes[0], 0);
1036      stream_set_write_buffer($pipes[1], 0);
1037      stream_set_write_buffer($pipes[2], 0);
1038  
1039      // We now have a connection to the remote SSH
1040      // Server which we may use to send commands/receive output
1041      $p = array('process' => $process
1042                  ,'std_in' => $pipes[0]
1043                      ,'std_out' => $pipes[1]
1044              ,'std_err' => $pipes[2] 
1045              ,'stdout_file' => $child_stdout
1046              ,'stderr_file' => $child_stderr
1047             );
1048      if ($debug)  {
1049          echo "process descriptor array is \n";
1050          print_r($p);
1051      }
1052      return $p;
1053      }
1054      
1055      /**
1056       * Get output until we reach a given regular expression
1057       *
1058       *    @note EXPERIMENTAL, requires more thought and experience.
1059       */
1060      function ssh_out_expect($p, $expr="^# ")
1061      {
1062          do {
1063              flush();
1064          fseek($p["std_out"], $last);
1065              $line = fgets($p["std_out"], 1024);
1066              #echo ">> ".$line;
1067          $last = ftell($p["std_out"]);
1068          } while ((! feof($p["std_out"]) ) || (! ereg($expr, $line)));
1069      }
1070  
1071      /**
1072       * Close an SSH interactive session
1073       *
1074       *    This method terminates a previously open interactive remote 
1075       * session. It will send a termination notification to the
1076       * remote end, close the connection with control and communication
1077       * streams, and terminate the local control process.
1078       *
1079       *    Copies of the log files that contain the output and error
1080       * of the communication are left out for later reference and 
1081       * local peruse. If you don't need them any longer, you may
1082       * delete them or just leave them around until the class destructor
1083       * is called (which will remove all session traces),
1084       *
1085       *    @param mixed p an associative array with the description of the interactive
1086       *        session control process, obtained by a previous call to one
1087       *        of the interactive session creation methods ssh_open_shell()
1088       *        or ssh_open_command().
1089       *
1090       *    @return integer the exit status of the remote interactive session.
1091       *
1092       *    @access public
1093       *  @since Method available since Release 1.0
1094       */
1095      function ssh_close($p)
1096      {
1097          global $debug;
1098      
1099          fwrite($p['std_in'], "\n");
1100          fwrite($p['std_in'], "logout\n");
1101          fflush($p['std_in']);
1102          fclose($p['std_in']); fclose($p['std_out']); fclose($p['std_err']);
1103          if ($debug) echo "pipes/files closed\n";
1104          // XXX we should delete the log files here...
1105          return proc_close($p['process']);
1106      }
1107      
1108  #    if ($php_version >= 5)
1109  #    {
1110  #    /**
1111  #     * send a signal to a running ssh_open_* process
1112  #     */
1113  #    function ssh_signal($p, $signal)
1114  #    {
1115  #            return proc_terminate($p['process'], $signal);
1116  #    }
1117  #    /**
1118  #     * get info about a running ssh_open_* process
1119  #     */
1120  #    function ssh_get_status($p)
1121  #    {
1122  #            return proc_get_status($p['process']);
1123  #    }
1124  #    }
1125      
1126      /**
1127       *    Execute a remote command and keep an unidirectional stream
1128       * contact with it.
1129       *
1130       *    This routine mimics 'popen()' but uses ssh to connect to
1131       * a remote host and run the requested command: in other words,
1132       * it opens a pipe to a remotely executed command. This pipe is
1133       * unidirectional, with the communications direction controlled
1134       * by a method parameter.
1135       *
1136       *    @see popen() for more details.
1137       *
1138       *    @param string command is the command to execute on the remote end
1139       *
1140       *    @param string mode specifies the communications direction for the 
1141       *        pipe: if set to "r" (read), then we will be able to
1142       *        collect command output only; if set to "w" (write)
1143       *        then we may only send input to the remote command.
1144       *
1145       *    @return resource a handle to the unidirectional communication stream,
1146       *        similar to that returned by fopen(), or FALSE on
1147       *        failure. This handle must be closed with ssh_pclose().
1148       *
1149       *    @access public
1150       *  @since Method available since Release 1.0
1151       */
1152      function ssh_popen($command, $mode)
1153      {
1154          global $debug;
1155  
1156      // go
1157      return popen("$this->ssh -x -t -t -S $this->mplex_socket $this->remote \"$command\"", $mode);
1158      }
1159      
1160      /**
1161       * Close a piped remote execution command control pipe.
1162       *
1163       *    This routine accepts as input the handle for the control stream
1164       * of a remote command and closes it, terminating the command as well.
1165       * The handle must be valid and obtained through a call to ssh_popen().
1166       *
1167       *    @param resource f is the file handle associated with the pipe control stream
1168       *
1169       *    @return integer the termination status of the command that was run.
1170       *
1171       *    @access public
1172       *  @since Method available since Release 1.0
1173       */
1174      function ssh_pclose($f)
1175      {
1176          return pclose($f);
1177      }
1178  
1179      //}}}
1180  }
1181  
1182  /*
1183   * Local variables:
1184   * tab-width: 4
1185   * c-basic-offset: 4
1186   * c-hanging-comment-ender-p: nil
1187   * End:
1188   */
1189  
1190  ?>


Generated: Wed May 25 19:30:43 2005 Cross-referenced by PHPXref 0.4.1