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 < 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                $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    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    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 \phpseclib\Net\SFTP
494     * does.
495     *
496     * @param string $path_from
497     * @param string $path_to
498     * @return bool
499     * @access public
500     */
501    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    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    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    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    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    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    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 \phpseclib\Net\SFTP doesn't cache stuff before writing
649     *
650     * @return bool
651     * @access public
652     */
653    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    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    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 \phpseclib\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    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    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 \phpseclib\Net\SFTP.
741     *
742     * @param int $option
743     * @param int $arg1
744     * @param int $arg2
745     * @return bool
746     * @access public
747     */
748    function _stream_set_option($option, $arg1, $arg2)
749    {
750        return false;
751    }
752
753    /**
754     * Close an resource
755     *
756     * @access public
757     */
758    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    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 call_user_func_array(array($this, $name), $arguments);
795    }
796}
797