You are here

clamav.inc in ClamAV 7

Same filename and directory in other branches
  1. 6 clamav.inc

clamav.inc API and helper functions for the ClamAV module.

File

clamav.inc
View source
<?php

/**
 * @file clamav.inc
 * API and helper functions for the ClamAV module.
 */

/**
 * Scan a file and raise an error on the form element (if required).
 *
 * @param String $filepath     Path to the file to test
 * @param String $form_element The form element to set an error on.
 *                             E.g. foo
 *                             E.g. foo][bar][baz
 */
function clamav_scan($filepath, $form_element) {
  $result = clamav_scan_file($filepath);
  if ($result == CLAMAV_SCANRESULT_INFECTED) {
    form_set_error($form_element, t('A virus has been detected in the file.  The file will not be accepted.'));
  }
  elseif ($result == CLAMAV_SCANRESULT_UNCHECKED && variable_get('clamav_unchecked_files', CLAMAV_DEFAULT_UNCHECKED) == CLAMAV_BLOCK_UNCHECKED) {
    form_set_error($form_element, t('The anti-virus scanner was not able to check the file.  The file cannot be uploaded.'));
  }
}

/**
 * Scan a single file
 *
 * @param String $filepath
 * Full filepath to the file which is to be scanned. This must be a file which
 * is readable by the web server.
 * @param optional String $filename
 * Filename of the uploaded file. This is used to log the original name of the
 * uploaded file ($filepath will typically be a random string generated by
 * PHP). Defaults to $filepath if not provided.
 *
 * @return int
 * one of:
 * - CLAMAV_SCANRESULT_UNCHECKED
 * - CLAMAV_SCANRESULT_CLEAN
 * - CLAMAV_SCANRESULT_INFECTED
 */
function clamav_scan_file($filepath, $filename = NULL) {
  if (is_null($filename)) {

    // Use the filepath to provide a default for the filename.
    $filename = basename($filepath);
  }
  else {

    // Ensure that the filename doesn't include the path.
    $filename = basename($filename);
  }
  switch (variable_get('clamav_mode', CLAMAV_DEFAULT_MODE)) {
    case CLAMAV_USE_DAEMON:
      return _clamav_scan_via_daemon($filepath, $filename);
    case CLAMAV_USE_EXECUTABLE:
      return _clamav_scan_via_exec($filepath, $filename);
    case CLAMAV_USE_DAEMON_UNIX_SOCKET:
      return _clamav_scan_via_unix_socket($filepath, $filename);
    default:
      watchdog('ClamAV', 'Unrecognised configuration.', array(), WATCHDOG_ERROR);
      return CLAMAV_SCANRESULT_UNCHECKED;
  }
}

/**
 * Scan a single file using a daemon.
 *
 * @param String $filepath
 * Full filepath to the file which is to be scanned. This must be a file which
 * is readable by the web server.
 * @param String $filename
 * Filename of the uploaded file (used for logging).
 *
 * @return int
 * one of:
 * - CLAMAV_SCANRESULT_UNCHECKED
 * - CLAMAV_SCANRESULT_CLEAN
 * - CLAMAV_SCANRESULT_INFECTED
 */
function _clamav_scan_via_daemon($filepath, $filename) {
  $host = variable_get('clamav_daemon_host', CLAMAV_DEFAULT_HOST);
  $port = variable_get('clamav_daemon_port', CLAMAV_DEFAULT_PORT);

  // try to open a socket to clamav
  $handler = $host && $port ? @fsockopen($host, $port) : FALSE;
  if (!$handler) {
    watchdog('clamav', 'The clamav module can not connect to the clamav daemon over TCP/IP.  The uploaded file %filename could not be scanned.', array(
      '%filename' => $filename,
    ), WATCHDOG_WARNING);
    return CLAMAV_SCANRESULT_UNCHECKED;
  }
  return _clamav_scan_via_daemon_handler($handler, $filepath, $filename);
}

