PageRenderTime 45ms CodeModel.GetById 16ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/antivirus/clamav/classes/scanner.php

https://gitlab.com/unofficial-mirrors/moodle
PHP | 292 lines | 150 code | 20 blank | 122 comment | 26 complexity | cefed76dc6fffa5de969e1f32e8b8391 MD5 | raw file
  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. * ClamAV antivirus integration.
  18. *
  19. * @package antivirus_clamav
  20. * @copyright 2015 Ruslan Kabalin, Lancaster University.
  21. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  22. */
  23. namespace antivirus_clamav;
  24. defined('MOODLE_INTERNAL') || die();
  25. /** Default socket timeout */
  26. define('ANTIVIRUS_CLAMAV_SOCKET_TIMEOUT', 10);
  27. /** Default socket data stream chunk size */
  28. define('ANTIVIRUS_CLAMAV_SOCKET_CHUNKSIZE', 1024);
  29. /**
  30. * Class implementing ClamAV antivirus.
  31. * @copyright 2015 Ruslan Kabalin, Lancaster University.
  32. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  33. */
  34. class scanner extends \core\antivirus\scanner {
  35. /**
  36. * Are the necessary antivirus settings configured?
  37. *
  38. * @return bool True if all necessary config settings been entered
  39. */
  40. public function is_configured() {
  41. if ($this->get_config('runningmethod') === 'commandline') {
  42. return (bool)$this->get_config('pathtoclam');
  43. } else if ($this->get_config('runningmethod') === 'unixsocket') {
  44. return (bool)$this->get_config('pathtounixsocket');
  45. }
  46. return false;
  47. }
  48. /**
  49. * Scan file.
  50. *
  51. * This method is normally called from antivirus manager (\core\antivirus\manager::scan_file).
  52. *
  53. * @param string $file Full path to the file.
  54. * @param string $filename Name of the file (could be different from physical file if temp file is used).
  55. * @return int Scanning result constant.
  56. */
  57. public function scan_file($file, $filename) {
  58. if (!is_readable($file)) {
  59. // This should not happen.
  60. debugging('File is not readable.');
  61. return self::SCAN_RESULT_ERROR;
  62. }
  63. // Execute the scan using preferable method.
  64. $method = 'scan_file_execute_' . $this->get_config('runningmethod');
  65. if (!method_exists($this, $method)) {
  66. throw new \coding_exception('Attempting to call non-existing method ' . $method);
  67. }
  68. $return = $this->$method($file);
  69. if ($return === self::SCAN_RESULT_ERROR) {
  70. $this->message_admins($this->get_scanning_notice());
  71. // If plugin settings require us to act like virus on any error,
  72. // return SCAN_RESULT_FOUND result.
  73. if ($this->get_config('clamfailureonupload') === 'actlikevirus') {
  74. return self::SCAN_RESULT_FOUND;
  75. }
  76. }
  77. return $return;
  78. }
  79. /**
  80. * Scan data.
  81. *
  82. * @param string $data The variable containing the data to scan.
  83. * @return int Scanning result constant.
  84. */
  85. public function scan_data($data) {
  86. // We can do direct stream scanning if unixsocket running method is in use,
  87. // if not, use default process.
  88. if ($this->get_config('runningmethod') === 'unixsocket') {
  89. $return = $this->scan_data_execute_unixsocket($data);
  90. if ($return === self::SCAN_RESULT_ERROR) {
  91. $this->message_admins($this->get_scanning_notice());
  92. // If plugin settings require us to act like virus on any error,
  93. // return SCAN_RESULT_FOUND result.
  94. if ($this->get_config('clamfailureonupload') === 'actlikevirus') {
  95. return self::SCAN_RESULT_FOUND;
  96. }
  97. }
  98. return $return;
  99. } else {
  100. return parent::scan_data($data);
  101. }
  102. }
  103. /**
  104. * Returns the string equivalent of a numeric clam error code
  105. *
  106. * @param int $returncode The numeric error code in question.
  107. * @return string The definition of the error code
  108. */
  109. private function get_clam_error_code($returncode) {
  110. $returncodes = array();
  111. $returncodes[0] = 'No virus found.';
  112. $returncodes[1] = 'Virus(es) found.';
  113. $returncodes[2] = ' An error occured'; // Specific to clamdscan.
  114. // All after here are specific to clamscan.
  115. $returncodes[40] = 'Unknown option passed.';
  116. $returncodes[50] = 'Database initialization error.';
  117. $returncodes[52] = 'Not supported file type.';
  118. $returncodes[53] = 'Can\'t open directory.';
  119. $returncodes[54] = 'Can\'t open file. (ofm)';
  120. $returncodes[55] = 'Error reading file. (ofm)';
  121. $returncodes[56] = 'Can\'t stat input file / directory.';
  122. $returncodes[57] = 'Can\'t get absolute path name of current working directory.';
  123. $returncodes[58] = 'I/O error, please check your filesystem.';
  124. $returncodes[59] = 'Can\'t get information about current user from /etc/passwd.';
  125. $returncodes[60] = 'Can\'t get information about user \'clamav\' (default name) from /etc/passwd.';
  126. $returncodes[61] = 'Can\'t fork.';
  127. $returncodes[63] = 'Can\'t create temporary files/directories (check permissions).';
  128. $returncodes[64] = 'Can\'t write to temporary directory (please specify another one).';
  129. $returncodes[70] = 'Can\'t allocate and clear memory (calloc).';
  130. $returncodes[71] = 'Can\'t allocate memory (malloc).';
  131. if (isset($returncodes[$returncode])) {
  132. return $returncodes[$returncode];
  133. }
  134. return get_string('unknownerror', 'antivirus_clamav');
  135. }
  136. /**
  137. * Scan file using command line utility.
  138. *
  139. * @param string $file Full path to the file.
  140. * @return int Scanning result constant.
  141. */
  142. public function scan_file_execute_commandline($file) {
  143. $pathtoclam = trim($this->get_config('pathtoclam'));
  144. if (!file_exists($pathtoclam) or !is_executable($pathtoclam)) {
  145. // Misconfigured clam, notify admins.
  146. $notice = get_string('invalidpathtoclam', 'antivirus_clamav', $pathtoclam);
  147. $this->set_scanning_notice($notice);
  148. return self::SCAN_RESULT_ERROR;
  149. }
  150. $clamparam = ' --stdout ';
  151. // If we are dealing with clamdscan, clamd is likely run as a different user
  152. // that might not have permissions to access your file.
  153. // To make clamdscan work, we use --fdpass parameter that passes the file
  154. // descriptor permissions to clamd, which allows it to scan given file
  155. // irrespective of directory and file permissions.
  156. if (basename($pathtoclam) == 'clamdscan') {
  157. $clamparam .= '--fdpass ';
  158. }
  159. // Execute scan.
  160. $cmd = escapeshellcmd($pathtoclam).$clamparam.escapeshellarg($file);
  161. exec($cmd, $output, $return);
  162. // Return variable will contain execution return code. It will be 0 if no virus is found,
  163. // 1 if virus is found, and 2 or above for the error. Return codes 0 and 1 correspond to
  164. // SCAN_RESULT_OK and SCAN_RESULT_FOUND constants, so we return them as it is.
  165. // If there is an error, it gets stored as scanning notice and function
  166. // returns SCAN_RESULT_ERROR.
  167. if ($return > self::SCAN_RESULT_FOUND) {
  168. $notice = get_string('clamfailed', 'antivirus_clamav', $this->get_clam_error_code($return));
  169. $notice .= "\n\n". implode("\n", $output);
  170. $this->set_scanning_notice($notice);
  171. return self::SCAN_RESULT_ERROR;
  172. }
  173. return (int)$return;
  174. }
  175. /**
  176. * Scan file using Unix domain sockets.
  177. *
  178. * @param string $file Full path to the file.
  179. * @return int Scanning result constant.
  180. */
  181. public function scan_file_execute_unixsocket($file) {
  182. $socket = stream_socket_client('unix://' . $this->get_config('pathtounixsocket'),
  183. $errno, $errstr, ANTIVIRUS_CLAMAV_SOCKET_TIMEOUT);
  184. if (!$socket) {
  185. // Can't open socket for some reason, notify admins.
  186. $notice = get_string('errorcantopensocket', 'antivirus_clamav', "$errstr ($errno)");
  187. $this->set_scanning_notice($notice);
  188. return self::SCAN_RESULT_ERROR;
  189. } else {
  190. // Execute scanning. We are running SCAN command and passing file as an argument,
  191. // it is the fastest option, but clamav user need to be able to access it, so
  192. // we give group read permissions first and assume 'clamav' user is in web server
  193. // group (in Debian the default webserver group is 'www-data').
  194. // Using 'n' as command prefix is forcing clamav to only treat \n as newline delimeter,
  195. // this is to avoid unexpected newline characters on different systems.
  196. $perms = fileperms($file);
  197. chmod($file, 0640);
  198. fwrite($socket, "nSCAN ".$file."\n");
  199. $output = stream_get_line($socket, 4096);
  200. fclose($socket);
  201. // After scanning we revert permissions to initial ones.
  202. chmod($file, $perms);
  203. // Parse the output.
  204. return $this->parse_unixsocket_response($output);
  205. }
  206. }
  207. /**
  208. * Scan data using unix socket.
  209. *
  210. * We are running INSTREAM command and passing data stream in chunks.
  211. * The format of the chunk is: <length><data> where <length> is the size of the following
  212. * data in bytes expressed as a 4 byte unsigned integer in network byte order and <data>
  213. * is the actual chunk. Streaming is terminated by sending a zero-length chunk.
  214. * Do not exceed StreamMaxLength as defined in clamd.conf, otherwise clamd will
  215. * reply with INSTREAM size limit exceeded and close the connection.
  216. *
  217. * @param string $data The varaible containing the data to scan.
  218. * @return int Scanning result constant.
  219. */
  220. public function scan_data_execute_unixsocket($data) {
  221. $socket = stream_socket_client('unix://' . $this->get_config('pathtounixsocket'), $errno, $errstr, ANTIVIRUS_CLAMAV_SOCKET_TIMEOUT);
  222. if (!$socket) {
  223. // Can't open socket for some reason, notify admins.
  224. $notice = get_string('errorcantopensocket', 'antivirus_clamav', "$errstr ($errno)");
  225. $this->set_scanning_notice($notice);
  226. return self::SCAN_RESULT_ERROR;
  227. } else {
  228. // Initiate data stream scanning.
  229. // Using 'n' as command prefix is forcing clamav to only treat \n as newline delimeter,
  230. // this is to avoid unexpected newline characters on different systems.
  231. fwrite($socket, "nINSTREAM\n");
  232. // Send data in chunks of ANTIVIRUS_CLAMAV_SOCKET_CHUNKSIZE size.
  233. while (strlen($data) > 0) {
  234. $chunk = substr($data, 0, ANTIVIRUS_CLAMAV_SOCKET_CHUNKSIZE);
  235. $data = substr($data, ANTIVIRUS_CLAMAV_SOCKET_CHUNKSIZE);
  236. $size = pack('N', strlen($chunk));
  237. fwrite($socket, $size);
  238. fwrite($socket, $chunk);
  239. }
  240. // Terminate streaming.
  241. fwrite($socket, pack('N', 0));
  242. $output = stream_get_line($socket, 4096);
  243. fclose($socket);
  244. // Parse the output.
  245. return $this->parse_unixsocket_response($output);
  246. }
  247. }
  248. /**
  249. * Parse unix socket command response.
  250. *
  251. * @param string $output The unix socket command response.
  252. * @return int Scanning result constant.
  253. */
  254. private function parse_unixsocket_response($output) {
  255. $splitoutput = explode(': ', $output);
  256. $message = trim($splitoutput[1]);
  257. if ($message === 'OK') {
  258. return self::SCAN_RESULT_OK;
  259. } else {
  260. $parts = explode(' ', $message);
  261. $status = array_pop($parts);
  262. if ($status === 'FOUND') {
  263. return self::SCAN_RESULT_FOUND;
  264. } else {
  265. $notice = get_string('clamfailed', 'antivirus_clamav', $this->get_clam_error_code(2));
  266. $notice .= "\n\n" . $output;
  267. $this->set_scanning_notice($notice);
  268. return self::SCAN_RESULT_ERROR;
  269. }
  270. }
  271. }
  272. }