See Release Notes
Long Term Support Release
Differences Between: [Versions 39 and 401]
1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 /** 18 * ClamAV antivirus integration. 19 * 20 * @package antivirus_clamav 21 * @copyright 2015 Ruslan Kabalin, Lancaster University. 22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 */ 24 25 namespace antivirus_clamav; 26 27 defined('MOODLE_INTERNAL') || die(); 28 29 /** Default socket timeout */ 30 define('ANTIVIRUS_CLAMAV_SOCKET_TIMEOUT', 10); 31 /** Default socket data stream chunk size (32Mb: 32 * 1024 * 1024) */ 32 define('ANTIVIRUS_CLAMAV_SOCKET_CHUNKSIZE', 33554432); 33 34 /** 35 * Class implementing ClamAV antivirus. 36 * @copyright 2015 Ruslan Kabalin, Lancaster University. 37 * @copyright 2019 Didier Raboud, Liip AG. 38 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 39 */ 40 class scanner extends \core\antivirus\scanner { 41 /** 42 * Are the necessary antivirus settings configured? 43 * 44 * @return bool True if all necessary config settings been entered 45 */ 46 public function is_configured() { 47 if ($this->get_config('runningmethod') === 'commandline') { 48 return (bool)$this->get_config('pathtoclam'); 49 } else if ($this->get_config('runningmethod') === 'unixsocket') { 50 return (bool)$this->get_config('pathtounixsocket'); 51 } else if ($this->get_config('runningmethod') === 'tcpsocket') { 52 return (bool)$this->get_config('tcpsockethost') && (bool)$this->get_config('tcpsocketport'); 53 } 54 return false; 55 } 56 57 /** 58 * Scan file. 59 * 60 * This method is normally called from antivirus manager (\core\antivirus\manager::scan_file). 61 * 62 * @param string $file Full path to the file. 63 * @param string $filename Name of the file (could be different from physical file if temp file is used). 64 * @return int Scanning result constant. 65 */ 66 public function scan_file($file, $filename) { 67 if (!is_readable($file)) { 68 // This should not happen. 69 debugging('File is not readable.'); 70 return self::SCAN_RESULT_ERROR; 71 } 72 73 // We can do direct stream scanning if unixsocket or tcpsocket running methods are in use, 74 // if not, use default process. 75 $maxtries = get_config('antivirus_clamav', 'tries'); 76 $tries = 0; 77 do { 78 $runningmethod = $this->get_config('runningmethod'); 79 $tries++; 80 switch ($runningmethod) { 81 case 'unixsocket': 82 case 'tcpsocket': 83 $return = $this->scan_file_execute_socket($file, $runningmethod); 84 break; 85 case 'commandline': 86 $return = $this->scan_file_execute_commandline($file); 87 break; 88 default: 89 // This should not happen. 90 throw new \coding_exception('Unknown running method.'); 91 } 92 } while ($return == self::SCAN_RESULT_ERROR && $tries < $maxtries); 93 94 $notice = get_string('tries_notice', 'antivirus_clamav', 95 ['tries' => $tries, 'notice' => $this->get_scanning_notice()]); 96 $this->set_scanning_notice($notice); 97 98 if ($return === self::SCAN_RESULT_ERROR) { 99 $this->message_admins($this->get_scanning_notice()); 100 // If plugin settings require us to act like virus on any error, 101 // return SCAN_RESULT_FOUND result. 102 if ($this->get_config('clamfailureonupload') === 'actlikevirus') { 103 return self::SCAN_RESULT_FOUND; 104 } else if ($this->get_config('clamfailureonupload') === 'tryagain') { 105 // Do not upload the file, just give a message to the user to try again later. 106 unlink($file); 107 throw new \core\antivirus\scanner_exception('antivirusfailed', '', ['item' => $filename], 108 null, 'antivirus_clamav'); 109 } 110 } 111 return $return; 112 } 113 114 /** 115 * Scan data. 116 * 117 * @param string $data The variable containing the data to scan. 118 * @return int Scanning result constant. 119 */ 120 public function scan_data($data) { 121 // We can do direct stream scanning if unixsocket or tcpsocket running methods are in use, 122 // if not, use default process. 123 $runningmethod = $this->get_config('runningmethod'); 124 if (in_array($runningmethod, array('unixsocket', 'tcpsocket'))) { 125 $return = $this->scan_data_execute_socket($data, $runningmethod); 126 127 if ($return === self::SCAN_RESULT_ERROR) { 128 $this->message_admins($this->get_scanning_notice()); 129 // If plugin settings require us to act like virus on any error, 130 // return SCAN_RESULT_FOUND result. 131 if ($this->get_config('clamfailureonupload') === 'actlikevirus') { 132 return self::SCAN_RESULT_FOUND; 133 } 134 } 135 return $return; 136 } else { 137 return parent::scan_data($data); 138 } 139 } 140 141 /** 142 * Returns a Unix domain socket destination url 143 * 144 * @return string The socket url, fit for stream_socket_client() 145 */ 146 private function get_unixsocket_destination() { 147 return 'unix://' . $this->get_config('pathtounixsocket'); 148 } 149 150 /** 151 * Returns a Internet domain socket destination url 152 * 153 * @return string The socket url, fit for stream_socket_client() 154 */ 155 private function get_tcpsocket_destination() { 156 return 'tcp://' . $this->get_config('tcpsockethost') . ':' . $this->get_config('tcpsocketport'); 157 } 158 159 /** 160 * Returns the string equivalent of a numeric clam error code 161 * 162 * @param int $returncode The numeric error code in question. 163 * @return string The definition of the error code 164 */ 165 private function get_clam_error_code($returncode) { 166 $returncodes = array(); 167 $returncodes[0] = 'No virus found.'; 168 $returncodes[1] = 'Virus(es) found.'; 169 $returncodes[2] = ' An error occured'; // Specific to clamdscan. 170 // All after here are specific to clamscan. 171 $returncodes[40] = 'Unknown option passed.'; 172 $returncodes[50] = 'Database initialization error.'; 173 $returncodes[52] = 'Not supported file type.'; 174 $returncodes[53] = 'Can\'t open directory.'; 175 $returncodes[54] = 'Can\'t open file. (ofm)'; 176 $returncodes[55] = 'Error reading file. (ofm)'; 177 $returncodes[56] = 'Can\'t stat input file / directory.'; 178 $returncodes[57] = 'Can\'t get absolute path name of current working directory.'; 179 $returncodes[58] = 'I/O error, please check your filesystem.'; 180 $returncodes[59] = 'Can\'t get information about current user from /etc/passwd.'; 181 $returncodes[60] = 'Can\'t get information about user \'clamav\' (default name) from /etc/passwd.'; 182 $returncodes[61] = 'Can\'t fork.'; 183 $returncodes[63] = 'Can\'t create temporary files/directories (check permissions).'; 184 $returncodes[64] = 'Can\'t write to temporary directory (please specify another one).'; 185 $returncodes[70] = 'Can\'t allocate and clear memory (calloc).'; 186 $returncodes[71] = 'Can\'t allocate memory (malloc).'; 187 if (isset($returncodes[$returncode])) { 188 return $returncodes[$returncode]; 189 } 190 return get_string('unknownerror', 'antivirus_clamav'); 191 } 192 193 /** 194 * Scan file using command line utility. 195 * 196 * @param string $file Full path to the file. 197 * @return int Scanning result constant. 198 */ 199 public function scan_file_execute_commandline($file) { 200 $pathtoclam = trim($this->get_config('pathtoclam')); 201 202 if (!file_exists($pathtoclam) or !is_executable($pathtoclam)) { 203 // Misconfigured clam, notify admins. 204 $notice = get_string('invalidpathtoclam', 'antivirus_clamav', $pathtoclam); 205 $this->set_scanning_notice($notice); 206 return self::SCAN_RESULT_ERROR; 207 } 208 209 $clamparam = ' --stdout '; 210 // If we are dealing with clamdscan, clamd is likely run as a different user 211 // that might not have permissions to access your file. 212 // To make clamdscan work, we use --fdpass parameter that passes the file 213 // descriptor permissions to clamd, which allows it to scan given file 214 // irrespective of directory and file permissions. 215 if (basename($pathtoclam) == 'clamdscan') { 216 $clamparam .= '--fdpass '; 217 } 218 // Execute scan. 219 $cmd = escapeshellcmd($pathtoclam).$clamparam.escapeshellarg($file); 220 exec($cmd, $output, $return); 221 // Return variable will contain execution return code. It will be 0 if no virus is found, 222 // 1 if virus is found, and 2 or above for the error. Return codes 0 and 1 correspond to 223 // SCAN_RESULT_OK and SCAN_RESULT_FOUND constants, so we return them as it is. 224 // If there is an error, it gets stored as scanning notice and function 225 // returns SCAN_RESULT_ERROR. 226 if ($return > self::SCAN_RESULT_FOUND) { 227 $notice = get_string('clamfailed', 'antivirus_clamav', $this->get_clam_error_code($return)); 228 $notice .= "\n\n". implode("\n", $output); 229 $this->set_scanning_notice($notice); 230 return self::SCAN_RESULT_ERROR; 231 } else { 232 $notice = "\n\n". implode("\n", $output); 233 $this->set_scanning_notice($notice); 234 } 235 236 return (int)$return; 237 } 238 239 /** 240 * Scan file using sockets. 241 * 242 * @param string $file Full path to the file. 243 * @param string $type Either 'tcpsocket' or 'unixsocket' 244 * @return int Scanning result constant. 245 */ 246 public function scan_file_execute_socket($file, $type) { 247 switch ($type) { 248 case "tcpsocket": 249 $socketurl = $this->get_tcpsocket_destination(); 250 break; 251 case "unixsocket": 252 $socketurl = $this->get_unixsocket_destination(); 253 break; 254 default; 255 // This should not happen. 256 debugging('Unknown socket type.'); 257 return self::SCAN_RESULT_ERROR; 258 } 259 260 $socket = stream_socket_client($socketurl, 261 $errno, $errstr, ANTIVIRUS_CLAMAV_SOCKET_TIMEOUT); 262 if (!$socket) { 263 // Can't open socket for some reason, notify admins. 264 $notice = get_string('errorcantopensocket', 'antivirus_clamav', "$errstr ($errno)"); 265 $this->set_scanning_notice($notice); 266 return self::SCAN_RESULT_ERROR; 267 } else { 268 if ($type == "unixsocket") { 269 // Execute scanning. We are running SCAN command and passing file as an argument, 270 // it is the fastest option, but clamav user need to be able to access it, so 271 // we give group read permissions first and assume 'clamav' user is in web server 272 // group (in Debian the default webserver group is 'www-data'). 273 // Using 'n' as command prefix is forcing clamav to only treat \n as newline delimeter, 274 // this is to avoid unexpected newline characters on different systems. 275 $perms = fileperms($file); 276 chmod($file, 0640); 277 278 // Actual scan. 279 fwrite($socket, "nSCAN ".$file."\n"); 280 // Get ClamAV answer. 281 $output = stream_get_line($socket, 4096); 282 283 // After scanning we revert permissions to initial ones. 284 chmod($file, $perms); 285 } else if ($type == "tcpsocket") { 286 // Execute scanning by passing the entire file through the TCP socket. 287 // This is not fast, but is the only possibility over a network. 288 // Using 'n' as command prefix is forcing clamav to only treat \n as newline delimeter, 289 // this is to avoid unexpected newline characters on different systems. 290 291 // Actual scan. 292 fwrite($socket, "nINSTREAM\n"); 293 294 // Open the file for reading. 295 $fhandle = fopen($file, 'rb'); 296 while (!feof($fhandle)) { 297 // Read it by chunks; write them to the TCP socket. 298 $chunk = fread($fhandle, ANTIVIRUS_CLAMAV_SOCKET_CHUNKSIZE); 299 $size = pack('N', strlen($chunk)); 300 fwrite($socket, $size); 301 fwrite($socket, $chunk); 302 } 303 // Terminate streaming. 304 fwrite($socket, pack('N', 0)); 305 // Get ClamAV answer. 306 $output = stream_get_line($socket, 4096); 307 308 fclose($fhandle); 309 } 310 // Free up the ClamAVÂ socket. 311 fclose($socket); 312 // Parse the output. 313 return $this->parse_socket_response($output); 314 } 315 } 316 317 /** 318 * Scan data socket. 319 * 320 * We are running INSTREAM command and passing data stream in chunks. 321 * The format of the chunk is: <length><data> where <length> is the size of the following 322 * data in bytes expressed as a 4 byte unsigned integer in network byte order and <data> 323 * is the actual chunk. Streaming is terminated by sending a zero-length chunk. 324 * Do not exceed StreamMaxLength as defined in clamd.conf, otherwise clamd will 325 * reply with INSTREAM size limit exceeded and close the connection. 326 * 327 * @param string $data The variable containing the data to scan. 328 * @param string $type Either 'tcpsocket' or 'unixsocket' 329 * @return int Scanning result constant. 330 */ 331 public function scan_data_execute_socket($data, $type) { 332 switch ($type) { 333 case "tcpsocket": 334 $socketurl = $this->get_tcpsocket_destination(); 335 break; 336 case "unixsocket": 337 $socketurl = $this->get_unixsocket_destination(); 338 break; 339 default; 340 // This should not happen. 341 debugging('Unknown socket type!'); 342 return self::SCAN_RESULT_ERROR; 343 } 344 345 $socket = stream_socket_client($socketurl, $errno, $errstr, ANTIVIRUS_CLAMAV_SOCKET_TIMEOUT); 346 if (!$socket) { 347 // Can't open socket for some reason, notify admins. 348 $notice = get_string('errorcantopensocket', 'antivirus_clamav', "$errstr ($errno)"); 349 $this->set_scanning_notice($notice); 350 return self::SCAN_RESULT_ERROR; 351 } else { 352 // Initiate data stream scanning. 353 // Using 'n' as command prefix is forcing clamav to only treat \n as newline delimeter, 354 // this is to avoid unexpected newline characters on different systems. 355 fwrite($socket, "nINSTREAM\n"); 356 // Send data in chunks of ANTIVIRUS_CLAMAV_SOCKET_CHUNKSIZE size. 357 while (strlen($data) > 0) { 358 $chunk = substr($data, 0, ANTIVIRUS_CLAMAV_SOCKET_CHUNKSIZE); 359 $data = substr($data, ANTIVIRUS_CLAMAV_SOCKET_CHUNKSIZE); 360 $size = pack('N', strlen($chunk)); 361 fwrite($socket, $size); 362 fwrite($socket, $chunk); 363 } 364 // Terminate streaming. 365 fwrite($socket, pack('N', 0)); 366 367 $output = stream_get_line($socket, 4096); 368 fclose($socket); 369 370 // Parse the output. 371 return $this->parse_socket_response($output); 372 } 373 } 374 375 /** 376 * Parse socket command response. 377 * 378 * @param string $output The socket response. 379 * @return int Scanning result constant. 380 */ 381 private function parse_socket_response($output) { 382 $splitoutput = explode(': ', $output); 383 $message = trim($splitoutput[1]); 384 if ($message === 'OK') { 385 return self::SCAN_RESULT_OK; 386 } else { 387 $parts = explode(' ', $message); 388 $status = array_pop($parts); 389 if ($status === 'FOUND') { 390 $notice = "\n\n" . $output; 391 $this->set_scanning_notice($notice); 392 return self::SCAN_RESULT_FOUND; 393 } else { 394 $notice = get_string('clamfailed', 'antivirus_clamav', $this->get_clam_error_code(2)); 395 $notice .= "\n\n" . $output; 396 $this->set_scanning_notice($notice); 397 return self::SCAN_RESULT_ERROR; 398 } 399 } 400 } 401 402 /** 403 * Scan data using Unix domain socket. 404 * 405 * @deprecated since Moodle 3.9 MDL-64075 - please do not use this function any more. 406 * @see antivirus_clamav\scanner::scan_data_execute_socket() 407 * 408 * @param string $data The variable containing the data to scan. 409 * @return int Scanning result constant. 410 */ 411 public function scan_data_execute_unixsocket($data) { 412 debugging('antivirus_clamav\scanner::scan_data_execute_unixsocket() is deprecated. ' . 413 'Use antivirus_clamav\scanner::scan_data_execute_socket() instead.', DEBUG_DEVELOPER); 414 return $this->scan_data_execute_socket($data, "unixsocket"); 415 } 416 417 /** 418 * Scan file using Unix domain socket. 419 * 420 * @deprecated since Moodle 3.9 MDL-64075 - please do not use this function any more. 421 * @see antivirus_clamav\scanner::scan_file_execute_socket() 422 * 423 * @param string $file Full path to the file. 424 * @return int Scanning result constant. 425 */ 426 public function scan_file_execute_unixsocket($file) { 427 debugging('antivirus_clamav\scanner::scan_file_execute_unixsocket() is deprecated. ' . 428 'Use antivirus_clamav\scanner::scan_file_execute_socket() instead.', DEBUG_DEVELOPER); 429 return $this->scan_file_execute_socket($file, "unixsocket"); 430 } 431 432 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body