/**
 * Scan a single file by connecting to ClamAV over a unix socket.
 *
 * @param String $filepath
 * Full filepath to the file which is to be scanned. This must be a file which
 * is readable by the web server.
 * @param String $filename
 * Filename of the uploaded file (used for logging).
 *
 * @return int
 * one of:
 * - CLAMAV_SCANRESULT_UNCHECKED
 * - CLAMAV_SCANRESULT_CLEAN
 * - CLAMAV_SCANRESULT_INFECTED
 */
function _clamav_scan_via_unix_socket($filepath, $filename) {
  $socket_path = variable_get('clamav_daemon_unix_socket', CLAMAV_DEFAULT_UNIX_SOCKET);
  $socket = "unix://{$socket_path}";

  // try to open a socket to clamav
  $handler = $socket_path ? @fsockopen($socket) : FALSE;
  if (!$handler) {
    watchdog('clamav', 'The clamav module can not connect to the clamav daemon over unix socket.  The uploaded file %filename could not be scanned.', array(
      '%filename' => $filename,
    ), WATCHDOG_WARNING);
    return CLAMAV_SCANRESULT_UNCHECKED;
  }
  return _clamav_scan_via_daemon_handler($handler, $filepath, $filename);
}

/**
 * Scan a single file via ClamAV daemon.
 *
 *
 * @param resource $handler
 * An opened connection to a daemon socket resource.
 * @param String $filepath
 * Full filepath to the file which is to be scanned. This must be a file which
 * is readable by the web server.
 * @param String $filename
 * Filename of the uploaded file (used for logging).
 *
 * @return int
 * one of:
 * - CLAMAV_SCANRESULT_UNCHECKED
 * - CLAMAV_SCANRESULT_CLEAN
 * - CLAMAV_SCANRESULT_INFECTED
 */
function _clamav_scan_via_daemon_handler($handler, $filepath, $filename) {

  // Request a scan from the daemon.
  $filehandler = fopen($filepath, 'r');
  if ($filehandler) {

    // Open a request with the daemon to stream file data.
    fwrite($handler, "zINSTREAM\0");
    $bytes = filesize($filepath);
    if ($bytes > 0) {

      // Tell the daemon how many bytes of data we're sending.
      fwrite($handler, pack("N", $bytes));

      // Send the file data.
      stream_copy_to_stream($filehandler, $handler);
    }

    // Send a zero-length block to indicate that we're done sending file data.
    fwrite($handler, pack("N", 0));
    $response = fgets($handler);
    fclose($filehandler);
    fclose($handler);
    $response = trim($response);
  }
  else {
    watchdog('clamav', 'Uploaded file %filename could not be scanned: failed to open file handle.', array(
      '%filename' => $filename,
    ), WATCHDOG_WARNING);
    return CLAMAV_SCANRESULT_UNCHECKED;
  }

  // clamd returns a string response in the format:
  // stream: OK
  // stream: <name of virus> FOUND
  // stream: <error string> ERROR
  if (preg_match('/^stream: OK$/', $response)) {

    // Log the message to watchdog, if verbose mode is used.
    if (variable_get('clamav_verbose', CLAMAV_VERBOSE_DEFAULT)) {
      watchdog('clamav', 'File %filename scanned by ClamAV and found clean.', array(
        '%filename' => $filename,
      ), WATCHDOG_INFO);
    }
    return CLAMAV_SCANRESULT_CLEAN;
  }
  elseif (preg_match('/^stream: (.*) FOUND$/', $response, $matches)) {
    $virus_name = $matches[1];
    watchdog('clamav', 'Virus detected in uploaded file %filename.  Clamav-daemon reported the virus:<br />@virus_name', array(
      '%filename' => $filename,
      '@virus_name' => $virus_name,
    ), WATCHDOG_CRITICAL);
    return CLAMAV_SCANRESULT_INFECTED;
  }
  else {

    // try to extract the error message from the response.
    preg_match('/^stream: (.*) ERROR$/', $response, $matches);
    $error_string = $matches[1];

    // the error message given by the daemon
    watchdog('clamav', 'Uploaded file %filename could not be scanned.  Clamscan reported:<br />@error_string', array(
      '%filename' => $filename,
      '@error_string' => $error_string,
    ), WATCHDOG_WARNING);
    return CLAMAV_SCANRESULT_UNCHECKED;
  }
}

