1<?php 2 3/** 4 * SFTP Stream Wrapper 5 * 6 * Creates an sftp:// protocol handler that can be used with, for example, fopen(), dir(), etc. 7 * 8 * PHP version 5 9 * 10 * @category Net 11 * @package SFTP 12 * @author Jim Wigginton <terrafrost@php.net> 13 * @copyright 2013 Jim Wigginton 14 * @license http://www.opensource.org/licenses/mit-license.html MIT License 15 * @link http://phpseclib.sourceforge.net 16 */ 17 18namespace phpseclib\Net\SFTP; 19 20use phpseclib\Crypt\RSA; 21use phpseclib\Net\SFTP; 22 23/** 24 * SFTP Stream Wrapper 25 * 26 * @package SFTP 27 * @author Jim Wigginton <terrafrost@php.net> 28 * @access public 29 */ 30class Stream 31{ 32 /** 33 * SFTP instances 34 * 35 * Rather than re-create the connection we re-use instances if possible 36 * 37 * @var array 38 */ 39 static $instances; 40 41 /** 42 * SFTP instance 43 * 44 * @var object 45 * @access private 46 */ 47 var $sftp; 48 49 /** 50 * Path 51 * 52 * @var string 53 * @access private 54 */ 55 var $path; 56 57 /** 58 * Mode 59 * 60 * @var string 61 * @access private 62 */ 63 var $mode; 64 65 /** 66 * Position 67 * 68 * @var int 69 * @access private 70 */ 71 var $pos; 72 73 /** 74 * Size 75 * 76 * @var int 77 * @access private 78 */ 79 var $size; 80 81 /** 82 * Directory entries 83 * 84 * @var array 85 * @access private 86 */ 87 var $entries; 88 89 /** 90 * EOF flag 91 * 92 * @var bool 93 * @access private 94 */ 95 var $eof; 96 97 /** 98 * Context resource 99 * 100 * Technically this needs to be publically accessible so PHP can set it directly 101 * 102 * @var resource 103 * @access public 104 */ 105 var $context; 106 107 /** 108 * Notification callback function 109 * 110 * @var callable 111 * @access public 112 */ 113 var $notification; 114 115 /** 116 * Registers this class as a URL wrapper. 117 * 118 * @param string $protocol The wrapper name to be registered. 119 * @return bool True on success, false otherwise. 120 * @access public 121 */ 122 static function register($protocol = 'sftp') 123 { 124 if (in_array($protocol, stream_get_wrappers(), true)) { 125 return false; 126 } 127 return stream_wrapper_register($protocol, get_called_class()); 128 } 129 130 /** 131 * The Constructor 132 * 133 * @access public 134 */ 135 function __construct() 136 { 137 if (defined('NET_SFTP_STREAM_LOGGING')) { 138 echo "__construct()\r\n"; 139 } 140 } 141 142 /** 143 * Path Parser 144 * 145 * Extract a path from a URI and actually connect to an SSH server if appropriate 146 * 147 * If "notification" is set as a context parameter the message code for successful login is 148 * NET_SSH2_MSG_USERAUTH_SUCCESS. For a failed login it's NET_SSH2_MSG_USERAUTH_FAILURE. 149 * 150 * @param string $path 151 * @return string 152 * @access private 153 */ 154 function _parse_path($path) 155 { 156 $orig = $path; 157 extract(parse_url($path) + array('port' => 22)); 158 if (isset($query)) { 159 $path.= '?' . $query; 160 } elseif (preg_match('/(\?|\?#)$/', $orig)) { 161 $path.= '?'; 162 } 163 if (isset($fragment)) { 164 $path.= '#' . $fragment; 165 } elseif ($orig[strlen($orig) - 1] == '#') { 166 $path.= '#'; 167 } 168 169 if (!isset($host)) { 170 return false; 171 } 172 173 if (isset($this->context)) { 174 $context = stream_context_get_params($this->context); 175 if (isset($context['notification'])) { 176 $this->notification = $context['notification']; 177 } 178 } 179 180 if ($host[0] == '$') { 181 $host = substr($host, 1); 182 global ${$host}; 183 if (($$host instanceof SFTP) === false) { 184 return false; 185 } 186 $this->sftp = $$host; 187 } else { 188 if (isset($this->context)) { 189 $context = stream_context_get_options($this->context); 190 } 191 if (isset($context[$scheme]['session'])) { 192 $sftp = $context[$scheme]['session']; 193 } 194 if (isset($context[$scheme]['sftp'])) { 195 $sftp = $context[$scheme]['sftp']; 196 } 197 if (isset($sftp) && $sftp instanceof SFTP) { 198 $this->sftp = $sftp; 199 return $path; 200 } 201 if (isset($context[$scheme]['username'])) { 202 $user = $context[$scheme]['username']; 203 } 204 if (isset($context[$scheme]['password'])) { 205 $pass = $context[$scheme]['password']; 206 } 207 if (isset($context[$scheme]['privkey']) && $context[$scheme]['privkey'] instanceof RSA) { 208 $pass = $context[$scheme]['privkey']; 209 } 210 211 if (!isset($user) || !isset($pass)) { 212 return false; 213 } 214 215 // casting $pass to a string is necessary in the event that it's a \phpseclib\Crypt\RSA object 216 if (isset(self::$instances[$host][$port][$user][(string) $pass])) { 217 $this->sftp = self::$instances[$host][$port][$user][(string) $pass]; 218 } else { 219 $this->sftp = new SFTP($host, $port); 220 $this->sftp->disableStatCache(); 221 if (isset($this->notification) && is_callable($this->notification)) { 222 /* if !is_callable($this->notification) we could do this: 223 224 user_error('fopen(): failed to call user notifier', E_USER_WARNING); 225 226 the ftp wrapper gives errors like that when the notifier isn't callable. 227 i've opted not to do that, however, since the ftp wrapper gives the line 228 on which the fopen occurred as the line number - not the line that the 229 user_error is on. 230 */ 231 call_user_func($this->notification, STREAM_NOTIFY_CONNECT, STREAM_NOTIFY_SEVERITY_INFO, '', 0, 0, 0); 232 call_user_func($this->notification, STREAM_NOTIFY_AUTH_REQUIRED, STREAM_NOTIFY_SEVERITY_INFO, '', 0, 0, 0); 233 if (!$this->sftp->login($user, $pass)) { 234 call_user_func($this->notification, STREAM_NOTIFY_AUTH_RESULT, STREAM_NOTIFY_SEVERITY_ERR, 'Login Failure', NET_SSH2_MSG_USERAUTH_FAILURE, 0, 0); 235 return false; 236 } 237 call_user_func($this->notification, STREAM_NOTIFY_AUTH_RESULT, STREAM_NOTIFY_SEVERITY_INFO, 'Login Success', NET_SSH2_MSG_USERAUTH_SUCCESS, 0, 0); 238 } else { 239 if (!$this->sftp->login($user, $pass)) { 240 return false; 241 } 242 } 243 self::$instances[$host][$port][$user][(string) $pass] = $this->sftp; 244 } 245 } 246 247 return $path; 248 } 249 250 /** 251 * Opens file or URL 252 * 253 * @param string $path 254 * @param string $mode 255 * @param int $options 256 * @param string $opened_path 257 * @return bool 258 * @access public 259 */ 260 function _stream_open($path, $mode, $options, &$opened_path) 261 { 262 $path = $this->_parse_path($path); 263 264 if ($path === false) { 265 return false; 266 } 267 $this->path = $path; 268 269 $this->size = $this->sftp->size($path); 270 $this->mode = preg_replace('#[bt]$#', '', $mode); 271 $this->eof = false; 272 273 if ($this->size === false) { 274 if ($this->mode[0] == 'r') { 275 return false; 276 } else { 277 $this->sftp->touch($path); 278 $this->size = 0; 279 } 280 } else { 281 switch ($this->mode[0]) { 282 case 'x': 283 return false; 284 case 'w': 285 $this->sftp->truncate($path, 0); 286 $this->size = 0; 287 } 288 } 289 290 $this->pos = $this->mode[0] != 'a' ? 0 : $this->size; 291 292 return true; 293 } 294 295 /** 296 * Read from stream 297 * 298 * @param int $count 299 * @return mixed 300 * @access public 301 */ 302 function _stream_read($count) 303 { 304 switch ($this->mode) { 305 case 'w': 306 case 'a': 307 case 'x': 308 case 'c': 309 return false; 310 } 311 312 // commented out because some files - eg. /dev/urandom - will say their size is 0 when in fact it's kinda infinite 313 //if ($this->pos >= $this->size) { 314 // $this->eof = true; 315 // return false; 316 //} 317 318 $result = $this->sftp->get($this->path, false, $this->pos, $count); 319 if (isset($this->notification) && is_callable($this->notification)) { 320 if ($result === false) { 321 call_user_func($this->notification, STREAM_NOTIFY_FAILURE, STREAM_NOTIFY_SEVERITY_ERR, $this->sftp->getLastSFTPError(), NET_SFTP_OPEN, 0, 0); 322 return 0; 323 } 324 // seems that PHP calls stream_read in 8k chunks 325 call_user_func($this->notification, STREAM_NOTIFY_PROGRESS, STREAM_NOTIFY_SEVERITY_INFO, '', 0, strlen($result), $this->size); 326 } 327 328 if (empty($result)) { // ie. false or empty string 329 $this->eof = true; 330 return false; 331 } 332 $this->pos+= strlen($result); 333 334 return $result; 335 } 336 337 /** 338 * Write to stream 339 * 340 * @param string $data 341 * @return mixed 342 * @access public 343 */ 344 function _stream_write($data) 345 { 346 switch ($this->mode) { 347 case 'r': 348 return false; 349 } 350 351 $result = $this->sftp->put($this->path, $data, SFTP::SOURCE_STRING, $this->pos); 352 if (isset($this->notification) && is_callable($this->notification)) { 353 if (!$result) { 354 call_user_func($this->notification, STREAM_NOTIFY_FAILURE, STREAM_NOTIFY_SEVERITY_ERR, $this->sftp->getLastSFTPError(), NET_SFTP_OPEN, 0, 0); 355 return 0; 356 } 357 // seems that PHP splits up strings into 8k blocks before calling stream_write 358 call_user_func($this->notification, STREAM_NOTIFY_PROGRESS, STREAM_NOTIFY_SEVERITY_INFO, '', 0, strlen($data), strlen($data)); 359 } 360 361 if ($result === false) { 362 return false; 363 } 364 $this->pos+= strlen($data); 365 if ($this->pos > $this->size) { 366 $this->size = $this->pos; 367 } 368 $this->eof = false; 369 return strlen($data); 370 } 371 372 /** 373 * Retrieve the current position of a stream 374 * 375 * @return int 376 * @access public 377 */ 378 function _stream_tell() 379 { 380 return $this->pos; 381 } 382 383 /** 384 * Tests for end-of-file on a file pointer 385 * 386 * In my testing there are four classes functions that normally effect the pointer: 387 * fseek, fputs / fwrite, fgets / fread and ftruncate. 388 * 389 * Only fgets / fread, however, results in feof() returning true. do fputs($fp, 'aaa') on a blank file and feof() 390 * will return false. do fread($fp, 1) and feof() will then return true. do fseek($fp, 10) on ablank file and feof() 391 * will return false. do fread($fp, 1) and feof() will then return true. 392 * 393 * @return bool 394 * @access public 395 */ 396 function _stream_eof() 397 { 398 return $this->eof; 399 } 400 401 /** 402 * Seeks to specific location in a stream 403 * 404 * @param int $offset 405 * @param int $whence 406 * @return bool 407 * @access public 408 */ 409 function _stream_seek($offset, $whence) 410 { 411 switch ($whence) { 412 case SEEK_SET: 413 if ($offset >= $this->size || $offset < 0) { 414 return false; 415 } 416 break; 417 case SEEK_CUR: 418 $offset+= $this->pos; 419 break; 420 case SEEK_END: 421 $offset+= $this->size; 422 } 423 424 $this->pos = $offset; 425 $this->eof = false; 426 return true; 427 } 428 429 /** 430 * Change stream options 431 * 432 * @param string $path 433 * @param int $option 434 * @param mixed $var 435 * @return bool 436 * @access public 437 */ 438 function _stream_metadata($path, $option, $var) 439 { 440 $path = $this->_parse_path($path); 441 if ($path === false) { 442 return false; 443 } 444 445 // stream_metadata was introduced in PHP 5.4.0 but as of 5.4.11 the constants haven't been defined 446 // see http://www.php.net/streamwrapper.stream-metadata and https://bugs.php.net/64246 447 // and https://github.com/php/php-src/blob/master/main/php_streams.h#L592 448 switch ($option) { 449 case 1: // PHP_STREAM_META_TOUCH 450 return $this->sftp->touch($path, $var[0], $var[1]); 451 case 2: // PHP_STREAM_OWNER_NAME 452 case 3: // PHP_STREAM_GROUP_NAME 453 return false; 454 case 4: // PHP_STREAM_META_OWNER 455 return $this->sftp->chown($path, $var); 456 case 5: // PHP_STREAM_META_GROUP 457 return $this->sftp->chgrp($path, $var); 458 case 6: // PHP_STREAM_META_ACCESS 459 return $this->sftp->chmod($path, $var) !== false; 460 } 461 } 462 463 /** 464 * Retrieve the underlaying resource 465 * 466 * @param int $cast_as 467 * @return resource 468 * @access public 469 */ 470 function _stream_cast($cast_as) 471 { 472 return $this->sftp->fsock; 473 } 474 475 /** 476 * Advisory file locking 477 * 478 * @param int $operation 479 * @return bool 480 * @access public 481 */ 482 function _stream_lock($operation) 483 { 484 return false; 485 } 486 487 /** 488 * Renames a file or directory 489 * 490 * Attempts to rename oldname to newname, moving it between directories if necessary. 491 * If newname exists, it will be overwritten. This is a departure from what \phpseclib\Net\SFTP 492 * does. 493 * 494 * @param string $path_from 495 * @param string $path_to 496 * @return bool 497 * @access public 498 */ 499 function _rename($path_from, $path_to) 500 { 501 $path1 = parse_url($path_from); 502 $path2 = parse_url($path_to); 503 unset($path1['path'], $path2['path']); 504 if ($path1 != $path2) { 505 return false; 506 } 507 508 $path_from = $this->_parse_path($path_from); 509 $path_to = parse_url($path_to); 510 if ($path_from === false) { 511 return false; 512 } 513 514 $path_to = $path_to['path']; // the $component part of parse_url() was added in PHP 5.1.2 515 // "It is an error if there already exists a file with the name specified by newpath." 516 // -- http://tools.ietf.org/html/draft-ietf-secsh-filexfer-02#section-6.5 517 if (!$this->sftp->rename($path_from, $path_to)) { 518 if ($this->sftp->stat($path_to)) { 519 return $this->sftp->delete($path_to, true) && $this->sftp->rename($path_from, $path_to); 520 } 521 return false; 522 } 523 524 return true; 525 } 526 527 /** 528 * Open directory handle 529 * 530 * The only $options is "whether or not to enforce safe_mode (0x04)". Since safe mode was deprecated in 5.3 and 531 * removed in 5.4 I'm just going to ignore it. 532 * 533 * Also, nlist() is the best that this function is realistically going to be able to do. When an SFTP client 534 * sends a SSH_FXP_READDIR packet you don't generally get info on just one file but on multiple files. Quoting 535 * the SFTP specs: 536 * 537 * The SSH_FXP_NAME response has the following format: 538 * 539 * uint32 id 540 * uint32 count 541 * repeats count times: 542 * string filename 543 * string longname 544 * ATTRS attrs 545 * 546 * @param string $path 547 * @param int $options 548 * @return bool 549 * @access public 550 */ 551 function _dir_opendir($path, $options) 552 { 553 $path = $this->_parse_path($path); 554 if ($path === false) { 555 return false; 556 } 557 $this->pos = 0; 558 $this->entries = $this->sftp->nlist($path); 559 return $this->entries !== false; 560 } 561 562 /** 563 * Read entry from directory handle 564 * 565 * @return mixed 566 * @access public 567 */ 568 function _dir_readdir() 569 { 570 if (isset($this->entries[$this->pos])) { 571 return $this->entries[$this->pos++]; 572 } 573 return false; 574 } 575 576 /** 577 * Rewind directory handle 578 * 579 * @return bool 580 * @access public 581 */ 582 function _dir_rewinddir() 583 { 584 $this->pos = 0; 585 return true; 586 } 587 588 /** 589 * Close directory handle 590 * 591 * @return bool 592 * @access public 593 */ 594 function _dir_closedir() 595 { 596 return true; 597 } 598 599 /** 600 * Create a directory 601 * 602 * Only valid $options is STREAM_MKDIR_RECURSIVE 603 * 604 * @param string $path 605 * @param int $mode 606 * @param int $options 607 * @return bool 608 * @access public 609 */ 610 function _mkdir($path, $mode, $options) 611 { 612 $path = $this->_parse_path($path); 613 if ($path === false) { 614 return false; 615 } 616 617 return $this->sftp->mkdir($path, $mode, $options & STREAM_MKDIR_RECURSIVE); 618 } 619 620 /** 621 * Removes a directory 622 * 623 * Only valid $options is STREAM_MKDIR_RECURSIVE per <http://php.net/streamwrapper.rmdir>, however, 624 * <http://php.net/rmdir> does not have a $recursive parameter as mkdir() does so I don't know how 625 * STREAM_MKDIR_RECURSIVE is supposed to be set. Also, when I try it out with rmdir() I get 8 as 626 * $options. What does 8 correspond to? 627 * 628 * @param string $path 629 * @param int $mode 630 * @param int $options 631 * @return bool 632 * @access public 633 */ 634 function _rmdir($path, $options) 635 { 636 $path = $this->_parse_path($path); 637 if ($path === false) { 638 return false; 639 } 640 641 return $this->sftp->rmdir($path); 642 } 643 644 /** 645 * Flushes the output 646 * 647 * See <http://php.net/fflush>. Always returns true because \phpseclib\Net\SFTP doesn't cache stuff before writing 648 * 649 * @return bool 650 * @access public 651 */ 652 function _stream_flush() 653 { 654 return true; 655 } 656 657 /** 658 * Retrieve information about a file resource 659 * 660 * @return mixed 661 * @access public 662 */ 663 function _stream_stat() 664 { 665 $results = $this->sftp->stat($this->path); 666 if ($results === false) { 667 return false; 668 } 669 return $results; 670 } 671 672 /** 673 * Delete a file 674 * 675 * @param string $path 676 * @return bool 677 * @access public 678 */ 679 function _unlink($path) 680 { 681 $path = $this->_parse_path($path); 682 if ($path === false) { 683 return false; 684 } 685 686 return $this->sftp->delete($path, false); 687 } 688 689 /** 690 * Retrieve information about a file 691 * 692 * Ignores the STREAM_URL_STAT_QUIET flag because the entirety of \phpseclib\Net\SFTP\Stream is quiet by default 693 * might be worthwhile to reconstruct bits 12-16 (ie. the file type) if mode doesn't have them but we'll 694 * cross that bridge when and if it's reached 695 * 696 * @param string $path 697 * @param int $flags 698 * @return mixed 699 * @access public 700 */ 701 function _url_stat($path, $flags) 702 { 703 $path = $this->_parse_path($path); 704 if ($path === false) { 705 return false; 706 } 707 708 $results = $flags & STREAM_URL_STAT_LINK ? $this->sftp->lstat($path) : $this->sftp->stat($path); 709 if ($results === false) { 710 return false; 711 } 712 713 return $results; 714 } 715 716 /** 717 * Truncate stream 718 * 719 * @param int $new_size 720 * @return bool 721 * @access public 722 */ 723 function _stream_truncate($new_size) 724 { 725 if (!$this->sftp->truncate($this->path, $new_size)) { 726 return false; 727 } 728 729 $this->eof = false; 730 $this->size = $new_size; 731 732 return true; 733 } 734 735 /** 736 * Change stream options 737 * 738 * STREAM_OPTION_WRITE_BUFFER isn't supported for the same reason stream_flush isn't. 739 * The other two aren't supported because of limitations in \phpseclib\Net\SFTP. 740 * 741 * @param int $option 742 * @param int $arg1 743 * @param int $arg2 744 * @return bool 745 * @access public 746 */ 747 function _stream_set_option($option, $arg1, $arg2) 748 { 749 return false; 750 } 751 752 /** 753 * Close an resource 754 * 755 * @access public 756 */ 757 function _stream_close() 758 { 759 } 760 761 /** 762 * __call Magic Method 763 * 764 * When you're utilizing an SFTP stream you're not calling the methods in this class directly - PHP is calling them for you. 765 * Which kinda begs the question... what methods is PHP calling and what parameters is it passing to them? This function 766 * lets you figure that out. 767 * 768 * If NET_SFTP_STREAM_LOGGING is defined all calls will be output on the screen and then (regardless of whether or not 769 * NET_SFTP_STREAM_LOGGING is enabled) the parameters will be passed through to the appropriate method. 770 * 771 * @param string 772 * @param array 773 * @return mixed 774 * @access public 775 */ 776 function __call($name, $arguments) 777 { 778 if (defined('NET_SFTP_STREAM_LOGGING')) { 779 echo $name . '('; 780 $last = count($arguments) - 1; 781 foreach ($arguments as $i => $argument) { 782 var_export($argument); 783 if ($i != $last) { 784 echo ','; 785 } 786 } 787 echo ")\r\n"; 788 } 789 $name = '_' . $name; 790 if (!method_exists($this, $name)) { 791 return false; 792 } 793 return call_user_func_array(array($this, $name), $arguments); 794 } 795} 796