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