/**
 * Scan a single file using a clamav executable.
 *
 * @param String $filepath
 * Full filepath to the file which is to be scanned. This must be a file which
 * is readable by the web server.
 * @param String $filename
 * Filename of the uploaded file (used for logging).
 *
 * @return int
 * one of:
 * - CLAMAV_SCANRESULT_UNCHECKED
 * - CLAMAV_SCANRESULT_CLEAN
 * - CLAMAV_SCANRESULT_INFECTED
 */
function _clamav_scan_via_exec($filepath, $filename) {

  // get the path to the executable
  $executable = variable_get('clamav_executable_path', CLAMAV_DEFAULT_PATH);

  // check that the executable is available
  if (!file_exists($executable)) {
    watchdog('clamav', "The clamscan executable could not be found at %path", array(
      '%path' => $executable,
    ), WATCHDOG_ERROR);
    return CLAMAV_SCANRESULT_UNCHECKED;
  }

  // optional additional options for the executable
  $exec_params = trim(variable_get('clamav_executable_parameters', CLAMAV_DEFAULT_PARAMS));
  if (!empty($exec_params)) {

    // individually shell-escape multiple options in the string
    $exec_params = explode(' ', $exec_params);
    $exec_params = array_filter($exec_params);
    $exec_params = array_map('escapeshellarg', $exec_params);
    $exec_params = implode(' ', $exec_params);
  }
  $filepath = drupal_realpath($filepath);

  // using 2>&1 to grab the full command-line output.
  $cmd = escapeshellcmd($executable);
  $cmd .= !empty($exec_params) ? ' ' . $exec_params : '';

  // already escaped
  $cmd .= ' ' . escapeshellarg($filepath) . ' 2>&1';

  // exec:
  // The lines of text output by clamscan are assigned as an array to $output
  // The actual result of clamscan is assigned to $result:
  // 0 = clean
  // 1 = infected
  // x = unchecked
  exec($cmd, $output, $result);

  /**
   * clamscan return values (documented from man clamscan)
   *  0 : No virus found.
   *  1 : Virus(es) found.
   *  2 : Some error(s) occured
   *
   * Specific return codes below retained for backwards-compatibilty
   *   (with very old versions of clamscan < 0.96)
   *
   * 40: Unknown option passed.
   * 50: Database initialization error.
   * 52: Not supported file type.
   * 53: Can't open directory.
   * 54: Can't open file. (ofm)
   * 55: Error reading file. (ofm)
   * 56: Can't stat input file / directory.
   * 57: Can't get absolute path name of current working directory.
   * 58: I/O error, please check your file system.
   * 62: Can't initialize logger.
   * 63: Can't create temporary files/directories (check permissions).
   * 64: Can't write to temporary directory (please specify another one).
   * 70: Can't allocate memory (calloc).
   * 71: Can't allocate memory (malloc).
   */
  switch ($result) {
    case 0:

      // Log the message to watchdog, if verbose mode is used.
      if (variable_get('clamav_verbose', CLAMAV_VERBOSE_DEFAULT)) {
        watchdog('clamav', 'File %filename scanned by ClamAV and found clean.', array(
          '%filename' => $filename,
        ), WATCHDOG_INFO);
      }
      return CLAMAV_SCANRESULT_CLEAN;
    case 1:
      watchdog('clamav', 'Virus detected in uploaded file %filename. Clamscan reported:<br />!clamscan_output', array(
        '%filename' => $filename,
        '!clamscan_output' => _clamav_sanitise_output($output),
      ), WATCHDOG_CRITICAL);
      return CLAMAV_SCANRESULT_INFECTED;
    default:
      $descriptions = array(
        2 => "Some error(s) occured.",
        40 => "Unknown option passed.",
        50 => "Database initialization error.",
        52 => "Not supported file type.",
        53 => "Can't open directory.",
        54 => "Can't open file. (ofm)",
        55 => "Error reading file. (ofm)",
        56 => "Can't stat input file / directory.",
        57 => "Can't get absolute path name of current working directory.",
        58 => "I/O error, please check your file system.",
        62 => "Can't initialize logger.",
        63 => "Can't create temporary files/directories (check permissions).",
        64 => "Can't write to temporary directory (please specify another one).",
        70 => "Can't allocate memory (calloc).",
        71 => "Can't allocate memory (malloc).",
      );
      $description = array_key_exists($result, $descriptions) ? $descriptions[$result] : 'unknown error';
      watchdog('clamav', 'Uploaded file %filename could not be scanned. Clamscan reported: [@error_code] - @error_description Output:<br />!clamscan_output', array(
        '%filename' => $filename,
        '@error_code' => $result,
        '@error_description' => $description,
        '!clamscan_output' => _clamav_sanitise_output($output),
      ), WATCHDOG_WARNING);
      return CLAMAV_SCANRESULT_UNCHECKED;
  }
}

