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