xref: /dokuwiki/vendor/phpseclib/phpseclib/phpseclib/Net/SCP.php (revision 8e88a29b81301f78509349ab1152bb09c229123e)
1<?php
2
3/**
4 * Pure-PHP implementation of SCP.
5 *
6 * PHP version 5
7 *
8 * The API for this library is modeled after the API from PHP's {@link http://php.net/book.ftp FTP extension}.
9 *
10 * Here's a short example of how to use this library:
11 * <code>
12 * <?php
13 *    include 'vendor/autoload.php';
14 *
15 *    $scp = new \phpseclib3\Net\SCP('www.domain.tld');
16 *    if (!$scp->login('username', 'password')) {
17 *        exit('Login Failed');
18 *    }
19 *
20 *    echo $scp->exec('pwd') . "\r\n";
21 *    $scp->put('filename.ext', 'hello, world!');
22 *    echo $scp->exec('ls -latr');
23 * ?>
24 * </code>
25 *
26 * @author    Jim Wigginton <terrafrost@php.net>
27 * @copyright 2009 Jim Wigginton
28 * @license   http://www.opensource.org/licenses/mit-license.html  MIT License
29 * @link      http://phpseclib.sourceforge.net
30 */
31
32namespace phpseclib3\Net;
33
34use phpseclib3\Common\Functions\Strings;
35use phpseclib3\Exception\FileNotFoundException;
36
37/**
38 * Pure-PHP implementations of SCP.
39 *
40 * @author  Jim Wigginton <terrafrost@php.net>
41 */
42class SCP extends SSH2
43{
44    /**
45     * Reads data from a local file.
46     *
47     * @see \phpseclib3\Net\SCP::put()
48     */
49    const SOURCE_LOCAL_FILE = 1;
50    /**
51     * Reads data from a string.
52     *
53     * @see \phpseclib3\Net\SCP::put()
54     */
55    // this value isn't really used anymore but i'm keeping it reserved for historical reasons
56    const SOURCE_STRING = 2;
57    /**
58     * SCP.php doesn't support SOURCE_CALLBACK because, with that one, we don't know the size, in advance
59     */
60    //const SOURCE_CALLBACK = 16;
61
62    /**
63     * Error information
64     *
65     * @see self::getSCPErrors()
66     * @see self::getLastSCPError()
67     * @var array
68     */
69    private $scp_errors = [];
70
71    /**
72     * Uploads a file to the SCP server.
73     *
74     * By default, \phpseclib\Net\SCP::put() does not read from the local filesystem.  $data is dumped directly into $remote_file.
75     * So, for example, if you set $data to 'filename.ext' and then do \phpseclib\Net\SCP::get(), you will get a file, twelve bytes
76     * long, containing 'filename.ext' as its contents.
77     *
78     * Setting $mode to self::SOURCE_LOCAL_FILE will change the above behavior.  With self::SOURCE_LOCAL_FILE, $remote_file will
79     * contain as many bytes as filename.ext does on your local filesystem.  If your filename.ext is 1MB then that is how
80     * large $remote_file will be, as well.
81     *
82     * Currently, only binary mode is supported.  As such, if the line endings need to be adjusted, you will need to take
83     * care of that, yourself.
84     *
85     * @param string $remote_file
86     * @param string $data
87     * @param int $mode
88     * @param callable $callback
89     * @return bool
90     * @access public
91     */
92    public function put($remote_file, $data, $mode = self::SOURCE_STRING, $callback = null)
93    {
94        if (!($this->bitmap & self::MASK_LOGIN)) {
95            return false;
96        }
97
98        if (empty($remote_file)) {
99            // remote file cannot be blank
100            return false;
101        }
102
103        if (!$this->exec('scp -t ' . escapeshellarg($remote_file), false)) { // -t = to
104            return false;
105        }
106
107        $temp = $this->get_channel_packet(self::CHANNEL_EXEC, true);
108        if ($temp !== chr(0)) {
109            $this->close_channel(self::CHANNEL_EXEC, true);
110            return false;
111        }
112
113        $packet_size = $this->packet_size_client_to_server[self::CHANNEL_EXEC] - 4;
114
115        $remote_file = basename($remote_file);
116
117        $dataCallback = false;
118        switch (true) {
119            case is_resource($data):
120                $mode = $mode & ~self::SOURCE_LOCAL_FILE;
121                $info = stream_get_meta_data($data);
122                if (isset($info['wrapper_type']) && $info['wrapper_type'] == 'PHP' && $info['stream_type'] == 'Input') {
123                    $fp = fopen('php://memory', 'w+');
124                    stream_copy_to_stream($data, $fp);
125                    rewind($fp);
126                } else {
127                    $fp = $data;
128                }
129                break;
130            case $mode & self::SOURCE_LOCAL_FILE:
131                if (!is_file($data)) {
132                    throw new FileNotFoundException("$data is not a valid file");
133                }
134                $fp = @fopen($data, 'rb');
135                if (!$fp) {
136                    $this->close_channel(self::CHANNEL_EXEC, true);
137                    return false;
138                }
139        }
140
141        if (isset($fp)) {
142            $stat = fstat($fp);
143            $size = !empty($stat) ? $stat['size'] : 0;
144        } else {
145            $size = strlen($data);
146        }
147
148        $sent = 0;
149        $size = $size < 0 ? ($size & 0x7FFFFFFF) + 0x80000000 : $size;
150
151        $temp = 'C0644 ' . $size . ' ' . $remote_file . "\n";
152        $this->send_channel_packet(self::CHANNEL_EXEC, $temp);
153
154        $temp = $this->get_channel_packet(self::CHANNEL_EXEC, true);
155        if ($temp !== chr(0)) {
156            $this->close_channel(self::CHANNEL_EXEC, true);
157            return false;
158        }
159
160        $sent = 0;
161        while ($sent < $size) {
162            $temp = $mode & self::SOURCE_STRING ? substr($data, $sent, $packet_size) : fread($fp, $packet_size);
163            $this->send_channel_packet(self::CHANNEL_EXEC, $temp);
164            $sent += strlen($temp);
165
166            if (is_callable($callback)) {
167                call_user_func($callback, $sent);
168            }
169        }
170        $this->close_channel(self::CHANNEL_EXEC, true);
171
172        if ($mode != self::SOURCE_STRING) {
173            fclose($fp);
174        }
175
176        return true;
177    }
178
179    /**
180     * Downloads a file from the SCP server.
181     *
182     * Returns a string containing the contents of $remote_file if $local_file is left undefined or a boolean false if
183     * the operation was unsuccessful.  If $local_file is defined, returns true or false depending on the success of the
184     * operation
185     *
186     * @param string $remote_file
187     * @param string $local_file
188     * @return mixed
189     * @access public
190     */
191    public function get($remote_file, $local_file = null, $progressCallback = null)
192    {
193        if (!($this->bitmap & self::MASK_LOGIN)) {
194            return false;
195        }
196
197        if (!$this->exec('scp -f ' . escapeshellarg($remote_file), false)) { // -f = from
198            return false;
199        }
200
201        $this->send_channel_packet(self::CHANNEL_EXEC, chr(0));
202
203        $info = $this->get_channel_packet(self::CHANNEL_EXEC, true);
204        // per https://goteleport.com/blog/scp-familiar-simple-insecure-slow/ non-zero responses mean there are errors
205        if ($info[0] === chr(1) || $info[0] == chr(2)) {
206            $type = $info[0] === chr(1) ? 'warning' : 'error';
207            $this->scp_errors[] = "$type: " . substr($info, 1);
208            $this->close_channel(self::CHANNEL_EXEC, true);
209            return false;
210        }
211
212        $this->send_channel_packet(self::CHANNEL_EXEC, chr(0));
213
214        if (!preg_match('#(?<perms>[^ ]+) (?<size>\d+) (?<name>.+)#', rtrim($info), $info)) {
215            $this->close_channel(self::CHANNEL_EXEC, true);
216            return false;
217        }
218
219        $fclose_check = false;
220        if (is_resource($local_file)) {
221            $fp = $local_file;
222        } elseif (!is_null($local_file)) {
223            $fp = @fopen($local_file, 'wb');
224            if (!$fp) {
225                $this->close_channel(self::CHANNEL_EXEC, true);
226                return false;
227            }
228            $fclose_check = true;
229        } else {
230            $content = '';
231        }
232
233        $size = 0;
234        while (true) {
235            $data = $this->get_channel_packet(self::CHANNEL_EXEC, true);
236            // Terminate the loop in case the server repeatedly sends an empty response
237            if ($data === false) {
238                $this->close_channel(self::CHANNEL_EXEC, true);
239                // no data received from server
240                return false;
241            }
242            // SCP usually seems to split stuff out into 16k chunks
243            $length = strlen($data);
244            $size += $length;
245            $end = $size > $info['size'];
246            if ($end) {
247                $diff = $size - $info['size'];
248                $offset = $length - $diff;
249                if ($data[$offset] === chr(0)) {
250                    $data = substr($data, 0, -$diff);
251                } else {
252                    $type = $data[$offset] === chr(1) ? 'warning' : 'error';
253                    $this->scp_errors[] = "$type: " . substr($data, 1);
254                    $this->close_channel(self::CHANNEL_EXEC, true);
255                    return false;
256                }
257            }
258
259            if (is_null($local_file)) {
260                $content .= $data;
261            } else {
262                fputs($fp, $data);
263            }
264
265            if (is_callable($progressCallback)) {
266                call_user_func($progressCallback, $size);
267            }
268
269            if ($end) {
270                break;
271            }
272        }
273
274        $this->close_channel(self::CHANNEL_EXEC, true);
275
276        if ($fclose_check) {
277            fclose($fp);
278        }
279
280        // if $content isn't set that means a file was written to
281        return isset($content) ? $content : true;
282    }
283
284    /**
285     * Returns all errors on the SCP layer
286     *
287     * @return array
288     */
289    public function getSCPErrors()
290    {
291        return $this->scp_errors;
292    }
293
294    /**
295     * Returns the last error on the SCP layer
296     *
297     * @return string
298     */
299    public function getLastSCPError()
300    {
301        return count($this->scp_errors) ? $this->scp_errors[count($this->scp_errors) - 1] : '';
302    }
303}
304