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