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