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 >= $this->size || $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                return $this->sftp->touch($path, $var[0], $var[1]);
451            case 2: // PHP_STREAM_OWNER_NAME
452            case 3: // PHP_STREAM_GROUP_NAME
453                return false;
454            case 4: // PHP_STREAM_META_OWNER
455                return $this->sftp->chown($path, $var);
456            case 5: // PHP_STREAM_META_GROUP
457                return $this->sftp->chgrp($path, $var);
458            case 6: // PHP_STREAM_META_ACCESS
459                return $this->sftp->chmod($path, $var) !== false;
460        }
461    }
462
463    /**
464     * Retrieve the underlaying resource
465     *
466     * @param int $cast_as
467     * @return resource
468     * @access public
469     */
470    function _stream_cast($cast_as)
471    {
472        return $this->sftp->fsock;
473    }
474
475    /**
476     * Advisory file locking
477     *
478     * @param int $operation
479     * @return bool
480     * @access public
481     */
482    function _stream_lock($operation)
483    {
484        return false;
485    }
486
487    /**
488     * Renames a file or directory
489     *
490     * Attempts to rename oldname to newname, moving it between directories if necessary.
491     * If newname exists, it will be overwritten.  This is a departure from what \phpseclib\Net\SFTP
492     * does.
493     *
494     * @param string $path_from
495     * @param string $path_to
496     * @return bool
497     * @access public
498     */
499    function _rename($path_from, $path_to)
500    {
501        $path1 = parse_url($path_from);
502        $path2 = parse_url($path_to);
503        unset($path1['path'], $path2['path']);
504        if ($path1 != $path2) {
505            return false;
506        }
507
508        $path_from = $this->_parse_path($path_from);
509        $path_to = parse_url($path_to);
510        if ($path_from === false) {
511            return false;
512        }
513
514        $path_to = $path_to['path']; // the $component part of parse_url() was added in PHP 5.1.2
515        // "It is an error if there already exists a file with the name specified by newpath."
516        //  -- http://tools.ietf.org/html/draft-ietf-secsh-filexfer-02#section-6.5
517        if (!$this->sftp->rename($path_from, $path_to)) {
518            if ($this->sftp->stat($path_to)) {
519                return $this->sftp->delete($path_to, true) && $this->sftp->rename($path_from, $path_to);
520            }
521            return false;
522        }
523
524        return true;
525    }
526
527    /**
528     * Open directory handle
529     *
530     * The only $options is "whether or not to enforce safe_mode (0x04)". Since safe mode was deprecated in 5.3 and
531     * removed in 5.4 I'm just going to ignore it.
532     *
533     * Also, nlist() is the best that this function is realistically going to be able to do. When an SFTP client
534     * sends a SSH_FXP_READDIR packet you don't generally get info on just one file but on multiple files. Quoting
535     * the SFTP specs:
536     *
537     *    The SSH_FXP_NAME response has the following format:
538     *
539     *        uint32     id
540     *        uint32     count
541     *        repeats count times:
542     *                string     filename
543     *                string     longname
544     *                ATTRS      attrs
545     *
546     * @param string $path
547     * @param int $options
548     * @return bool
549     * @access public
550     */
551    function _dir_opendir($path, $options)
552    {
553        $path = $this->_parse_path($path);
554        if ($path === false) {
555            return false;
556        }
557        $this->pos = 0;
558        $this->entries = $this->sftp->nlist($path);
559        return $this->entries !== false;
560    }
561
562    /**
563     * Read entry from directory handle
564     *
565     * @return mixed
566     * @access public
567     */
568    function _dir_readdir()
569    {
570        if (isset($this->entries[$this->pos])) {
571            return $this->entries[$this->pos++];
572        }
573        return false;
574    }
575
576    /**
577     * Rewind directory handle
578     *
579     * @return bool
580     * @access public
581     */
582    function _dir_rewinddir()
583    {
584        $this->pos = 0;
585        return true;
586    }
587
588    /**
589     * Close directory handle
590     *
591     * @return bool
592     * @access public
593     */
594    function _dir_closedir()
595    {
596        return true;
597    }
598
599    /**
600     * Create a directory
601     *
602     * Only valid $options is STREAM_MKDIR_RECURSIVE
603     *
604     * @param string $path
605     * @param int $mode
606     * @param int $options
607     * @return bool
608     * @access public
609     */
610    function _mkdir($path, $mode, $options)
611    {
612        $path = $this->_parse_path($path);
613        if ($path === false) {
614            return false;
615        }
616
617        return $this->sftp->mkdir($path, $mode, $options & STREAM_MKDIR_RECURSIVE);
618    }
619
620    /**
621     * Removes a directory
622     *
623     * Only valid $options is STREAM_MKDIR_RECURSIVE per <http://php.net/streamwrapper.rmdir>, however,
624     * <http://php.net/rmdir>  does not have a $recursive parameter as mkdir() does so I don't know how
625     * STREAM_MKDIR_RECURSIVE is supposed to be set. Also, when I try it out with rmdir() I get 8 as
626     * $options. What does 8 correspond to?
627     *
628     * @param string $path
629     * @param int $mode
630     * @param int $options
631     * @return bool
632     * @access public
633     */
634    function _rmdir($path, $options)
635    {
636        $path = $this->_parse_path($path);
637        if ($path === false) {
638            return false;
639        }
640
641        return $this->sftp->rmdir($path);
642    }
643
644    /**
645     * Flushes the output
646     *
647     * See <http://php.net/fflush>. Always returns true because \phpseclib\Net\SFTP doesn't cache stuff before writing
648     *
649     * @return bool
650     * @access public
651     */
652    function _stream_flush()
653    {
654        return true;
655    }
656
657    /**
658     * Retrieve information about a file resource
659     *
660     * @return mixed
661     * @access public
662     */
663    function _stream_stat()
664    {
665        $results = $this->sftp->stat($this->path);
666        if ($results === false) {
667            return false;
668        }
669        return $results;
670    }
671
672    /**
673     * Delete a file
674     *
675     * @param string $path
676     * @return bool
677     * @access public
678     */
679    function _unlink($path)
680    {
681        $path = $this->_parse_path($path);
682        if ($path === false) {
683            return false;
684        }
685
686        return $this->sftp->delete($path, false);
687    }
688
689    /**
690     * Retrieve information about a file
691     *
692     * Ignores the STREAM_URL_STAT_QUIET flag because the entirety of \phpseclib\Net\SFTP\Stream is quiet by default
693     * might be worthwhile to reconstruct bits 12-16 (ie. the file type) if mode doesn't have them but we'll
694     * cross that bridge when and if it's reached
695     *
696     * @param string $path
697     * @param int $flags
698     * @return mixed
699     * @access public
700     */
701    function _url_stat($path, $flags)
702    {
703        $path = $this->_parse_path($path);
704        if ($path === false) {
705            return false;
706        }
707
708        $results = $flags & STREAM_URL_STAT_LINK ? $this->sftp->lstat($path) : $this->sftp->stat($path);
709        if ($results === false) {
710            return false;
711        }
712
713        return $results;
714    }
715
716    /**
717     * Truncate stream
718     *
719     * @param int $new_size
720     * @return bool
721     * @access public
722     */
723    function _stream_truncate($new_size)
724    {
725        if (!$this->sftp->truncate($this->path, $new_size)) {
726            return false;
727        }
728
729        $this->eof = false;
730        $this->size = $new_size;
731
732        return true;
733    }
734
735    /**
736     * Change stream options
737     *
738     * STREAM_OPTION_WRITE_BUFFER isn't supported for the same reason stream_flush isn't.
739     * The other two aren't supported because of limitations in \phpseclib\Net\SFTP.
740     *
741     * @param int $option
742     * @param int $arg1
743     * @param int $arg2
744     * @return bool
745     * @access public
746     */
747    function _stream_set_option($option, $arg1, $arg2)
748    {
749        return false;
750    }
751
752    /**
753     * Close an resource
754     *
755     * @access public
756     */
757    function _stream_close()
758    {
759    }
760
761    /**
762     * __call Magic Method
763     *
764     * When you're utilizing an SFTP stream you're not calling the methods in this class directly - PHP is calling them for you.
765     * Which kinda begs the question... what methods is PHP calling and what parameters is it passing to them? This function
766     * lets you figure that out.
767     *
768     * If NET_SFTP_STREAM_LOGGING is defined all calls will be output on the screen and then (regardless of whether or not
769     * NET_SFTP_STREAM_LOGGING is enabled) the parameters will be passed through to the appropriate method.
770     *
771     * @param string
772     * @param array
773     * @return mixed
774     * @access public
775     */
776    function __call($name, $arguments)
777    {
778        if (defined('NET_SFTP_STREAM_LOGGING')) {
779            echo $name . '(';
780            $last = count($arguments) - 1;
781            foreach ($arguments as $i => $argument) {
782                var_export($argument);
783                if ($i != $last) {
784                    echo ',';
785                }
786            }
787            echo ")\r\n";
788        }
789        $name = '_' . $name;
790        if (!method_exists($this, $name)) {
791            return false;
792        }
793        return call_user_func_array(array($this, $name), $arguments);
794    }
795}
796