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