MDL-50888 antivirus_clamav: Implement scan using Unix domain sockets.

This is a faster way of scanning files than using command line exec call,
but only available on unix-like systems.
This commit is contained in:
Ruslan Kabalin 2015-07-09 12:03:55 +01:00
parent 919b9dfabd
commit 7be0d4292a
6 changed files with 208 additions and 6 deletions

View File

@ -0,0 +1,105 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* 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;
}
}

View File

@ -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);
}
}
}
}
}

View File

@ -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;
}

View File

@ -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.';

View File

@ -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'),

View File

@ -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).