[ Index ] |
PHP Cross Reference of phpSSH |
[Summary view] [Print]
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@acm.org> 37 * @license doc/lic/lgpl.txt 38 * @version CVS: $Id: ssh.php,v 1.9 2005/05/30 15:32:41 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 tricky 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_sexec 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: 2.1 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.2"; 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 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 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_sexec; 377 378 if ($debug_sexec) echo "\nSExec::SExec($remote, $password)\n"; 379 if ($debug_sexec) echo "--> Creating a new SExec\n"; 380 $this->remote = $remote; 381 $this->password = "$password"; 382 umask(0077); 383 /* DESIGN 384 * In order to increase efficiency, we will create a master channel 385 * on class instantiation. The master channel should be closed by a 386 * corresponding class destructor! 387 * 388 * Creating a master channel has the advantage that subsequent SSH 389 * connections will use it and avoid repeating the slow authentication 390 * process: in other words, they will go much, much faster. 391 */ 392 393 // first we must generate a unique UNIX socket address or we'll fail 394 // We use a tricky trick: generate two random numbers and use them; 395 // this is tricky since there might be a problem, but with very low 396 // probability. BUT IT MAY STILL FAIL: there's a race condition between 397 // the end of the while and the subsequent if. 398 do { 399 mt_srand((double)microtime()*1000000 ) . 400 $this->workdir = "/tmp/phpSsh-" . mt_rand() .".". mt_rand(); 401 if ($debug_sexec) echo "\nSExec: trying $this->workdir/ ..."; 402 // CAUTION: this is potentially an endless loop (albeit with very 403 // low probability) if every possible file did exist. 404 } 405 while (file_exists($this->workdir)); 406 if (mkdir($this->workdir) == FALSE) { 407 // we can't continue. How can we cancel this? 408 // try these and then check what happens 409 unset($this); 410 $this = NULL; 411 return FALSE; 412 } 413 else 414 if ($debug_sexec) echo " OK\n"; 415 // Now we have a place to put the socket... Mmm... 416 // Come to think of it, we have a place to put ANY temporary 417 // for the class... 418 // XXX Maybe we can change everywhere else to use this? 419 $this->mplex_socket = $this->workdir."/mplex_socket"; 420 421 // Finally we can call SSH -M 422 // Create SSH_ASKPASS script to provide the password 423 $tmpfname = tempnam($this->workdir, 'SExec-'); 424 chmod($tmpfname, 0700); 425 putenv("DISPLAY=none:0."); 426 putenv("SSH_ASKPASS=$tmpfname"); 427 $fp = fopen($tmpfname, "w"); 428 fputs($fp, "#!/bin/sh\necho $this->password\n"); 429 if (!$debug_sexec) 430 fputs($fp, "/bin/touch $tmpfname.called\n"); 431 else 432 fputs($fp, "/bin/rm -f $tmpfname\n"); 433 fclose($fp); 434 435 // OK, we are ready. Now let's open a master shell 436 $child_stdout = tempnam($this->workdir, "open_sh-O-"); 437 $child_stderr = tempnam($this->workdir, "open_sh-E-"); 438 $descriptorspec = array( 439 0 => array("pipe", "r"), // connect child's stdin to the read end of a pipe 440 1 => array("file", $child_stdout, "a"), // connect child's stdout to the write end of a pipe 441 2 => array("file", $child_stderr, "a") // stderr is a pipe to read from 442 ); 443 444 if ($debug_sexec) echo "$this->ssh -x -t -t ". 445 "-M -S $this->mplex_socket " . 446 "$this->remote\n"; 447 $this->master = proc_open("$this->ssh -x -t -t ". 448 "-M -S $this->mplex_socket " . 449 "$this->remote", 450 $descriptorspec, 451 $pipes); 452 if ((! is_resource($this->master)) || ($this->master == FALSE)) { 453 putenv("SSH_ASKPASS=dummy"); 454 unset($this); 455 $this = NULL; 456 return FALSE; 457 } 458 // we do not need to worry about the output log files, just the 459 // input pipe for logout 460 $this->master_input = $pipes[0]; 461 462 // Before going ahead, we need to ensure the control shell 463 // has started: wait for the socket to become available 464 // note: there should be a timeout here to avoid a possibly 465 // infinite loop XXX JR XXX 466 do { 467 if ($debug_sexec) echo "waiting 0.1 sec\n"; 468 usleep(100000); // wait 0.1 seconds 469 } while (! file_exists($this->mplex_socket)); 470 471 // and now we must register a destructor for the class 472 // that will close the connection. 473 //register_shutdown_function($this->destruct()); 474 475 return $this; 476 } 477 478 /** Class destructor 479 * 480 * Destroy all working processes and data streams and structures 481 * used by an instance of this class. 482 * 483 * This method will send a termination message to the other end 484 * of the master channel, close the control stream of the master 485 * channel and terminate its controlling process, finally unsetting 486 * the object and setting the object handle to NULL. 487 * 488 * If a global $debug_sexec is not set to TRUE, then it will also remove 489 * all communication traces of this object: i.e. all log files for 490 * interactive and master sessions, communications socket, etc... 491 * 492 * If global $debug_sexec is set to TRUE, then a copy of all log files 493 * created during the lifetime of the object will be left on a 494 * temporary directory for your perusal and reference. 495 * 496 * @return integer exit status of the master channel control process. 497 * 498 * @access public 499 * @since Method available since Release 1.0 500 */ 501 function destruct() 502 { 503 global $debug_sexec; 504 505 if ($debug_sexec) echo "\nSExec::destruct()\n"; 506 if ($debug_sexec) echo "--> Destroying SExec master\n"; 507 if ($debug_sexec) print_r($this); 508 if ($debug_sexec) echo "sending logout\n"; 509 // log out master process 510 fputs($this->master_input, "\n\nlogout\n\n"); 511 // close master stdin 512 fclose($this->master_input); 513 // close master process 514 $ret = proc_close($this->master); 515 // remove temporaries 516 if (! $debug_sexec) system("/bin/rm -rf $this->workdir"); 517 // utterly destroy this instance 518 unset($this); 519 $this = NULL; 520 return $ret; 521 } 522 523 //}}} 524 525 //{{{ methods 526 /** 527 * Copy a file or directory from one source to a destination 528 * 529 * This function copies source to dest, where one of them is a 530 * local filespec and the other a remote filespec of the form 531 * [user@]host:path 532 * 533 * If the original source is a directory, it will be copied 534 * recursively to destination (hence easing file transfers). 535 * 536 * The function returns TRUE on success or FALSE on failure. 537 * 538 * <b>EFFICIENCY NOTICE:</b> 539 * 540 * The copy routines use 'scp' to do their actual work. Since 541 * scp seems to be unable to hitchhike on the master channel, 542 * we must do authentication for each copy operation (subroutine 543 * call). These routines are hence a lot more time-expensive 544 * than all the other ones. 545 * 546 * You may want to consider whether you can group several 547 * copies into one single call to reduce authentication 548 * overheads. 549 * 550 * @note DEPRECATED (inconsistent with the class) 551 * 552 * @see scp(1) 553 * 554 * @param string The origin path, of the form 555 * [user@][host][:port]path 556 * You may omit the optional sections if 557 * the default values (local username, local 558 * host, standard SSH port) are OK 559 * 560 * @param string The destination path, of the form 561 * [user@][host][:port:]path 562 * You may omit the optional sections if 563 * the default values (local username, local 564 * host, standard SSH port) are OK 565 * 566 * @param string The password to use to connect to the remote 567 * end of the copy (be it the origin or the 568 * destination, it's all the same). If connection 569 * is automatic by some means (.shosts or RSA/DSA 570 * authentication) then it should be ignored and 571 * any password should do. 572 * 573 * @return bool TRUE if all went well, or FALSE on failure. 574 * 575 * @access public 576 * @since Method available since Release 1.0 577 * @deprecated Method deprecated as of Release 2.1 578 */ 579 function ssh_copy($origin, $destination, $password) 580 { 581 global $debug_sexec; 582 583 if ($debug_sexec) echo "\nSExec::ssh_copy($origin, $destination, $password)\n"; 584 umask(0077); 585 $tmpfname = tempnam($this->workdir, "copy-"); 586 chmod($tmpfname, 0700); 587 putenv("DISPLAY=none:0."); 588 putenv("SSH_ASKPASS=$tmpfname"); 589 $fp = fopen($tmpfname, "w"); 590 fputs($fp, "#!/bin/sh\necho $password\n"); 591 if (! $debug_sexec) 592 fputs($fp, "/bin/touch $tmpfname.called\n"); 593 else 594 fputs($fp, "/bin/rm $tmpfname\n"); 595 fclose($fp); 596 $out=""; 597 exec("$this->scp -pqrC $origin $destination", $out, $status); 598 if ($status == 0) 599 return TRUE; 600 else 601 return FALSE; 602 } 603 604 605 /** 606 * Copy a file or directory from a local source to a remote destination 607 * 608 * This function copies source to dest, where first of them is a 609 * local filespec and then comes a remote filespec as a normal 610 * system path. 611 * 612 * Both, local and remote paths may be absolute or relative. 613 * 614 * If the original source is a directory, it will be copied 615 * recursively to destination (hence easing file transfers). 616 * 617 * The function returns TRUE on success or FALSE on failure. 618 * 619 * @param string The origin local path, either absolute or 620 * relative to the current working directory. 621 * If it denotes a directory, the copy will 622 * be recursive. 623 * 624 * @param string The destination path, either 625 * absolute or relative to the login home. 626 * 627 * @param array An optional array of strings to be appended the 628 * copy operation's output for debugging/diagnostics. 629 * 630 * @return bool TRUE if all went well, or FALSE on failure. 631 * 632 * @access public 633 * @since Method available since Release 2.1 634 */ 635 function ssh_copy_to($localpath, $remotepath, &$out) 636 { 637 global $debug_sexec; 638 639 if ($debug_sexec) echo "\nSExec::ssh_copy_to($localpath, $remotepath)\n"; 640 641 /* This would be great if SCP could hijack the shared connection (sic) 642 umask(0077); 643 $tmpfname = tempnam($this->workdir, "copy-to-"); 644 chmod($tmpfname, 0700); 645 putenv("DISPLAY=none:0."); 646 putenv("SSH_ASKPASS=$tmpfname"); 647 $fp = fopen($tmpfname, "w"); 648 fputs($fp, "#!/bin/sh\necho $this->password\n"); 649 if (! $debug_sexec) 650 fputs($fp, "/bin/touch $tmpfname.called\n"); 651 else 652 fputs($fp, "/bin/rm $tmpfname\n"); 653 fclose($fp); 654 if ($debug_sexec) echo "$this->scp -pqrC $localpath $this->remote:$remotepath\n"; 655 $out = ""; 656 exec("$this->scp -pqrC $localpath $this->remote:$remotepath", $out, $status); 657 if ($status == 0) 658 return TRUE; 659 else { 660 if ($debug_sexec) echo $out . "\n"; 661 return FALSE; 662 } 663 */ 664 // NOTE THAT WE NEED GNU TAR !!! 665 $retval = $this->ssh_exec("test -d $remotepath 2>&1", $out); 666 if ($retval == 0) { 667 // destination is a directory, copy $local inside it 668 if ($debug_sexec) echo "--> Remote is a directory\n"; 669 $fn = basename($localpath); 670 $dn = dirname($localpath); 671 if ($debug_sexec) echo "--> Executing\n" . 672 "/bin/tar -C $dn -cf - $fn | " . 673 "ssh -x -T -C -S $this->mplex_socket $this->remote " . 674 "\"/bin/tar -C $remotepath -xf -\"\n"; 675 exec("(/bin/tar -C $dn -cf - $fn | " . 676 "ssh -x -T -C -S $this->mplex_socket $this->remote " . 677 "\"/bin/tar -C $remotepath -xf -\")2>&1", 678 $out, $retval); 679 } else { 680 // destination is not a directory, copy _to_ it 681 if ($debug_sexec) 682 echo "--> remote is not a directory or does not exist\n"; 683 if ((file_exists("$localpath/.")) && (is_dir("$localpath/."))) { 684 // if local is a dir, try to create it remotely with new name 685 $retval = $this->ssh_exec("/bin/mkdir -p $remotepath ", $out); 686 if ($retval != 0) { 687 // we can't create it, either it already exists as a 688 // regular file or we don't have permissions, anyhow, 689 // we can't do the copy 690 if ($debug_sexec) print_r($out); 691 return FALSE; 692 } 693 // now cd lo local and copy over to remote 694 if ($debug_sexec) echo "--> Executing \n" . 695 " /bin/tar -C $localpath -cf - . | \n" . 696 " $this->ssh -x -T -C -S $this->mplex_socket $this->remote \n" . 697 " /bin/tar -C $remotepath -xf -\n"; 698 exec("(/bin/tar -C $localpath -cf - . | " . 699 "$this->ssh -x -T -C -S $this->mplex_socket $this->remote " . 700 "/bin/tar -C $remotepath -xf -)2>&1", 701 $out, $retval); 702 } 703 else { 704 // non-dir: file, block-special, char-special, pipe, socket... 705 if ($debug_sexec) echo "--> Executing \n" . 706 " cat $localpath | \n" . 707 " $this->ssh -x -T -C -S $this->mplex_socket $this->remote \n" . 708 " \"cat > $remotepath\"\n"; 709 exec("(cat $localpath | " . 710 "$this->ssh -x -T -C -S $this->mplex_socket $this->remote " . 711 "\"cat > $remotepath\") 2>&1", $out, $retval); 712 } 713 } 714 if ($retval != 0) { 715 if ($debug_sexec) print_r($out); 716 return FALSE; 717 } 718 else 719 return TRUE; 720 } 721 722 /** 723 * Copy a file or directory from a remote source to a local destination 724 * 725 * This function copies source to dest, where first of them is a 726 * remote filespec and then comes a local filespec, both specified 727 * as normal system paths. 728 * 729 * Both, local and remote paths may be absolute or relative. 730 * 731 * If the original source is a directory, it will be copied 732 * recursively to destination (hence easing file transfers). 733 * 734 * The function returns TRUE on success or FALSE on failure. 735 * 736 * @param string The origin remote path, either absolute or 737 * relative to the login home. If it denotes a 738 * directory, the copy will be recursive. 739 * 740 * @param string The local destination path, either 741 * absolute or relative to the current working 742 * directory. 743 * 744 * @param array An optional array of strings to be appended the 745 * copy operation's output for debugging/diagnostics. 746 * 747 * @return bool TRUE if all went well, or FALSE on failure. 748 * 749 * @access public 750 * @since Method available since Release 1.0 751 */ 752 function ssh_copy_from($remotepath, $localpath, &$out) 753 { 754 global $debug_sexec; 755 756 if ($debug_sexec) echo "SExec::ssh_copy_from($remotepath, $localpath)\n"; 757 758 /* This would be great if SCP could hijack the shared connection (sic) 759 umask(0077); 760 $tmpfname = tempnam($this->workdir, "copy-from-"); 761 chmod($tmpfname, 0700); 762 putenv("DISPLAY=none:0."); 763 putenv("SSH_ASKPASS=$tmpfname"); 764 $fp = fopen($tmpfname, "w"); 765 fputs($fp, "#!/bin/sh\necho $this->password\n"); 766 if (! $debug_sexec) 767 fputs($fp, "/bin/touch $tmpfname.called\n"); 768 else 769 fputs($fp, "/bin/rm $tmpfname\n"); 770 fclose($fp); 771 if ($debug_sexec) echo "$this->scp -pqrC $this->remote:$remotepath $localpath\n"; 772 $out = ""; 773 exec("$this->scp -pqrC $this->remote:$remotepath $localpath", $out, $status); 774 if ($status == 0) 775 return TRUE; 776 else { 777 if ($debug_sexec) echo $out . "\n"; 778 return FALSE; 779 } 780 */ 781 if ((file_exists("$localpath/.")) && (is_dir("$localpath/."))) { 782 // Local is a directory. Copy remote into it. 783 if ($debug_sexec) echo "--> $localpath/. is a dir\n"; 784 if ($debug_sexec) echo "--> Executing\n" . 785 "$this->ssh -x -T -C -S $this->mplex_socket $this->remote " . 786 "\"/bin/tar -C " .dirname($remotepath). 787 " -cf - ". basename($remotepath) ."\" | ". 788 "/bin/tar -C $localpath -xf -\n"; 789 exec("($this->ssh -x -T -C -S $this->mplex_socket $this->remote " . 790 "\"/usr/local/bin/tar -C " .dirname($remotepath). 791 " -cf - ". basename($remotepath) ."\" | ". 792 "/bin/tar -C $localpath -xf -) 2>&1", 793 $out, $res); 794 } 795 else { 796 // either the local side does not exist or is not a directory 797 // if remote is a directory 798 // make local equivalent and copy contents (make will 799 // fail if local exists as a non-dir) 800 if ($debug_sexec) echo "--> $localpath is NOT a dir\n"; 801 $res = $this->ssh_exec("test -d $remotepath 2>&1", $out); 802 if ($res == 0) { 803 exec("/bin/mkdir -p $localpath 2>&1", $out, $res); 804 if ($res != 0) { 805 // can't create the dir, either it is a regular file 806 // or we don't have privileges 807 if ($debug_sexec) print_r($out); 808 return FALSE; 809 } 810 // copy in the remote contents 811 if ($debug_sexec) echo "-->Executing\n" . 812 "($this->ssh -x -T -C -S $this->mplex_socket $this->remote " . 813 "\"/bin/tar -C $remotepath -cf - .\" | " . 814 "/bin/tar -C $localpath -xf -)2>&1\n"; 815 exec("($this->ssh -x -T -C -S $this->mplex_socket $this->remote " . 816 "\"/bin/tar -C $remotepath -cf - .\" | " . 817 "/bin/tar -C $localpath -xf -)2>&1", 818 $out, $res); 819 820 } else { 821 // remote is a non-dir: cat over local 822 if ($debug_sexec) echo "-->Executing\n" . 823 "($this->ssh -x -T -C -S $this->mplex_socket $this->remote " . 824 "\"cat $remotepath\" | ". 825 " cat > $localpath) 2>&1\n"; 826 exec("($this->ssh -x -T -C -S $this->mplex_socket $this->remote " . 827 "\"cat $remotepath\" | ". 828 " cat > $localpath) 2>&1", $out, $res); 829 } 830 } 831 if ($res == 0) 832 return TRUE; 833 else { 834 if ($debug_sexec) print_r($out); 835 return FALSE; 836 } 837 } 838 839 /** 840 * Execute a single command remotely 841 * 842 * Execute a single command remotely using ssh and 843 * display its output, optionally returning its exit 844 * status (like passthru) 845 * 846 * This function is intended to be used as a one-time 847 * all-at-once non-interactive execution mechanism which 848 * will run the command remotely and display its output. 849 * 850 * If you try to issue an interactive command using this 851 * function, all you will get is unneccessary trouble. So 852 * don't! 853 * 854 * This might be done as well using a pipe on /tmp and 855 * making the command 'cat' the pipe: when ssh runs, it 856 * runs the command 'cat' on the pipe and hangs on read. 857 * Then we just need a thread to open the pipe, put the 858 * password and close the pipe. 859 * 860 * This other way the password is never wirtten down. 861 * But, OTOH, the file life is so ephemeral that most 862 * of the time it will only exist in the internal system 863 * cache, so this approach is not that bad either. 864 * 865 * @see passthru() 866 * 867 * @param string command The command to execute on the remote end 868 * NOTE: if you want to use redirection, the 869 * entire remote command line should be 870 * enclosed in additional quotes! 871 * @param integer status Optional, this will hold the termination 872 * status of SSH after invocation, which 873 * should be the exit status of the remote 874 * command or 255 if an error occurred 875 * @return void 876 * 877 * @access public 878 * @since Method available since Release 1.0 879 */ 880 function ssh_passthru($command, &$status) 881 { 882 global $debug_sexec; 883 884 if ($debug_sexec) echo "status = $status\n"; 885 // go 886 if (isset($status)) { 887 if ($debug_sexec) echo "st: $this->ssh -x -t -t -S $this->mplex_socket $this->remote \"$command\"\n"; 888 passthru("$this->ssh -x -t -t -S $this->mplex_socket $this->remote \"$command\"", $status); 889 } 890 else { 891 if ($debug_sexec) echo "~st: $this->ssh -x -t -t -S $this->mplex_socket $this->remote \"$command\"\n"; 892 passthru("$this->ssh -x -t -t -S $this->mplex_socket $this->remote \"$command\""); 893 } 894 } 895 896 897 /** 898 * Execute a remote command using SSH 899 * 900 * This function sort of mimics rexec(3) using SSH as the transport 901 * protocol. 902 * 903 * The function returns the exit status of the remote command, and 904 * appends the remote job output to an optional argument. 905 * 906 * This function is intended to be used as a one-time 907 * all-at-once non-interactive execution mechanism which 908 * will run the command remotely and return its output. 909 * 910 * If you try to issue an interactive command using this 911 * function, all you will get is unneccessary trouble. So 912 * don't! 913 * 914 * @param string command The command to execute on the remote end 915 * NOTE: if you want to use redirection, the 916 * entire remote command line should be 917 * enclosed in additional quotes! 918 * @param array If the output argument is present, then the specified 919 * array will be filled with every line of output 920 * from the command. Line endings, such as \n, are 921 * not included in this array. Note that if the array 922 * already contains some elements, exec() will append 923 * to the end of the array. If you do not want the 924 * function to append elements, call unset() on the 925 * array before passing it to exec(). 926 * @return integer status will hold the termination 927 * status of SSH after invocation, which 928 * should be the exit status of the remote 929 * command or 255 if an error occurred 930 * 931 * @access public 932 * @since Method available since Release 1.0 933 */ 934 function ssh_exec($command, &$out) 935 { 936 global $debug_sexec; 937 938 if ($debug_sexec) echo "SExec::ssh_exec($command, $out)\n"; 939 umask(0077); 940 $tmpfname = tempnam($this->workdir, 'exec'); 941 chmod($tmpfname, 0700); 942 if ($debug_sexec) echo $tmpfname . "\n"; 943 944 exec("$this->ssh -x -t -t -S $this->mplex_socket $this->remote \"$command\"", $out, $retval); 945 return $retval; 946 947 } 948 949 /** 950 * Open an SSH connection to a remote site with a shell to run 951 * interactive commands 952 * 953 * Connects to a remote host and opens an interactive shell session 954 * with NO controlling terminal. 955 * 956 * This routine creates communication streams with the remote shell, 957 * and stores all output (standard and error) of the connection into 958 * two separate local log files (one for stdout and one for stderr). 959 * 960 * Returns a process_control array which contains the process resource 961 * ID and an the standard file descriptors which the caller may use to 962 * interact with the remote shell. 963 * 964 * The process control array contains: 965 * 966 * 'process' -- the process resource for the newly created connection 967 * 968 * 'std_in' -- handle to the standard input of the new connection 969 * 970 * 'std_out' -- handle to standard output of the new connection 971 * 972 * 'std_err' -- handle to standard error of the new connection 973 * 974 * 'stdout_file' -- actual filename of the local log file for the 975 * new connection standard output 976 * 977 * 'stderr_file' -- actual filename of the local log file for the 978 * new connection standard error 979 * 980 * @return mixed|false a process control associative array or FALSE 981 * on failure. 982 * 983 * @access public 984 * @since Method available since Release 1.0 985 */ 986 function ssh_open_shell() 987 { 988 global $debug_sexec; 989 990 // Open a child process with the 'proc_open' function. 991 // 992 // Some tricks: we must open the connection using '-x' to disable 993 // X11 forwarding, and use '-t -t' to avoid SSH generating an error 994 // because we are not connected to any terminal. 995 // 996 // NOTE: if the web server is trusted remotely (i.e. it's SSH public 997 // key is accepted in ~user@host:.ssh/authorized_keys) then any 998 // password will do. 999 1000 // Prepare I/O 1001 umask(0077); 1002 if ($debug_sexec) { 1003 $child_stdout = tempnam($this->workdir, "open_sh-".getmypid()."-O-"); 1004 $child_stderr = tempnam($this->workdir, "open_sh-".getmypid()."-E-"); 1005 } else { 1006 $child_stdout = tempnam($this->workdir, "open_sh-"); 1007 $child_stderr = tempnam($this->workdir, "open_sh-"); 1008 } 1009 $descriptorspec = array( 1010 0 => array("pipe", "r"), // connect child's stdin to the read end of a pipe 1011 1 => array("file", $child_stdout, "a"), // connect child's stdout to the write end of a pipe 1012 2 => array("file", $child_stderr, "a") // stderr is a pipe to read from 1013 ); 1014 if ($debug_sexec) echo "$this->ssh -x -t -t -S $this->mplex_socket $this->remote<br />\n"; 1015 $process = proc_open("$this->ssh -x -t -t -S $this->mplex_socket $this->remote", 1016 $descriptorspec, 1017 $pipes); 1018 1019 // check status 1020 if ((!is_resource($process)) || ($process == FALSE)) 1021 { 1022 letal("SSH::connect", "cannot connect to the remote host"); 1023 return FALSE; 1024 } 1025 if ($debug_sexec) echo "proc_open done<br />\n"; 1026 1027 // $pipes now looks like this: 1028 // 0 => writeable handle connected to child stdin 1029 1030 // Open child's stdin and stdout 1031 $pipes[1] = fopen($child_stdout, "r"); 1032 $pipes[2] = fopen($child_stderr, "r"); 1033 1034 // Should we leave this to the user? 1035 // set to non-blocking and avoid having to call fflush 1036 //stream_set_blocking($pipes[0], FALSE); 1037 //stream_set_blocking($pipes[1], FALSE); 1038 //stream_set_blocking($pipes[2], FALSE); 1039 stream_set_write_buffer($pipes[0], 0); 1040 stream_set_write_buffer($pipes[1], 0); 1041 stream_set_write_buffer($pipes[2], 0); 1042 1043 // We now have a connection to the remote SSH 1044 // Server which we may use to send commands/receive output 1045 $p = array('process' => $process 1046 ,'std_in' => $pipes[0] 1047 ,'std_out' => $pipes[1] 1048 ,'std_err' => $pipes[2] 1049 ,'stdout_file' => $child_stdout 1050 ,'stderr_file' => $child_stderr 1051 ); 1052 if ($debug_sexec) { 1053 echo "process descriptor array is \n"; 1054 print_r($p); 1055 } 1056 return $p; 1057 } 1058 1059 /** 1060 * Open an SSH connection to run an interactive command on a remote 1061 * site 1062 * 1063 * Connects to a remote host and runs an interactive command 1064 * with NO controlling terminal. 1065 * 1066 * This routine creates communication streams with the remote shell, 1067 * and stores all output (standard and error) of the connection into 1068 * two separate local log files (one for stdout and one for stderr). 1069 * 1070 * Returns a process_control array which contains the process resource 1071 * ID and an the standard file descriptors which the caller may use to 1072 * interact with the remote shell. 1073 * 1074 * The process control array contains: 1075 * 1076 * 'process' -- the process resource for the newly created connection 1077 * 1078 * 'std_in' -- handle to the standard input of the new connection 1079 * 1080 * 'std_out' -- handle to standard output of the new connection 1081 * 1082 * 'std_err' -- handle to standard error of the new connection 1083 * 1084 * 'stdout_file' -- actual filename of the local log file for the 1085 * new connection standard output 1086 * 1087 * 'stderr_file' -- actual filename of the local log file for the 1088 * new connection standard error 1089 * 1090 * @param string command to be executed interactively on the remote end 1091 * 1092 * @return mixed|false a process control associative array or FALSE 1093 * on failure. 1094 * 1095 * @access public 1096 * @since Method available since Release 1.0 1097 */ 1098 function ssh_open_command($command) 1099 { 1100 global $debug_sexec; 1101 1102 // Open a child process with the 'proc_open' function. 1103 // 1104 // Some tricks: we must open the connection using '-x' to disable 1105 // X11 forwarding, and use '-t -t' to avoid SSH generating an error 1106 // because we are not connected to any terminal. 1107 // 1108 // NOTE: if the web server is trusted remotely (i.e. it's SSH public 1109 // key is accepted in ~user@host:.ssh/authorized_keys) then any 1110 // password will do. 1111 1112 // Prepare I/O 1113 umask(0077); 1114 if ($debug_sexec) { 1115 $child_stdout = tempnam($this->workdir, "open_cmd-".getmypid()."-1-"); 1116 $child_stderr = tempnam($this->workdir, "open_cmd-".getmypid()."-2-"); 1117 } else { 1118 $child_stdout = tempnam($this->workdir, "open_cmd-"); 1119 $child_stderr = tempnam($this->workdir, "open_cmd-"); 1120 } 1121 $descriptorspec = array( 1122 0 => array("pipe", "r"), // connect child's stdin to the read end of a pipe 1123 #1 => array("pipe", "a"), // connect child's stdout to the write end of a pipe 1124 #2 => array("pipe", "a") // stderr is a pipe to read from 1125 1 => array("file", $child_stdout, "a"), 1126 2 => array("file", $child_stderr, "a") 1127 ); 1128 1129 if ($debug_sexec) echo "$this->ssh -x -t -t -S $this->mplex_socket $this->remote $command<br />\n"; 1130 $process = proc_open("$this->ssh -x -t -t -S $this->mplex_socket $this->remote \"$command\"", 1131 $descriptorspec, 1132 $pipes); 1133 1134 // check status 1135 if ((!is_resource($process)) || ($process == FALSE)) 1136 { 1137 letal("SSH::connect", "cannot connect to the remote host"); 1138 return FALSE; 1139 } 1140 if ($debug_sexec) echo "proc_open done<br />\n"; 1141 1142 // $pipes now looks like this: 1143 // 0 => writeable handle connected to child stdin 1144 1145 // Open child's stdin and stdout 1146 $pipes[1] = fopen($child_stdout, "r"); 1147 $pipes[2] = fopen($child_stderr, "r"); 1148 1149 // Should we leave this to the user? 1150 // set to non-blocking and avoid having to call fflush 1151 #stream_set_blocking($pipes[0], FALSE); 1152 #stream_set_blocking($pipes[1], FALSE); 1153 #stream_set_blocking($pipes[2], FALSE); 1154 stream_set_write_buffer($pipes[0], 0); 1155 stream_set_write_buffer($pipes[1], 0); 1156 stream_set_write_buffer($pipes[2], 0); 1157 1158 // We now have a connection to the remote SSH 1159 // Server which we may use to send commands/receive output 1160 $p = array('process' => $process 1161 ,'std_in' => $pipes[0] 1162 ,'std_out' => $pipes[1] 1163 ,'std_err' => $pipes[2] 1164 ,'stdout_file' => $child_stdout 1165 ,'stderr_file' => $child_stderr 1166 ); 1167 if ($debug_sexec) { 1168 echo "process descriptor array is \n"; 1169 print_r($p); 1170 } 1171 return $p; 1172 } 1173 1174 /** 1175 * Get output until we reach a given regular expression 1176 * 1177 * @note EXPERIMENTAL, requires more thought and experience. 1178 */ 1179 function ssh_out_expect($p, $expr="^# ") 1180 { 1181 do { 1182 flush(); 1183 fseek($p["std_out"], $last); 1184 $line = fgets($p["std_out"], 1024); 1185 #echo ">> ".$line; 1186 $last = ftell($p["std_out"]); 1187 } while ((! feof($p["std_out"]) ) || (! ereg($expr, $line))); 1188 } 1189 1190 /** 1191 * Close an SSH interactive session 1192 * 1193 * This method terminates a previously open interactive remote 1194 * session. It will send a termination notification to the 1195 * remote end, close the connection with control and communication 1196 * streams, and terminate the local control process. 1197 * 1198 * Copies of the log files that contain the output and error 1199 * of the communication are left out for later reference and 1200 * local peruse. If you don't need them any longer, you may 1201 * delete them or just leave them around until the class destructor 1202 * is called (which will remove all session traces), 1203 * 1204 * @param mixed p an associative array with the description of the interactive 1205 * session control process, obtained by a previous call to one 1206 * of the interactive session creation methods ssh_open_shell() 1207 * or ssh_open_command(). 1208 * 1209 * @return integer the exit status of the remote interactive session. 1210 * 1211 * @access public 1212 * @since Method available since Release 1.0 1213 */ 1214 function ssh_close($p) 1215 { 1216 global $debug_sexec; 1217 1218 fwrite($p['std_in'], "\n"); 1219 fwrite($p['std_in'], "logout\n"); 1220 fflush($p['std_in']); 1221 fclose($p['std_in']); fclose($p['std_out']); fclose($p['std_err']); 1222 if ($debug_sexec) echo "pipes/files closed\n"; 1223 // XXX we should delete the log files here... 1224 return proc_close($p['process']); 1225 } 1226 1227 # if ($php_version >= 5) 1228 # { 1229 # /** 1230 # * send a signal to a running ssh_open_* process 1231 # */ 1232 # function ssh_signal($p, $signal) 1233 # { 1234 # return proc_terminate($p['process'], $signal); 1235 # } 1236 # /** 1237 # * get info about a running ssh_open_* process 1238 # */ 1239 # function ssh_get_status($p) 1240 # { 1241 # return proc_get_status($p['process']); 1242 # } 1243 # } 1244 1245 /** 1246 * Execute a remote command and keep an unidirectional stream 1247 * contact with it. 1248 * 1249 * This routine mimics 'popen()' but uses ssh to connect to 1250 * a remote host and run the requested command: in other words, 1251 * it opens a pipe to a remotely executed command. This pipe is 1252 * unidirectional, with the communications direction controlled 1253 * by a method parameter. 1254 * 1255 * @see popen() for more details. 1256 * 1257 * @param string command is the command to execute on the remote end 1258 * 1259 * @param string mode specifies the communications direction for the 1260 * pipe: if set to "r" (read), then we will be able to 1261 * collect command output only; if set to "w" (write) 1262 * then we may only send input to the remote command. 1263 * 1264 * @return resource a handle to the unidirectional communication stream, 1265 * similar to that returned by fopen(), or FALSE on 1266 * failure. This handle must be closed with ssh_pclose(). 1267 * 1268 * @access public 1269 * @since Method available since Release 1.0 1270 */ 1271 function ssh_popen($command, $mode) 1272 { 1273 global $debug_sexec; 1274 1275 // go 1276 return popen("$this->ssh -x -t -t -S $this->mplex_socket $this->remote \"$command\"", $mode); 1277 } 1278 1279 /** 1280 * Close a piped remote execution command control pipe. 1281 * 1282 * This routine accepts as input the handle for the control stream 1283 * of a remote command and closes it, terminating the command as well. 1284 * The handle must be valid and obtained through a call to ssh_popen(). 1285 * 1286 * @param resource f is the file handle associated with the pipe control stream 1287 * 1288 * @return integer the termination status of the command that was run. 1289 * 1290 * @access public 1291 * @since Method available since Release 1.0 1292 */ 1293 function ssh_pclose($f) 1294 { 1295 return pclose($f); 1296 } 1297 1298 //}}} 1299 } 1300 1301 /* 1302 * Local variables: 1303 * tab-width: 4 1304 * c-basic-offset: 4 1305 * c-hanging-comment-ender-p: nil 1306 * End: 1307 */ 1308 1309 ?>
title
Description
Body
title
Description
Body
title
Body
Generated: Tue May 31 15:44:47 2005 | Cross-referenced by PHPXref 0.4.1 |