Subversion Repositories cheapmusic

Rev

Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
103 - 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
 
18
namespace phpseclib\Net\SFTP;
19
 
20
use phpseclib\Crypt\RSA;
21
use phpseclib\Net\SFTP;
22
 
23
/**
24
 * SFTP Stream Wrapper
25
 *
26
 * @package SFTP
27
 * @author  Jim Wigginton <terrafrost@php.net>
28
 * @access  public
29
 */
30
class 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
}