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 phpseclib3\Net\SFTP; 19 20use phpseclib3\Crypt\Common\PrivateKey; 21use phpseclib3\Net\SFTP; 22use phpseclib3\Net\SSH2; 23 24/** 25 * SFTP Stream Wrapper 26 * 27 * @package SFTP 28 * @author Jim Wigginton <terrafrost@php.net> 29 * @access public 30 */ 31class Stream 32{ 33 /** 34 * SFTP instances 35 * 36 * Rather than re-create the connection we re-use instances if possible 37 * 38 * @var array 39 */ 40 public static $instances; 41 42 /** 43 * SFTP instance 44 * 45 * @var object 46 * @access private 47 */ 48 private $sftp; 49 50 /** 51 * Path 52 * 53 * @var string 54 * @access private 55 */ 56 private $path; 57 58 /** 59 * Mode 60 * 61 * @var string 62 * @access private 63 */ 64 private $mode; 65 66 /** 67 * Position 68 * 69 * @var int 70 * @access private 71 */ 72 private $pos; 73 74 /** 75 * Size 76 * 77 * @var int 78 * @access private 79 */ 80 private $size; 81 82 /** 83 * Directory entries 84 * 85 * @var array 86 * @access private 87 */ 88 private $entries; 89 90 /** 91 * EOF flag 92 * 93 * @var bool 94 * @access private 95 */ 96 private $eof; 97 98 /** 99 * Context resource 100 * 101 * Technically this needs to be publicly accessible so PHP can set it directly 102 * 103 * @var resource 104 * @access public 105 */ 106 public $context; 107 108 /** 109 * Notification callback function 110 * 111 * @var callable 112 * @access public 113 */ 114 private $notification; 115 116 /** 117 * Registers this class as a URL wrapper. 118 * 119 * @param string $protocol The wrapper name to be registered. 120 * @return bool True on success, false otherwise. 121 * @access public 122 */ 123 public static function register($protocol = 'sftp') 124 { 125 if (in_array($protocol, stream_get_wrappers(), true)) { 126 return false; 127 } 128 return stream_wrapper_register($protocol, get_called_class()); 129 } 130 131 /** 132 * The Constructor 133 * 134 * @access public 135 */ 136 public function __construct() 137 { 138 if (defined('NET_SFTP_STREAM_LOGGING')) { 139 echo "__construct()\r\n"; 140 } 141 } 142 143 /** 144 * Path Parser 145 * 146 * Extract a path from a URI and actually connect to an SSH server if appropriate 147 * 148 * If "notification" is set as a context parameter the message code for successful login is 149 * NET_SSH2_MSG_USERAUTH_SUCCESS. For a failed login it's NET_SSH2_MSG_USERAUTH_FAILURE. 150 * 151 * @param string $path 152 * @return string 153 * @access private 154 */ 155 protected function parse_path($path) 156 { 157 $orig = $path; 158 extract(parse_url($path) + ['port' => 22]); 159 if (isset($query)) { 160 $path .= '?' . $query; 161 } elseif (preg_match('/(\?|\?#)$/', $orig)) { 162 $path .= '?'; 163 } 164 if (isset($fragment)) { 165 $path .= '#' . $fragment; 166 } elseif ($orig[strlen($orig) - 1] == '#') { 167 $path .= '#'; 168 } 169 170 if (!isset($host)) { 171 return false; 172 } 173 174 if (isset($this->context)) { 175 $context = stream_context_get_params($this->context); 176 if (isset($context['notification'])) { 177 $this->notification = $context['notification']; 178 } 179 } 180 181 if (preg_match('/^{[a-z0-9]+}$/i', $host)) { 182 $host = SSH2::getConnectionByResourceId($host); 183 if ($host === 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 PrivateKey) { 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 \phpseclib3\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 private 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->filesize($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 private 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 int|false 342 * @access public 343 */ 344 private 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 private 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 private 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 private function _stream_seek($offset, $whence) 410 { 411 switch ($whence) { 412 case SEEK_SET: 413 if ($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 private 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 $time = isset($var[0]) ? $var[0] : null; 451 $atime = isset($var[1]) ? $var[1] : null; 452 return $this->sftp->touch($path, $time, $atime); 453 case 2: // PHP_STREAM_OWNER_NAME 454 case 3: // PHP_STREAM_GROUP_NAME 455 return false; 456 case 4: // PHP_STREAM_META_OWNER 457 return $this->sftp->chown($path, $var); 458 case 5: // PHP_STREAM_META_GROUP 459 return $this->sftp->chgrp($path, $var); 460 case 6: // PHP_STREAM_META_ACCESS 461 return $this->sftp->chmod($path, $var) !== false; 462 } 463 } 464 465 /** 466 * Retrieve the underlaying resource 467 * 468 * @param int $cast_as 469 * @return resource 470 * @access public 471 */ 472 private function _stream_cast($cast_as) 473 { 474 return $this->sftp->fsock; 475 } 476 477 /** 478 * Advisory file locking 479 * 480 * @param int $operation 481 * @return bool 482 * @access public 483 */ 484 private function _stream_lock($operation) 485 { 486 return false; 487 } 488 489 /** 490 * Renames a file or directory 491 * 492 * Attempts to rename oldname to newname, moving it between directories if necessary. 493 * If newname exists, it will be overwritten. This is a departure from what \phpseclib3\Net\SFTP 494 * does. 495 * 496 * @param string $path_from 497 * @param string $path_to 498 * @return bool 499 * @access public 500 */ 501 private function _rename($path_from, $path_to) 502 { 503 $path1 = parse_url($path_from); 504 $path2 = parse_url($path_to); 505 unset($path1['path'], $path2['path']); 506 if ($path1 != $path2) { 507 return false; 508 } 509 510 $path_from = $this->parse_path($path_from); 511 $path_to = parse_url($path_to); 512 if ($path_from === false) { 513 return false; 514 } 515 516 $path_to = $path_to['path']; // the $component part of parse_url() was added in PHP 5.1.2 517 // "It is an error if there already exists a file with the name specified by newpath." 518 // -- http://tools.ietf.org/html/draft-ietf-secsh-filexfer-02#section-6.5 519 if (!$this->sftp->rename($path_from, $path_to)) { 520 if ($this->sftp->stat($path_to)) { 521 return $this->sftp->delete($path_to, true) && $this->sftp->rename($path_from, $path_to); 522 } 523 return false; 524 } 525 526 return true; 527 } 528 529 /** 530 * Open directory handle 531 * 532 * The only $options is "whether or not to enforce safe_mode (0x04)". Since safe mode was deprecated in 5.3 and 533 * removed in 5.4 I'm just going to ignore it. 534 * 535 * Also, nlist() is the best that this function is realistically going to be able to do. When an SFTP client 536 * sends a SSH_FXP_READDIR packet you don't generally get info on just one file but on multiple files. Quoting 537 * the SFTP specs: 538 * 539 * The SSH_FXP_NAME response has the following format: 540 * 541 * uint32 id 542 * uint32 count 543 * repeats count times: 544 * string filename 545 * string longname 546 * ATTRS attrs 547 * 548 * @param string $path 549 * @param int $options 550 * @return bool 551 * @access public 552 */ 553 private function _dir_opendir($path, $options) 554 { 555 $path = $this->parse_path($path); 556 if ($path === false) { 557 return false; 558 } 559 $this->pos = 0; 560 $this->entries = $this->sftp->nlist($path); 561 return $this->entries !== false; 562 } 563 564 /** 565 * Read entry from directory handle 566 * 567 * @return mixed 568 * @access public 569 */ 570 private function _dir_readdir() 571 { 572 if (isset($this->entries[$this->pos])) { 573 return $this->entries[$this->pos++]; 574 } 575 return false; 576 } 577 578 /** 579 * Rewind directory handle 580 * 581 * @return bool 582 * @access public 583 */ 584 private function _dir_rewinddir() 585 { 586 $this->pos = 0; 587 return true; 588 } 589 590 /** 591 * Close directory handle 592 * 593 * @return bool 594 * @access public 595 */ 596 private function _dir_closedir() 597 { 598 return true; 599 } 600 601 /** 602 * Create a directory 603 * 604 * Only valid $options is STREAM_MKDIR_RECURSIVE 605 * 606 * @param string $path 607 * @param int $mode 608 * @param int $options 609 * @return bool 610 * @access public 611 */ 612 private function _mkdir($path, $mode, $options) 613 { 614 $path = $this->parse_path($path); 615 if ($path === false) { 616 return false; 617 } 618 619 return $this->sftp->mkdir($path, $mode, $options & STREAM_MKDIR_RECURSIVE); 620 } 621 622 /** 623 * Removes a directory 624 * 625 * Only valid $options is STREAM_MKDIR_RECURSIVE per <http://php.net/streamwrapper.rmdir>, however, 626 * <http://php.net/rmdir> does not have a $recursive parameter as mkdir() does so I don't know how 627 * STREAM_MKDIR_RECURSIVE is supposed to be set. Also, when I try it out with rmdir() I get 8 as 628 * $options. What does 8 correspond to? 629 * 630 * @param string $path 631 * @param int $options 632 * @return bool 633 * @access public 634 */ 635 private function _rmdir($path, $options) 636 { 637 $path = $this->parse_path($path); 638 if ($path === false) { 639 return false; 640 } 641 642 return $this->sftp->rmdir($path); 643 } 644 645 /** 646 * Flushes the output 647 * 648 * See <http://php.net/fflush>. Always returns true because \phpseclib3\Net\SFTP doesn't cache stuff before writing 649 * 650 * @return bool 651 * @access public 652 */ 653 private function _stream_flush() 654 { 655 return true; 656 } 657 658 /** 659 * Retrieve information about a file resource 660 * 661 * @return mixed 662 * @access public 663 */ 664 private function _stream_stat() 665 { 666 $results = $this->sftp->stat($this->path); 667 if ($results === false) { 668 return false; 669 } 670 return $results; 671 } 672 673 /** 674 * Delete a file 675 * 676 * @param string $path 677 * @return bool 678 * @access public 679 */ 680 private function _unlink($path) 681 { 682 $path = $this->parse_path($path); 683 if ($path === false) { 684 return false; 685 } 686 687 return $this->sftp->delete($path, false); 688 } 689 690 /** 691 * Retrieve information about a file 692 * 693 * Ignores the STREAM_URL_STAT_QUIET flag because the entirety of \phpseclib3\Net\SFTP\Stream is quiet by default 694 * might be worthwhile to reconstruct bits 12-16 (ie. the file type) if mode doesn't have them but we'll 695 * cross that bridge when and if it's reached 696 * 697 * @param string $path 698 * @param int $flags 699 * @return mixed 700 * @access public 701 */ 702 private function _url_stat($path, $flags) 703 { 704 $path = $this->parse_path($path); 705 if ($path === false) { 706 return false; 707 } 708 709 $results = $flags & STREAM_URL_STAT_LINK ? $this->sftp->lstat($path) : $this->sftp->stat($path); 710 if ($results === false) { 711 return false; 712 } 713 714 return $results; 715 } 716 717 /** 718 * Truncate stream 719 * 720 * @param int $new_size 721 * @return bool 722 * @access public 723 */ 724 private function _stream_truncate($new_size) 725 { 726 if (!$this->sftp->truncate($this->path, $new_size)) { 727 return false; 728 } 729 730 $this->eof = false; 731 $this->size = $new_size; 732 733 return true; 734 } 735 736 /** 737 * Change stream options 738 * 739 * STREAM_OPTION_WRITE_BUFFER isn't supported for the same reason stream_flush isn't. 740 * The other two aren't supported because of limitations in \phpseclib3\Net\SFTP. 741 * 742 * @param int $option 743 * @param int $arg1 744 * @param int $arg2 745 * @return bool 746 * @access public 747 */ 748 private function _stream_set_option($option, $arg1, $arg2) 749 { 750 return false; 751 } 752 753 /** 754 * Close an resource 755 * 756 * @access public 757 */ 758 private function _stream_close() 759 { 760 } 761 762 /** 763 * __call Magic Method 764 * 765 * When you're utilizing an SFTP stream you're not calling the methods in this class directly - PHP is calling them for you. 766 * Which kinda begs the question... what methods is PHP calling and what parameters is it passing to them? This function 767 * lets you figure that out. 768 * 769 * If NET_SFTP_STREAM_LOGGING is defined all calls will be output on the screen and then (regardless of whether or not 770 * NET_SFTP_STREAM_LOGGING is enabled) the parameters will be passed through to the appropriate method. 771 * 772 * @param string $name 773 * @param array $arguments 774 * @return mixed 775 * @access public 776 */ 777 public function __call($name, $arguments) 778 { 779 if (defined('NET_SFTP_STREAM_LOGGING')) { 780 echo $name . '('; 781 $last = count($arguments) - 1; 782 foreach ($arguments as $i => $argument) { 783 var_export($argument); 784 if ($i != $last) { 785 echo ','; 786 } 787 } 788 echo ")\r\n"; 789 } 790 $name = '_' . $name; 791 if (!method_exists($this, $name)) { 792 return false; 793 } 794 return $this->$name(...$arguments); 795 } 796} 797