Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 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
/**
18
 * ClamAV antivirus integration.
19
 *
20
 * @package    antivirus_clamav
21
 * @copyright  2015 Ruslan Kabalin, Lancaster University.
22
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23
 */
24
 
25
namespace antivirus_clamav;
26
 
27
defined('MOODLE_INTERNAL') || die();
28
 
29
/** Default socket timeout */
30
define('ANTIVIRUS_CLAMAV_SOCKET_TIMEOUT', 10);
31
/** Default socket data stream chunk size (32Mb: 32 * 1024 * 1024) */
32
define('ANTIVIRUS_CLAMAV_SOCKET_CHUNKSIZE', 33554432);
33
 
34
/**
35
 * Class implementing ClamAV antivirus.
36
 * @copyright  2015 Ruslan Kabalin, Lancaster University.
37
 * @copyright  2019 Didier Raboud, Liip AG.
38
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
39
 */
40
class scanner extends \core\antivirus\scanner {
41
    /**
42
     * Are the necessary antivirus settings configured?
43
     *
44
     * @return bool True if all necessary config settings been entered
45
     */
46
    public function is_configured() {
47
        if ($this->get_config('runningmethod') === 'commandline') {
48
            return (bool)$this->get_config('pathtoclam');
49
        } else if ($this->get_config('runningmethod') === 'unixsocket') {
50
            return (bool)$this->get_config('pathtounixsocket');
51
        } else if ($this->get_config('runningmethod') === 'tcpsocket') {
52
            return (bool)$this->get_config('tcpsockethost') && (bool)$this->get_config('tcpsocketport');
53
        }
54
        return false;
55
    }
56
 
57
    /**
58
     * Scan file.
59
     *
60
     * This method is normally called from antivirus manager (\core\antivirus\manager::scan_file).
61
     *
62
     * @param string $file Full path to the file.
63
     * @param string $filename Name of the file (could be different from physical file if temp file is used).
64
     * @return int Scanning result constant.
65
     */
66
    public function scan_file($file, $filename) {
67
        if (!is_readable($file)) {
68
            // This should not happen.
69
            debugging('File is not readable.');
70
            return self::SCAN_RESULT_ERROR;
71
        }
72
 
73
        // We can do direct stream scanning if unixsocket or tcpsocket running methods are in use,
74
        // if not, use default process.
75
        $maxtries = get_config('antivirus_clamav', 'tries');
76
        $tries = 0;
77
        do {
78
            $runningmethod = $this->get_config('runningmethod');
79
            $tries++;
80
            switch ($runningmethod) {
81
                case 'unixsocket':
82
                case 'tcpsocket':
83
                    $return = $this->scan_file_execute_socket($file, $runningmethod);
84
                    break;
85
                case 'commandline':
86
                    $return = $this->scan_file_execute_commandline($file);
87
                    break;
88
                default:
89
                    // This should not happen.
90
                    throw new \coding_exception('Unknown running method.');
91
            }
92
        } while ($return == self::SCAN_RESULT_ERROR && $tries < $maxtries);
93
 
94
        $notice = get_string('tries_notice', 'antivirus_clamav',
95
            ['tries' => $tries, 'notice' => $this->get_scanning_notice()]);
96
        $this->set_scanning_notice($notice);
97
 
98
        if ($return === self::SCAN_RESULT_ERROR) {
99
            $this->message_admins($this->get_scanning_notice());
100
            // If plugin settings require us to act like virus on any error,
101
            // return SCAN_RESULT_FOUND result.
102
            if ($this->get_config('clamfailureonupload') === 'actlikevirus') {
103
                return self::SCAN_RESULT_FOUND;
104
            } else if ($this->get_config('clamfailureonupload') === 'tryagain') {
105
                // Do not upload the file, just give a message to the user to try again later.
106
                unlink($file);
107
                throw new \core\antivirus\scanner_exception('antivirusfailed', '', ['item' => $filename],
108
                        null, 'antivirus_clamav');
109
            }
110
        }
111
        return $return;
112
    }
113
 
114
    /**
115
     * Scan data.
116
     *
117
     * @param string $data The variable containing the data to scan.
118
     * @return int Scanning result constant.
119
     */
120
    public function scan_data($data) {
121
        // We can do direct stream scanning if unixsocket or tcpsocket running methods are in use,
122
        // if not, use default process.
123
        $runningmethod = $this->get_config('runningmethod');
124
        if (in_array($runningmethod, array('unixsocket', 'tcpsocket'))) {
125
            $return = $this->scan_data_execute_socket($data, $runningmethod);
126
 
127
            if ($return === self::SCAN_RESULT_ERROR) {
128
                $this->message_admins($this->get_scanning_notice());
129
                // If plugin settings require us to act like virus on any error,
130
                // return SCAN_RESULT_FOUND result.
131
                if ($this->get_config('clamfailureonupload') === 'actlikevirus') {
132
                    return self::SCAN_RESULT_FOUND;
133
                }
134
            }
135
            return $return;
136
        } else {
137
            return parent::scan_data($data);
138
        }
139
    }
140
 
141
    /**
142
     * Returns a Unix domain socket destination url
143
     *
144
     * @return string The socket url, fit for stream_socket_client()
145
     */
146
    private function get_unixsocket_destination() {
147
        return 'unix://' . $this->get_config('pathtounixsocket');
148
    }
149
 
150
    /**
151
     * Returns a Internet domain socket destination url
152
     *
153
     * @return string The socket url, fit for stream_socket_client()
154
     */
155
    private function get_tcpsocket_destination() {
156
        return 'tcp://' . $this->get_config('tcpsockethost') . ':' . $this->get_config('tcpsocketport');
157
    }
158
 
159
    /**
160
     * Returns the string equivalent of a numeric clam error code
161
     *
162
     * @param int $returncode The numeric error code in question.
163
     * @return string The definition of the error code
164
     */
165
    private function get_clam_error_code($returncode) {
166
        $returncodes = array();
167
        $returncodes[0] = 'No virus found.';
168
        $returncodes[1] = 'Virus(es) found.';
169
        $returncodes[2] = ' An error occured'; // Specific to clamdscan.
170
        // All after here are specific to clamscan.
171
        $returncodes[40] = 'Unknown option passed.';
172
        $returncodes[50] = 'Database initialization error.';
173
        $returncodes[52] = 'Not supported file type.';
174
        $returncodes[53] = 'Can\'t open directory.';
175
        $returncodes[54] = 'Can\'t open file. (ofm)';
176
        $returncodes[55] = 'Error reading file. (ofm)';
177
        $returncodes[56] = 'Can\'t stat input file / directory.';
178
        $returncodes[57] = 'Can\'t get absolute path name of current working directory.';
179
        $returncodes[58] = 'I/O error, please check your filesystem.';
180
        $returncodes[59] = 'Can\'t get information about current user from /etc/passwd.';
181
        $returncodes[60] = 'Can\'t get information about user \'clamav\' (default name) from /etc/passwd.';
182
        $returncodes[61] = 'Can\'t fork.';
183
        $returncodes[63] = 'Can\'t create temporary files/directories (check permissions).';
184
        $returncodes[64] = 'Can\'t write to temporary directory (please specify another one).';
185
        $returncodes[70] = 'Can\'t allocate and clear memory (calloc).';
186
        $returncodes[71] = 'Can\'t allocate memory (malloc).';
187
        if (isset($returncodes[$returncode])) {
188
            return $returncodes[$returncode];
189
        }
190
        return get_string('unknownerror', 'antivirus_clamav');
191
    }
192
 
193
    /**
194
     * Scan file using command line utility.
195
     *
196
     * @param string $file Full path to the file.
197
     * @return int Scanning result constant.
198
     */
199
    public function scan_file_execute_commandline($file) {
200
        $pathtoclam = trim($this->get_config('pathtoclam'));
201
 
202
        if (!file_exists($pathtoclam) or !is_executable($pathtoclam)) {
203
            // Misconfigured clam, notify admins.
204
            $notice = get_string('invalidpathtoclam', 'antivirus_clamav', $pathtoclam);
205
            $this->set_scanning_notice($notice);
206
            return self::SCAN_RESULT_ERROR;
207
        }
208
 
209
        $clamparam = ' --stdout ';
210
        // If we are dealing with clamdscan, clamd is likely run as a different user
211
        // that might not have permissions to access your file.
212
        // To make clamdscan work, we use --fdpass parameter that passes the file
213
        // descriptor permissions to clamd, which allows it to scan given file
214
        // irrespective of directory and file permissions.
215
        if (basename($pathtoclam) == 'clamdscan') {
216
            $clamparam .= '--fdpass ';
217
        }
218
        // Execute scan.
219
        $cmd = escapeshellcmd($pathtoclam).$clamparam.escapeshellarg($file);
220
        exec($cmd, $output, $return);
221
        // Return variable will contain execution return code. It will be 0 if no virus is found,
222
        // 1 if virus is found, and 2 or above for the error. Return codes 0 and 1 correspond to
223
        // SCAN_RESULT_OK and SCAN_RESULT_FOUND constants, so we return them as it is.
224
        // If there is an error, it gets stored as scanning notice and function
225
        // returns SCAN_RESULT_ERROR.
226
        if ($return > self::SCAN_RESULT_FOUND) {
227
            $notice = get_string('clamfailed', 'antivirus_clamav', $this->get_clam_error_code($return));
228
            $notice .= "\n\n". implode("\n", $output);
229
            $this->set_scanning_notice($notice);
230
            return self::SCAN_RESULT_ERROR;
231
        } else {
232
            $notice = "\n\n". implode("\n", $output);
233
            $this->set_scanning_notice($notice);
234
        }
235
 
236
        return (int)$return;
237
    }
238
 
239
    /**
240
     * Scan file using sockets.
241
     *
242
     * @param string $file Full path to the file.
243
     * @param string $type Either 'tcpsocket' or 'unixsocket'
244
     * @return int Scanning result constant.
245
     */
246
    public function scan_file_execute_socket($file, $type) {
247
        switch ($type) {
248
            case "tcpsocket":
249
                $socketurl = $this->get_tcpsocket_destination();
250
                break;
251
            case "unixsocket":
252
                $socketurl = $this->get_unixsocket_destination();
253
                break;
254
            default;
255
                // This should not happen.
256
                debugging('Unknown socket type.');
257
                return self::SCAN_RESULT_ERROR;
258
        }
259
 
260
        $socket = stream_socket_client($socketurl,
261
                $errno, $errstr, ANTIVIRUS_CLAMAV_SOCKET_TIMEOUT);
262
        if (!$socket) {
263
            // Can't open socket for some reason, notify admins.
264
            $notice = get_string('errorcantopensocket', 'antivirus_clamav', "$errstr ($errno)");
265
            $this->set_scanning_notice($notice);
266
            return self::SCAN_RESULT_ERROR;
267
        } else {
268
            if ($type == "unixsocket") {
269
                // Execute scanning. We are running SCAN command and passing file as an argument,
270
                // it is the fastest option, but clamav user need to be able to access it, so
271
                // we give group read permissions first and assume 'clamav' user is in web server
272
                // group (in Debian the default webserver group is 'www-data').
273
                // Using 'n' as command prefix is forcing clamav to only treat \n as newline delimeter,
274
                // this is to avoid unexpected newline characters on different systems.
275
                $perms = fileperms($file);
276
                chmod($file, 0640);
277
 
278
                // Actual scan.
279
                fwrite($socket, "nSCAN ".$file."\n");
280
                // Get ClamAV answer.
281
                $output = stream_get_line($socket, 4096);
282
 
283
                // After scanning we revert permissions to initial ones.
284
                chmod($file, $perms);
285
            } else if ($type == "tcpsocket") {
286
                // Execute scanning by passing the entire file through the TCP socket.
287
                // This is not fast, but is the only possibility over a network.
288
                // Using 'n' as command prefix is forcing clamav to only treat \n as newline delimeter,
289
                // this is to avoid unexpected newline characters on different systems.
290
 
291
                // Actual scan.
292
                fwrite($socket, "nINSTREAM\n");
293
 
294
                // Open the file for reading.
295
                $fhandle = fopen($file, 'rb');
296
                while (!feof($fhandle)) {
297
                    // Read it by chunks; write them to the TCP socket.
298
                    $chunk = fread($fhandle, ANTIVIRUS_CLAMAV_SOCKET_CHUNKSIZE);
299
                    $size = pack('N', strlen($chunk));
300
                    fwrite($socket, $size);
301
                    fwrite($socket, $chunk);
302
                }
303
                // Terminate streaming.
304
                fwrite($socket, pack('N', 0));
305
                // Get ClamAV answer.
306
                $output = stream_get_line($socket, 4096);
307
 
308
                fclose($fhandle);
309
            }
310
            // Free up the ClamAV socket.
311
            fclose($socket);
312
            // Parse the output.
313
            return $this->parse_socket_response($output);
314
        }
315
    }
316
 
317
    /**
318
     * Scan data socket.
319
     *
320
     * We are running INSTREAM command and passing data stream in chunks.
321
     * The format of the chunk is: <length><data> where <length> is the size of the following
322
     * data in bytes expressed as a 4 byte unsigned integer in network byte order and <data>
323
     * is the actual chunk. Streaming is terminated by sending a zero-length chunk.
324
     * Do not exceed StreamMaxLength as defined in clamd.conf, otherwise clamd will
325
     * reply with INSTREAM size limit exceeded and close the connection.
326
     *
327
     * @param string $data The variable containing the data to scan.
328
     * @param string $type Either 'tcpsocket' or 'unixsocket'
329
     * @return int Scanning result constant.
330
     */
331
    public function scan_data_execute_socket($data, $type) {
332
        switch ($type) {
333
            case "tcpsocket":
334
                $socketurl = $this->get_tcpsocket_destination();
335
                break;
336
            case "unixsocket":
337
                $socketurl = $this->get_unixsocket_destination();
338
                break;
339
            default;
340
                // This should not happen.
341
                debugging('Unknown socket type!');
342
                return self::SCAN_RESULT_ERROR;
343
        }
344
 
345
        $socket = stream_socket_client($socketurl, $errno, $errstr, ANTIVIRUS_CLAMAV_SOCKET_TIMEOUT);
346
        if (!$socket) {
347
            // Can't open socket for some reason, notify admins.
348
            $notice = get_string('errorcantopensocket', 'antivirus_clamav', "$errstr ($errno)");
349
            $this->set_scanning_notice($notice);
350
            return self::SCAN_RESULT_ERROR;
351
        } else {
352
            // Initiate data stream scanning.
353
            // Using 'n' as command prefix is forcing clamav to only treat \n as newline delimeter,
354
            // this is to avoid unexpected newline characters on different systems.
355
            fwrite($socket, "nINSTREAM\n");
356
            // Send data in chunks of ANTIVIRUS_CLAMAV_SOCKET_CHUNKSIZE size.
357
            while (strlen($data) > 0) {
358
                $chunk = substr($data, 0, ANTIVIRUS_CLAMAV_SOCKET_CHUNKSIZE);
359
                $data = substr($data, ANTIVIRUS_CLAMAV_SOCKET_CHUNKSIZE);
360
                $size = pack('N', strlen($chunk));
361
                fwrite($socket, $size);
362
                fwrite($socket, $chunk);
363
            }
364
            // Terminate streaming.
365
            fwrite($socket, pack('N', 0));
366
 
367
            $output = stream_get_line($socket, 4096);
368
            fclose($socket);
369
 
370
            // Parse the output.
371
            return $this->parse_socket_response($output);
372
        }
373
    }
374
 
375
    /**
376
     * Parse socket command response.
377
     *
378
     * @param string $output The socket response.
379
     * @return int Scanning result constant.
380
     */
381
    private function parse_socket_response($output) {
382
        $splitoutput = explode(': ', $output);
383
        $message = trim($splitoutput[1]);
384
        if ($message === 'OK') {
385
            return self::SCAN_RESULT_OK;
386
        } else {
387
            $parts = explode(' ', $message);
388
            $status = array_pop($parts);
389
            if ($status === 'FOUND') {
390
                $notice = "\n\n" . $output;
391
                $this->set_scanning_notice($notice);
392
                return self::SCAN_RESULT_FOUND;
393
            } else {
394
                $notice = get_string('clamfailed', 'antivirus_clamav', $this->get_clam_error_code(2));
395
                $notice .= "\n\n" . $output;
396
                $this->set_scanning_notice($notice);
397
                return self::SCAN_RESULT_ERROR;
398
            }
399
        }
400
    }
401
 
402
    /**
403
     * Scan data using Unix domain socket.
404
     *
405
     * @deprecated since Moodle 3.9 MDL-64075 - please do not use this function any more.
406
     * @see antivirus_clamav\scanner::scan_data_execute_socket()
407
     *
408
     * @param string $data The variable containing the data to scan.
409
     * @return int Scanning result constant.
410
     */
411
    public function scan_data_execute_unixsocket($data) {
412
        debugging('antivirus_clamav\scanner::scan_data_execute_unixsocket() is deprecated. ' .
413
                  'Use antivirus_clamav\scanner::scan_data_execute_socket() instead.', DEBUG_DEVELOPER);
414
        return $this->scan_data_execute_socket($data, "unixsocket");
415
    }
416
 
417
    /**
418
     * Scan file using Unix domain socket.
419
     *
420
     * @deprecated since Moodle 3.9 MDL-64075 - please do not use this function any more.
421
     * @see antivirus_clamav\scanner::scan_file_execute_socket()
422
     *
423
     * @param string $file Full path to the file.
424
     * @return int Scanning result constant.
425
     */
426
    public function scan_file_execute_unixsocket($file) {
427
        debugging('antivirus_clamav\scanner::scan_file_execute_unixsocket() is deprecated. ' .
428
                  'Use antivirus_clamav\scanner::scan_file_execute_socket() instead.', DEBUG_DEVELOPER);
429
        return $this->scan_file_execute_socket($file, "unixsocket");
430
    }
431
 
432
}