diff --git a/lib/antivirus/clamav/classes/scanner.php b/lib/antivirus/clamav/classes/scanner.php index c4c10704b87..23a35f82bf3 100644 --- a/lib/antivirus/clamav/classes/scanner.php +++ b/lib/antivirus/clamav/classes/scanner.php @@ -28,6 +28,8 @@ defined('MOODLE_INTERNAL') || die(); /** Default socket timeout */ define('ANTIVIRUS_CLAMAV_SOCKET_TIMEOUT', 10); +/** Default socket data stream chunk size */ +define('ANTIVIRUS_CLAMAV_SOCKET_CHUNKSIZE', 1024); /** * Class implementing ClamAV antivirus. @@ -67,6 +69,9 @@ class scanner extends \core\antivirus\scanner { // Execute the scan using preferable method. $method = 'scan_file_execute_' . $this->get_config('runningmethod'); + if (!method_exists($this, $method)) { + throw new \coding_exception('Attempting to call non-existing method ' . $method); + } $return = $this->$method($file); if ($return === self::SCAN_RESULT_ERROR) { @@ -80,6 +85,32 @@ class scanner extends \core\antivirus\scanner { return $return; } + /** + * Scan data. + * + * @param string $data The varaible containing the data to scan. + * @return int Scanning result constant. + */ + public function scan_data($data) { + // We can do direct stream scanning if unixsocket running method is in use, + // if not, use default process. + if ($this->get_config('runningmethod') === 'unixsocket') { + $return = $this->scan_data_execute_unixsocket($data); + + if ($return === self::SCAN_RESULT_ERROR) { + $this->message_admins($this->get_scanning_notice()); + // If plugin settings require us to act like virus on any error, + // return SCAN_RESULT_FOUND result. + if ($this->get_config('clamfailureonupload') === 'actlikevirus') { + return self::SCAN_RESULT_FOUND; + } + } + return $return; + } else { + return parent::scan_data($data); + } + } + /** * Returns the string equivalent of a numeric clam error code * @@ -186,21 +217,75 @@ class scanner extends \core\antivirus\scanner { // After scanning we revert permissions to initial ones. chmod($file, $perms); // Parse the output. - $splitoutput = explode(': ', $output); - $message = trim($splitoutput[1]); - if ($message === 'OK') { - return self::SCAN_RESULT_OK; + return $this->parse_unixsocket_response($output); + } + } + + /** + * Scan data using unix socket. + * + * We are running INSTREAM command and passing data stream in chunks. + * The format of the chunk is: <length><data> where <length> is the size of the following + * data in bytes expressed as a 4 byte unsigned integer in network byte order and <data> + * is the actual chunk. Streaming is terminated by sending a zero-length chunk. + * Do not exceed StreamMaxLength as defined in clamd.conf, otherwise clamd will + * reply with INSTREAM size limit exceeded and close the connection. + * + * @param string $data The varaible containing the data to scan. + * @return int Scanning result constant. + */ + private function scan_data_execute_unixsocket($data) { + $socket = stream_socket_client('unix://' . $this->get_config('pathtounixsocket'), $errno, $errstr, ANTIVIRUS_CLAMAV_SOCKET_TIMEOUT); + if (!$socket) { + // Can't open socket for some reason, notify admins. + $notice = get_string('errorcantopensocket', 'antivirus_clamav', "$errstr ($errno)"); + $this->set_scanning_notice($notice); + return self::SCAN_RESULT_ERROR; + } else { + // Initiate data stream scanning. + // Using 'n' as command prefix is forcing clamav to only treat \n as newline delimeter, + // this is to avoid unexpected newline characters on different systems. + fwrite($socket, "nINSTREAM\n"); + // Send data in chunks of ANTIVIRUS_CLAMAV_SOCKET_CHUNKSIZE size. + while (strlen($data) > 0) { + $chunk = substr($data, 0, ANTIVIRUS_CLAMAV_SOCKET_CHUNKSIZE); + $data = substr($data, ANTIVIRUS_CLAMAV_SOCKET_CHUNKSIZE); + $size = pack('N', strlen($chunk)); + fwrite($socket, $size); + fwrite($socket, $chunk); + } + // Terminate streaming. + fwrite($socket, pack('N', 0)); + + $output = stream_get_line($socket, 4096); + fclose($socket); + + // Parse the output. + return $this->parse_unixsocket_response($output); + } + } + + /** + * Parse unix socket command response. + * + * @param string $output The unix socket command response. + * @return int Scanning result constant. + */ + private function parse_unixsocket_response($output) { + $splitoutput = explode(': ', $output); + $message = trim($splitoutput[1]); + if ($message === 'OK') { + return self::SCAN_RESULT_OK; + } else { + $parts = explode(' ', $message); + $status = array_pop($parts); + if ($status === 'FOUND') { + return self::SCAN_RESULT_FOUND; } else { - $parts = explode(' ', $message); - $status = array_pop($parts); - if ($status === 'FOUND') { - return self::SCAN_RESULT_FOUND; - } else { - $notice = get_string('clamfailed', 'antivirus_clamav', $this->get_clam_error_code(2)); - $notice .= "\n\n" . $output; - $this->set_scanning_notice($notice); - return self::SCAN_RESULT_ERROR; - } + $notice = get_string('clamfailed', 'antivirus_clamav', $this->get_clam_error_code(2)); + $notice .= "\n\n" . $output; + $this->set_scanning_notice($notice); + return self::SCAN_RESULT_ERROR; } } }