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