diff --git a/README.md b/README.md index 2681cf6..0a090bb 100755 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ remove the file if it's infected. The App is not complete yet, the following works/is done: * It can be configured to work with the executable or the daemon mode of ClamAV +* If used in daemon mode it can connect through network- or local file-socket * In daemon mode, it sends files to a remote/local server using INSTREAM command * When the user uploads a file, it's checked * If an uploaded file is infected, it's deleted and a notification is shown to the user on screen and an email is sent with details. @@ -35,6 +36,10 @@ The App is not complete yet, the following works/is done: * Owncloud 4 * ClamAV (Binaries or a server running ClamAV in daemon mode) +## 3rd party software used +* Simplesocketclient for connecting to ClamAV file-socket: http://github.com/kijin/simplesocket + + ## Install * Install and enable the App diff --git a/appinfo/info.xml b/appinfo/info.xml index 6c7a42b..3577a8a 100755 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -2,10 +2,10 @@ files_antivirus Antivirus App for files - 0.4 + 0.4-1 Verify files for virus using ClamAV AGPL - Manuel Delgado, Bart Visscher + Manuel Delgado, Bart Visscher, thinksilicon.de 5 diff --git a/js/settings.js b/js/settings.js index 94db0ff..1923736 100755 --- a/js/settings.js +++ b/js/settings.js @@ -1,10 +1,18 @@ function av_mode_show_options(str){ if ( str == 'daemon'){ + $('p.av_socket').hide('slow'); $('p.av_host').show('slow'); $('p.av_port').show('slow'); $('p.av_chunk_size').show('slow'); $('p.av_path').hide('slow'); - } else if (str == 'executable'){ + } else if ( str == 'socket' ) { + $('p.av_socket').show('slow'); + $('p.av_path').hide('slow'); + $('p.av_host').hide('slow'); + $('p.av_port').hide('slow'); + $('p.av_chunk_size').hide('slow'); + } else if (str == 'executable'){ + $('p.av_socket').hide('slow'); $('p.av_host').hide('slow'); $('p.av_port').hide('slow'); $('p.av_chunk_size').hide('slow'); diff --git a/l10n/de.php b/l10n/de.php index d595392..ea87899 100644 --- a/l10n/de.php +++ b/l10n/de.php @@ -1,24 +1,26 @@ "Herzlich Willkommen {user},", -"Sorry, but a malware was detected in a file you tried to upload and it had to be deleted." => "Entschuldige, es wurde Malware in einer Datei gefunden die Du hochladen wolltest und sie musste gelöscht werden.", -"This email is a notification from {host}. Please, do not reply." => "Diese E-Mail ist eine Benachrichtigung von {host}. Bitte nicht antworten.", -"File uploaded: {file}" => "Datei {file} hochgeladen.", +"Sorry, but a malware was detected in a file you tried to upload and it had to be deleted." => "Entschuldigung, in einer Datei, die Sie hochladen wollten, wurde Malware gefunden und sie musste daher gelöscht werden.", +"This email is a notification from {host}. Please, do not reply." => "Diese E-Mail ist eine Benachrichtigung von {host}. Bitte antworten Sie nicht.", +"File uploaded: {file}" => "Datei wurde hochgeladen: {file}", "Antivirus Configuration" => "Antivirus-Konfiguration", "Mode" => "Modus", -"Executable" => "ausführbar", -"Daemon" => "Dienst", +"Executable" => "Ausführbar", +"Daemon" => "Dienst (Daemon)", +"Daemon (Socket)" => "Dienst (Socket)", "Host" => "Host", -"Address of Antivirus Host." => "Adresse des Antivirus-Servers", -"Not required in Executable Mode." => "Nicht benötigt im ausführbaren Modus", +"Address of Antivirus Host." => "Die Adresse des Antivirus-Hosts.", +"Not required in Executable Mode." => "Nicht erforderlich im ausführbaren Modus.", "Port" => "Port", -"Port number of Antivirus Host." => "Port des Antivirus-Servers", -"Stream Length" => "Stream-Länge", -"ClamAV StreamMaxLength value in bytes." => "ClamAV-Wert 'StreamMaxLength' in Bytes.", +"Port number of Antivirus Host." => "Die Portnummer des Antivirus-Hosts.", +"Stream Length" => "Übertragungslänge", +"ClamAV StreamMaxLength value in bytes." => "ClamAV StreamMaxLength-Wert in Bytes.", "Path to clamscan" => "Pfad zu clamscan", -"Path to clamscan executable." => "Pfad zur clamscan Datei", -"Not required in Daemon Mode." => "Nicht erforderlich im Dienst Modus", -"Action for infected files found while scanning" => "Aktion für infizierte Dateien die beim Scannen gefunden werden", -"Only log" => "Nur protokollieren", +"Path to clamscan executable." => "Pfad zum clamscan-Programm", +"ClamAV Socket" => "Pfad zum ClamAV-Socket", +"Not required in Daemon Mode." => "Nicht erforderlich im Dienstmodus (Daemon)", +"Action for infected files found while scanning" => "Aktion für infizierte Dateien, welche beim Scannen gefunden wurden", +"Only log" => "Nur loggen", "Delete file" => "Datei löschen", "Save" => "Speichern" ); diff --git a/l10n/de_DE.php b/l10n/de_DE.php index a42d64e..ea87899 100644 --- a/l10n/de_DE.php +++ b/l10n/de_DE.php @@ -7,6 +7,7 @@ "Mode" => "Modus", "Executable" => "Ausführbar", "Daemon" => "Dienst (Daemon)", +"Daemon (Socket)" => "Dienst (Socket)", "Host" => "Host", "Address of Antivirus Host." => "Die Adresse des Antivirus-Hosts.", "Not required in Executable Mode." => "Nicht erforderlich im ausführbaren Modus.", @@ -16,6 +17,7 @@ "ClamAV StreamMaxLength value in bytes." => "ClamAV StreamMaxLength-Wert in Bytes.", "Path to clamscan" => "Pfad zu clamscan", "Path to clamscan executable." => "Pfad zum clamscan-Programm", +"ClamAV Socket" => "Pfad zum ClamAV-Socket", "Not required in Daemon Mode." => "Nicht erforderlich im Dienstmodus (Daemon)", "Action for infected files found while scanning" => "Aktion für infizierte Dateien, welche beim Scannen gefunden wurden", "Only log" => "Nur loggen", diff --git a/lib/clamav.php b/lib/clamav.php index 9b52b67..1a3861c 100755 --- a/lib/clamav.php +++ b/lib/clamav.php @@ -75,6 +75,8 @@ class OC_Files_Antivirus { return self::_clamav_scan_via_daemon($filepath); case 'executable': return self::_clamav_scan_via_exec($filepath); + case 'socket': + return self::_clamav_scan_via_socket($filepath); } } @@ -194,4 +196,45 @@ class OC_Files_Antivirus { return CLAMAV_SCANRESULT_UNCHECKED; } } + private static function _clamav_scan_via_socket( $filepath ) { + $av_socket = \OCP\Config::getAppValue( 'files_antivirus', 'av_socket', '' ); + require_once( 'simplesocketclient.php' ); + $socket = new SimpleSocketClient( $av_socket, false, 5 ); + if( $socket->connect() === false ) { + \OCP\Util::writeLog( 'files_antivirus', 'Could not connect to Clamd via socket '.$av_socket.'!', \OCP\Util::ERROR ); + return CLAMAV_SCANRESULT_UNCHECKED; + } + $socket->write('SCAN ' . $filepath ); + $response = $socket->readline(); + $response = trim($response); + $socket->disconnect(); + + if (!strncmp($response, $filepath . ':', strlen($filepath) + 1)) { + + // Cut the filename from the response. + $response = substr($response, strlen($filepath) + 2); + + // OK + if ($response === 'OK') { + \OCP\Util::writeLog( 'files_antivirus', 'Result CLEAN!', \OCP\Util::DEBUG ); + return CLAMAV_SCANRESULT_CLEAN; + } + + // FOUND + if (substr($response, strlen($response) - 5) === 'FOUND') { + $virus = substr($response, 0, strlen($response) - 6); + \OCP\Util::writeLog( 'files_antivirus', 'Virus detected in file. Clamd reported '.$virus , \OCP\Util::WARN ); + return CLAMAV_SCANRESULT_INFECTED; + } + + // ERROR + if (substr($response, strlen($response) - 5) === 'ERROR') { + substr( $response, 0, strlen( $response ) - 6 ); + \OCP\Util::writeLog( 'files_antivirus', 'File could not be scanned. Clamd reported '.$response, \OCP\Util::ERROR ); + return CLAMAV_SCANRESULT_UNCHECKED; + } + } + \OCP\Util::writeLog( 'files_antivirus', 'No response from Clamd!', \OCP\Util::ERROR ); + return CLAMAV_SCANRESULT_UNCHECKED; + } } diff --git a/lib/simplesocketclient.php b/lib/simplesocketclient.php new file mode 100644 index 0000000..8c8621f --- /dev/null +++ b/lib/simplesocketclient.php @@ -0,0 +1,351 @@ + + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +class SimpleSocketClient +{ + /** + * Connection settings. + * + * Although these properties can be manipulated directly by children, + * it is best to keep them in the hands of the parent class. + */ + + protected $_con = null; + protected $_host = ''; + protected $_port = 0; + protected $_timeout = 0; + + + /** + * Default host and port. + * + * These properties can be overridden by children to provide default values + * for host and port if they're not passed to the constructor. + */ + + protected $_default_host = null; + protected $_default_port = null; + + + /** + * Constructor. + * + * Host and port must be supplied at the time of instantiation. + * + * @param string The hostname, IP address, or UNIX socket for the server. + * @param int The port of the server, or false for UNIX sockets. + * @param int Connection timeout in seconds. [optional: default is 5] + */ + + public function __construct($host = null, $port = null, $timeout = 5) + { + // A quick check for IPv6 addresses. (They contain colons.) + + if (strpos($host, ':') !== false && strpos($host, '[') === false) + { + $host = '[' . $host . ']'; + } + + // Use default values? + + if (is_null($host) && is_null($port)) + { + $host = $this->_default_host; + $port = $this->_default_port; + } + + // Keep the connection info, but don't connect now. + + $this->_host = $host; + $this->_port = $port; + $this->_timeout = $timeout; + } + + + /** + * Connect to the server. + * + * Normally, this method is useful only for debugging purposes, because + * it will be called automatically the first time a read/write operation + * is attempted. + */ + + public function connect() + { + // If already connected, don't do anything. + + if ($this->_con !== null && $this->_con !== false) return true; + + // If a previous connection attempt had failed, do not retry. + + if ($this->_con === false) throw new SimpleSocketException('Cannot connect to ' . $this->_host . ' port ' . $this->_port); + + // Attempt to connect. + + $socket = $this->_port ? ($this->_host . ':' . $this->_port) : ('unix://' . $this->_host); + $this->_con = stream_socket_client($socket, $errno, $errstr, $this->_timeout); + + // If there's an error, set $_con to false, and throw an exception. + + if (!$this->_con) + { + $this->_con = false; + throw new SimpleSocketException('Cannot connect to ' . $this->_host . ' port ' . $this->_port . ': ' . $errstr . ' (code ' . $errno . ')'); + } + + // Return true to indicate success. + + return true; + } + + + /** + * Disconnect from the server. + * + * Normally, this method is useful only for debugging purposes, because + * it will be automatically called in the event of an error (resulting in + * reconnection the next time a read/write operation is attempted), as + * well as at the end of the execution of the script. + */ + + public function disconnect() + { + // Close the socket. + + @fclose($this->_con); + $this->_con = null; + + // Return true to indicate success. + + return true; + } + + + /** + * Generic read method. + * + * This method reads a specified number of bytes from the socket. + * By default, it will also read a CRLF sequence (2 bytes) in addition to + * the specified number of bytes, and remove that CRLF sequence once it + * has been read. This is useful for most text-based protocols; however, + * if you do not want such behavior, pass additional 'false' arguments. + * + * @param int The number of bytes to read, or -1 to read until EOF. + * @param bool Whether or not to read CRLF at the end, too. [optional: default is true] + * @return string Data read from the socket. + * @throws Exception If an error occurs while reading from the socket. + */ + + public function read($bytes = -1, $autonewline = true) + { + // If not connected yet, connect now. + + if ($this->_con === null) $this->connect(); + + // Read the data from the socket. + + $data = stream_get_contents($this->_con, $bytes); + + // If $autonewline is true, read 2 more bytes. + + if ($autonewline && $bytes !== -1) stream_get_contents($this->_con, 2); + + // If the result is false, throw an exception. + + if ($data === false) + { + $this->disconnect(); + throw new SimpleSocketException('Cannot read ' . $bytes . ' bytes from ' . $this->_host . ' port ' . $this->_port); + } + + // Otherwise, return the data. + + return $data; + } + + + /** + * Generic readline method. + * + * This method reads one line from the socket, i.e. it reads until it hits + * a CRLF sequence. By default, that CRLF sequence will be removed from the + * return value. This is useful for most text-based protocols; however, + * if you do not want such behavior, pass an additional 'false' argument. + * + * @param bool Whether or not to strip CRLF from the end of the response. [optional: default is true] + * @return string Data read from the socket. + * @throws Exception If an error occurs while reading from the socket. + */ + + public function readline($trim = true) + { + // If not connected yet, connect now. + + if ($this->_con === null) $this->connect(); + + // Read a line from the socket. + + $data = fgets($this->_con); + + // If the result is false, throw an exception. + + if ($data === false) + { + $this->disconnect(); + throw new SimpleSocketException('Cannot read a line from ' . $this->_host . ' port ' . $this->_port); + } + + // Otherwise, trim and return the data. + + if ($trim && substr($data, strlen($data) - 2) === "\r\n") $data = substr($data, 0, strlen($data) - 2); + return $data; + } + + + /** + * Generic write method. + * + * This method writes a string to the socket. By default, this method will + * write a CRLF sequence in addition to the given string. This is useful + * for most text-based protocols; however, if you do not want such behavior, + * make sure to pass an additional 'false' argument. + * + * @param string The string to write to the socket. + * @param bool Whether or write CRLF in addition to the given string. [optional: default is true] + * @return bool True on success. + * @throws Exception If an error occurs while reading from the socket. + */ + + public function write($string, $autonewline = true) + { + // If not connected yet, connect now. + + if ($this->_con === null) $this->connect(); + + // If $autonewline is true, add CRLF to the content. + + if ($autonewline) $string .= "\r\n"; + + // Write the whole string to the socket. + + while ($string !== '') + { + // Start writing. + + $written = fwrite($this->_con, $string); + + // If the result is false, throw an exception. + + if ($written === false) + { + $this->disconnect(); + throw new SimpleSocketException('Cannot write to ' . $this->_host . ' port ' . $this->_port); + } + + // If nothing was written, it probably means we've already done writing. + + if ($written == 0) return true; + + // Prepare the string for the next write. + + $string = substr($string, $written); + } + + // Return true to indicate success. + + return true; + } + + + /** + * Generic key validation method. + * + * This method will throw an exception if: + * - The key is empty. + * - The key is more than 250 bytes long. + * - The key contains characters outside of the ASCII printable range. + * + * @param string The key to validate. + * @return bool True of the key is valid. + * @throws Exception If the key is invalid. + */ + + public function validate_key($key) + { + if ($key === '') throw new InvalidKeyException('Key is empty'); + if (strlen($key) > 250) throw new InvalidKeyException('Key is too long: ' . $key); + if (preg_match('/[^\\x21-\\x7e]/', $key)) throw new InvalidKeyException('Illegal character in key: ' . $key); + return true; + } + + + /** + * Generic command building method. + * + * This method will accept one or more string arguments, and return them + * all concatenated with one space between each. If this is convenient + * for you, help yourself. + * + * @param string As many arguments as you wish. + * @return string The concatenated string. + */ + + public function build_command( /* arguments */ ) + { + $args = func_get_args(); + return implode(' ' , $args); + } + + + /** + * Destructor. + * + * Although not really necessary, the destructor will attempt to + * disconnect in case something weird happens. + */ + + public function __destruct() + { + @fclose($this->_con); + } +} + + +/** + * Exception class. + */ + +class SimpleSocketException extends Exception { } +class InvalidKeyException extends SimpleSocketException { } diff --git a/settings.php b/settings.php index 7ffad48..059840f 100755 --- a/settings.php +++ b/settings.php @@ -25,6 +25,7 @@ OCP\User::checkAdminUser(); $params = array( 'av_mode' => 'executable', + 'av_socket' => '/var/run/clamav/clamd.ctl', 'av_host' => '', 'av_port' => '', 'av_chunk_size' => '1024', diff --git a/templates/settings.php b/templates/settings.php index 0cd42e1..945f7ee 100755 --- a/templates/settings.php +++ b/templates/settings.php @@ -2,8 +2,9 @@
t('Antivirus Configuration'));?>

- +

+

bytes