diff --git a/lib/antivirus/clamav/adminlib.php b/lib/antivirus/clamav/adminlib.php new file mode 100644 index 00000000000..f473cea3198 --- /dev/null +++ b/lib/antivirus/clamav/adminlib.php @@ -0,0 +1,105 @@ +. + +/** + * ClamAV antivirus adminlib. + * + * @package antivirus_clamav + * @copyright 2015 Ruslan Kabalin, Lancaster University. + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Admin setting for running, adds verification. + * + * @package antivirus_clamav + * @copyright 2015 Ruslan Kabalin, Lancaster University. + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class antivirus_clamav_runningmethod_setting extends admin_setting_configselect { + /** + * Save a setting + * + * @param string $data + * @return string empty or error string + */ + public function write_setting($data) { + $validated = $this->validate($data); + if ($validated !== true) { + return $validated; + } + return parent::write_setting($data); + } + + /** + * Validate data. + * + * This ensures that unix socket transport is supported by this system. + * + * @param string $data + * @return mixed True on success, else error message. + */ + public function validate($data) { + if ($data === 'unixsocket') { + $supportedtransports = stream_get_transports(); + if (!array_search('unix', $supportedtransports)) { + return get_string('errornounixsocketssupported', 'antivirus_clamav'); + } + } + return true; + } +} +/** + * Admin setting for unix socket path, adds verification. + * + * @package antivirus_clamav + * @copyright 2015 Ruslan Kabalin, Lancaster University. + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class antivirus_clamav_pathtounixsocket_setting extends admin_setting_configtext { + /** + * Validate data. + * + * This ensures that unix socket setting is correct and ClamAV is running. + * + * @param string $data + * @return mixed True on success, else error message. + */ + public function validate($data) { + $result = parent::validate($data); + if ($result !== true) { + return $result; + } + $runningmethod = get_config('antivirus_clamav', 'runningmethod'); + if ($runningmethod === 'unixsocket') { + $socket = stream_socket_client('unix://' . $data, $errno, $errstr, ANTIVIRUS_CLAMAV_SOCKET_TIMEOUT); + if (!$socket) { + return get_string('errorcantopensocket', 'antivirus_clamav', "$errstr ($errno)"); + } else { + // Send PING query to ClamAV socket to check its running state. + fwrite($socket, "nPING\n"); + $response = stream_get_line($socket, 4); + fclose($socket); + if ($response !== 'PONG') { + return get_string('errorclamavnoresponse', 'antivirus_clamav'); + } + } + } + return true; + } +} diff --git a/lib/antivirus/clamav/classes/scanner.php b/lib/antivirus/clamav/classes/scanner.php index e8e58e36dab..d5112d1307d 100644 --- a/lib/antivirus/clamav/classes/scanner.php +++ b/lib/antivirus/clamav/classes/scanner.php @@ -26,6 +26,9 @@ namespace antivirus_clamav; defined('MOODLE_INTERNAL') || die(); +/** Default socket timeout */ +define('ANTIVIRUS_CLAMAV_SOCKET_TIMEOUT', 10); + /** * Class implemeting ClamAV antivirus. * @copyright 2015 Ruslan Kabalin, Lancaster University. @@ -38,8 +41,14 @@ class scanner extends \core\antivirus\scanner { * @return bool True if all necessary config settings been entered */ public function is_configured() { - return (bool)$this->get_config('pathtoclam'); + if ($this->get_config('runningmethod') === 'commandline') { + return (bool)$this->get_config('pathtoclam'); + } else if ($this->get_config('runningmethod') === 'unixsocket') { + return (bool)$this->get_config('pathtounixsocket'); + } + return false; } + /** * Scan file, throws exception in case of infected file. * @@ -59,7 +68,8 @@ class scanner extends \core\antivirus\scanner { } // Execute the scan using preferable method. - list($return, $notice) = $this->scan_file_execute_commandline($file); + $method = 'scan_file_execute_' . $this->get_config('runningmethod'); + list($return, $notice) = $this->$method($file); if ($return == 0) { // Perfect, no problem found, file is clean. @@ -153,4 +163,49 @@ class scanner extends \core\antivirus\scanner { return array($return, $notice); } + + /** + * Scan file using unix socket. + * + * @param string $file Full path to the file. + * @return array ($return, $notice) Execution return code and notification text. + */ + public function scan_file_execute_unixsocket($file) { + $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)"); + return array(-1, $notice); + } else { + // Execute scanning. We are running SCAN command and passing file as an argument, + // it is the fastest option, but clamav user need to be able to access it, so + // we give group read permissions first and assume 'clamav' user is in web server + // group (in Debian the default webserver group is 'www-data'). + // 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. + $perms = fileperms($file); + chmod($file, 0640); + fwrite($socket, "nSCAN ".$file."\n"); + $output = stream_get_line($socket, 4096); + fclose($socket); + // 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 array(0, ''); + } else { + $parts = explode(' ', $message); + $status = array_pop($parts); + if ($status === 'FOUND') { + return array(1, ''); + } else { + $notice = get_string('clamfailed', 'antivirus_clamav', $this->get_clam_error_code(2)); + $notice .= "\n\n" . $output; + return array(2, $notice); + } + } + } + } } diff --git a/lib/antivirus/clamav/db/upgrade.php b/lib/antivirus/clamav/db/upgrade.php index 45eb5c104ed..7b870956e05 100644 --- a/lib/antivirus/clamav/db/upgrade.php +++ b/lib/antivirus/clamav/db/upgrade.php @@ -34,5 +34,15 @@ function xmldb_antivirus_clamav_upgrade($oldversion) { // Moodle v3.1.0 release upgrade line. // Put any upgrade step following this. + if ($oldversion < 2016072100) { + // Make command line a default running method for now. We depend on this + // config variable in antivirus scan running, it should be defined. + if (!get_config('antivirus_clamav', 'runningmethod')) { + set_config('runningmethod', 'commandline', 'antivirus_clamav'); + } + + upgrade_plugin_savepoint(true, 2016072100, 'antivirus', 'clamav'); + } + return true; } diff --git a/lib/antivirus/clamav/lang/en/antivirus_clamav.php b/lib/antivirus/clamav/lang/en/antivirus_clamav.php index d83a608caa1..543dd026be5 100644 --- a/lib/antivirus/clamav/lang/en/antivirus_clamav.php +++ b/lib/antivirus/clamav/lang/en/antivirus_clamav.php @@ -25,12 +25,21 @@ $string['configclamactlikevirus'] = 'Treat files like viruses'; $string['configclamdonothing'] = 'Treat files as OK'; $string['configclamfailureonupload'] = 'If you have configured clam to scan uploaded files, but it is configured incorrectly or fails to run for some unknown reason, how should it behave? If you choose \'Treat files like viruses\', they\'ll be moved into the quarantine area, or deleted. If you choose \'Treat files as OK\', the files will be moved to the destination directory like normal. Either way, admins will be alerted that clam has failed. If you choose \'Treat files like viruses\' and for some reason clam fails to run (usually because you have entered an invalid pathtoclam), ALL files that are uploaded will be moved to the given quarantine area, or deleted. Be careful with this setting.'; -$string['configpathtoclam'] = 'Path to ClamAV. Probably something like /usr/bin/clamscan or /usr/bin/clamdscan. You need this in order for ClamAV to run.'; $string['configquarantinedir'] = 'If you want ClamAV to move infected files to a quarantine directory, enter it here. It must be writable by the webserver. If you leave this blank, or if you enter a directory that doesn\'t exist or isn\'t writable, infected files will be deleted. Do not include a trailing slash.'; $string['clamfailed'] = 'ClamAV has failed to run. The return error message was "{$a}". Here is the output from ClamAV:'; $string['clamfailureonupload'] = 'On ClamAV failure'; +$string['errorcantopensocket'] = 'Connecting to Unix domain socket resulted in error {$a}'; +$string['errorclamavnoresponse'] = 'ClamAV does not respond; check daemon running state.'; +$string['errornounixsocketssupported'] = 'Unix domain socket transport is not supported on this system. Please use the command line option instead.'; $string['invalidpathtoclam'] = 'Path to ClamAV, {$a}, is invalid.'; -$string['pathtoclam'] = 'ClamAV path'; +$string['pathtoclam'] = 'Command line'; +$string['pathtoclamdesc'] = 'If the running method is set to "command line", enter the path to ClamAV here. On Linux this will be /usr/bin/clamscan or /usr/bin/clamdscan.'; +$string['pathtounixsocket'] = 'Unix domain socket'; +$string['pathtounixsocketdesc'] = 'If the running method is set to "Unix domain socket", enter the path to ClamAV Unix socket here. On Debian Linux this will be /var/run/clamav/clamd.ctl. Please make sure that clamav daemon has read access to uploaded files, the easiest way to ensure that is to add \'clamav\' user to your webserver group (\'www-data\' on Debian Linux).'; $string['pluginname'] = 'ClamAV antivirus'; $string['quarantinedir'] = 'Quarantine directory'; +$string['runningmethod'] = 'Running method'; +$string['runningmethoddesc'] = 'Method of running ClamAV. Command line is used by default, however on Unix systems better performance can be obtained by using system sockets.'; +$string['runningmethodcommandline'] = 'Command line'; +$string['runningmethodunixsocket'] = 'Unix domain socket'; $string['unknownerror'] = 'There was an unknown error with ClamAV.'; diff --git a/lib/antivirus/clamav/settings.php b/lib/antivirus/clamav/settings.php index 6e41ec761f1..cdba4445516 100644 --- a/lib/antivirus/clamav/settings.php +++ b/lib/antivirus/clamav/settings.php @@ -25,10 +25,33 @@ defined('MOODLE_INTERNAL') || die(); if ($ADMIN->fulltree) { + require_once(__DIR__ . '/adminlib.php'); + require_once(__DIR__ . '/classes/scanner.php'); + + // Running method. + $runningmethodchoice = array( + 'commandline' => get_string('runningmethodcommandline', 'antivirus_clamav'), + 'unixsocket' => get_string('runningmethodunixsocket', 'antivirus_clamav'), + ); + $settings->add(new antivirus_clamav_runningmethod_setting('antivirus_clamav/runningmethod', + get_string('runningmethod', 'antivirus_clamav'), + get_string('runningmethoddesc', 'antivirus_clamav'), + 'commandline', $runningmethodchoice)); + + // Path to ClamAV scanning utility (used in command line running method). $settings->add(new admin_setting_configexecutable('antivirus_clamav/pathtoclam', - new lang_string('pathtoclam', 'antivirus_clamav'), new lang_string('configpathtoclam', 'antivirus_clamav'), '')); + new lang_string('pathtoclam', 'antivirus_clamav'), new lang_string('pathtoclamdesc', 'antivirus_clamav'), '')); + + // Path to ClamAV unix socket (used in unix socket running method). + $settings->add(new antivirus_clamav_pathtounixsocket_setting('antivirus_clamav/pathtounixsocket', + new lang_string('pathtounixsocket', 'antivirus_clamav'), + new lang_string('pathtounixsocketdesc', 'antivirus_clamav'), '', PARAM_PATH)); + + // Quarantine directory path. $settings->add(new admin_setting_configdirectory('antivirus_clamav/quarantinedir', new lang_string('quarantinedir', 'antivirus_clamav'), new lang_string('configquarantinedir', 'antivirus_clamav'), '')); + + // How to act on ClamAV failure. $options = array( 'donothing' => new lang_string('configclamdonothing', 'antivirus_clamav'), 'actlikevirus' => new lang_string('configclamactlikevirus', 'antivirus_clamav'), diff --git a/lib/antivirus/clamav/version.php b/lib/antivirus/clamav/version.php index 80dd77710fc..05b3d6823eb 100644 --- a/lib/antivirus/clamav/version.php +++ b/lib/antivirus/clamav/version.php @@ -24,6 +24,6 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2016052300; // The current plugin version (Date: YYYYMMDDXX). +$plugin->version = 2016072100; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2016051900; // Requires this Moodle version. $plugin->component = 'antivirus_clamav'; // Full name of the plugin (used for diagnostics).