See Release Notes
Long Term Support Release
Differences Between: [Versions 39 and 310] [Versions 39 and 311] [Versions 39 and 400] [Versions 39 and 401] [Versions 39 and 402] [Versions 39 and 403]
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 } 232 233 return (int)$return; 234 } 235 236 /** 237 * Scan file using sockets. 238 * 239 * @param string $file Full path to the file. 240 * @param string $type Either 'tcpsocket' or 'unixsocket' 241 * @return int Scanning result constant. 242 */ 243 public function scan_file_execute_socket($file, $type) { 244 switch ($type) { 245 case "tcpsocket": 246 $socketurl = $this->get_tcpsocket_destination(); 247 break; 248 case "unixsocket": 249 $socketurl = $this->get_unixsocket_destination(); 250 break; 251 default; 252 // This should not happen. 253 debugging('Unknown socket type.'); 254 return self::SCAN_RESULT_ERROR; 255 } 256 257 $socket = stream_socket_client($socketurl, 258 $errno, $errstr, ANTIVIRUS_CLAMAV_SOCKET_TIMEOUT); 259 if (!$socket) { 260 // Can't open socket for some reason, notify admins. 261 $notice = get_string('errorcantopensocket', 'antivirus_clamav', "$errstr ($errno)"); 262 $this->set_scanning_notice($notice); 263 return self::SCAN_RESULT_ERROR; 264 } else { 265 if ($type == "unixsocket") { 266 // Execute scanning. We are running SCAN command and passing file as an argument, 267 // it is the fastest option, but clamav user need to be able to access it, so 268 // we give group read permissions first and assume 'clamav' user is in web server 269 // group (in Debian the default webserver group is 'www-data'). 270 // Using 'n' as command prefix is forcing clamav to only treat \n as newline delimeter, 271 // this is to avoid unexpected newline characters on different systems. 272 $perms = fileperms($file); 273 chmod($file, 0640); 274 275 // Actual scan. 276 fwrite($socket, "nSCAN ".$file."\n"); 277 // Get ClamAV answer. 278 $output = stream_get_line($socket, 4096); 279 280 // After scanning we revert permissions to initial ones. 281 chmod($file, $perms); 282 } else if ($type == "tcpsocket") { 283 // Execute scanning by passing the entire file through the TCP socket. 284 // This is not fast, but is the only possibility over a network. 285 // Using 'n' as command prefix is forcing clamav to only treat \n as newline delimeter, 286 // this is to avoid unexpected newline characters on different systems. 287 288 // Actual scan. 289 fwrite($socket, "nINSTREAM\n"); 290 291 // Open the file for reading. 292 $fhandle = fopen($file, 'rb'); 293 while (!feof($fhandle)) { 294 // Read it by chunks; write them to the TCP socket. 295 $chunk = fread($fhandle, ANTIVIRUS_CLAMAV_SOCKET_CHUNKSIZE); 296 $size = pack('N', strlen($chunk)); 297 fwrite($socket, $size); 298 fwrite($socket, $chunk); 299 } 300 // Terminate streaming. 301 fwrite($socket, pack('N', 0)); 302 // Get ClamAV answer. 303 $output = stream_get_line($socket, 4096); 304 305 fclose($fhandle); 306 } 307 // Free up the ClamAVÂ socket. 308 fclose($socket); 309 // Parse the output. 310 return $this->parse_socket_response($output); 311 } 312 } 313 314 /** 315 * Scan data socket. 316 * 317 * We are running INSTREAM command and passing data stream in chunks. 318 * The format of the chunk is: <length><data> where <length> is the size of the following 319 * data in bytes expressed as a 4 byte unsigned integer in network byte order and <data> 320 * is the actual chunk. Streaming is terminated by sending a zero-length chunk. 321 * Do not exceed StreamMaxLength as defined in clamd.conf, otherwise clamd will 322 * reply with INSTREAM size limit exceeded and close the connection. 323 * 324 * @param string $data The variable containing the data to scan. 325 * @param string $type Either 'tcpsocket' or 'unixsocket' 326 * @return int Scanning result constant. 327 */ 328 public function scan_data_execute_socket($data, $type) { 329 switch ($type) { 330 case "tcpsocket": 331 $socketurl = $this->get_tcpsocket_destination(); 332 break; 333 case "unixsocket": 334 $socketurl = $this->get_unixsocket_destination(); 335 break; 336 default; 337 // This should not happen. 338 debugging('Unknown socket type!'); 339 return self::SCAN_RESULT_ERROR; 340 } 341 342 $socket = stream_socket_client($socketurl, $errno, $errstr, ANTIVIRUS_CLAMAV_SOCKET_TIMEOUT); 343 if (!$socket) { 344 // Can't open socket for some reason, notify admins. 345 $notice = get_string('errorcantopensocket', 'antivirus_clamav', "$errstr ($errno)"); 346 $this->set_scanning_notice($notice); 347 return self::SCAN_RESULT_ERROR; 348 } else { 349 // Initiate data stream scanning. 350 // Using 'n' as command prefix is forcing clamav to only treat \n as newline delimeter, 351 // this is to avoid unexpected newline characters on different systems. 352 fwrite($socket, "nINSTREAM\n"); 353 // Send data in chunks of ANTIVIRUS_CLAMAV_SOCKET_CHUNKSIZE size. 354 while (strlen($data) > 0) { 355 $chunk = substr($data, 0, ANTIVIRUS_CLAMAV_SOCKET_CHUNKSIZE); 356 $data = substr($data, ANTIVIRUS_CLAMAV_SOCKET_CHUNKSIZE); 357 $size = pack('N', strlen($chunk)); 358 fwrite($socket, $size); 359 fwrite($socket, $chunk); 360 } 361 // Terminate streaming. 362 fwrite($socket, pack('N', 0)); 363 364 $output = stream_get_line($socket, 4096); 365 fclose($socket); 366 367 // Parse the output. 368 return $this->parse_socket_response($output); 369 } 370 } 371 372 /** 373 * Parse socket command response. 374 * 375 * @param string $output The socket response. 376 * @return int Scanning result constant. 377 */ 378 private function parse_socket_response($output) { 379 $splitoutput = explode(': ', $output); 380 $message = trim($splitoutput[1]); 381 if ($message === 'OK') { 382 return self::SCAN_RESULT_OK; 383 } else { 384 $parts = explode(' ', $message); 385 $status = array_pop($parts); 386 if ($status === 'FOUND') { 387 return self::SCAN_RESULT_FOUND; 388 } else { 389 $notice = get_string('clamfailed', 'antivirus_clamav', $this->get_clam_error_code(2)); 390 $notice .= "\n\n" . $output; 391 $this->set_scanning_notice($notice); 392 return self::SCAN_RESULT_ERROR; 393 } 394 } 395 } 396 397 /** 398 * Scan data using Unix domain socket. 399 * 400 * @deprecated since Moodle 3.9 MDL-64075 - please do not use this function any more. 401 * @see antivirus_clamav\scanner::scan_data_execute_socket() 402 * 403 * @param string $data The variable containing the data to scan. 404 * @return int Scanning result constant. 405 */ 406 public function scan_data_execute_unixsocket($data) { 407 debugging('antivirus_clamav\scanner::scan_data_execute_unixsocket() is deprecated. ' . 408 'Use antivirus_clamav\scanner::scan_data_execute_socket() instead.', DEBUG_DEVELOPER); 409 return $this->scan_data_execute_socket($data, "unixsocket"); 410 } 411 412 /** 413 * Scan file using Unix domain socket. 414 * 415 * @deprecated since Moodle 3.9 MDL-64075 - please do not use this function any more. 416 * @see antivirus_clamav\scanner::scan_file_execute_socket() 417 * 418 * @param string $file Full path to the file. 419 * @return int Scanning result constant. 420 */ 421 public function scan_file_execute_unixsocket($file) { 422 debugging('antivirus_clamav\scanner::scan_file_execute_unixsocket() is deprecated. ' . 423 'Use antivirus_clamav\scanner::scan_file_execute_socket() instead.', DEBUG_DEVELOPER); 424 return $this->scan_file_execute_socket($file, "unixsocket"); 425 } 426 427 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body