/**
 * Sanitise output from clamscan.
 *
 * @param array $output
 * Lines of output from clamscan executable.
 *
 * @return string
 * Sanitised output.
 */
function _clamav_sanitise_output($output) {
  if (!is_array($output)) {
    $output = array(
      $output,
    );
  }
  $output = array_map('check_plain', $output);
  return implode('<br />', $output);
}

/**
 * Get the version information for clamav.
 *
 * @param Mixed $settings For executable:
 *                          String: path to the clamscan executable
 *                        For daemon:
 *                          Array: providing the keys 'host' and 'port'
 *
 * @return String
 * The version string, as provided by clamav.
 */
function clamav_get_version($settings) {
  if (is_string($settings) && !empty($settings)) {
    return _clamav_get_version_via_exec($settings);
  }
  elseif (is_array($settings) && isset($settings['host']) && isset($settings['port'])) {
    return _clamav_get_version_via_daemon($settings['host'], $settings['port']);
  }
  elseif (is_array($settings) && isset($settings['unix_socket_path'])) {
    $socket = "unix://{$settings['unix_socket_path']}";
    return _clamav_get_version_via_daemon($socket, NULL);
  }
}

/**
 * Get version information from a clamav executable.
 *
 * @param String $executable_path
 *
 * @return String
 */
function _clamav_get_version_via_exec($executable_path) {
  if (is_file($executable_path)) {
    return exec(escapeshellcmd($executable_path) . ' -V');
  }
  else {
    return NULL;
  }
}

/**
 * Get version information from a clamav daemon.
 *
 * @param String $host
 * @param Int    $port
 *
 * @return String
 */
function _clamav_get_version_via_daemon($host, $port) {
  $handler = @fsockopen($host, $port);
  if (!$handler) {
    return NULL;
  }
  fwrite($handler, "VERSION\n");
  $content = fgets($handler);
  fclose($handler);
  return $content;
}

Functions

Namesort descending Description
clamav_get_version Get the version information for clamav.
clamav_scan Scan a file and raise an error on the form element (if required).
clamav_scan_file Scan a single file
_clamav_get_version_via_daemon Get version information from a clamav daemon.
_clamav_get_version_via_exec Get version information from a clamav executable.
_clamav_sanitise_output Sanitise output from clamscan.
_clamav_scan_via_daemon Scan a single file using a daemon.
_clamav_scan_via_daemon_handler Scan a single file via ClamAV daemon.
_clamav_scan_via_exec Scan a single file using a clamav executable.
_clamav_scan_via_unix_socket Scan a single file by connecting to ClamAV over a unix socket.