ClamAV::Client - A client class for the ClamAV C<clamd> virus scanner daemon


ClamAV-Client documentation Contained in the ClamAV-Client distribution.

Index


Code Index:

NAME

Top

ClamAV::Client - A client class for the ClamAV clamd virus scanner daemon

VERSION

Top

0.11

SYNOPSIS

Top

Creating a scanner client

    use ClamAV::Client;

    # Try using socket options from clamd.conf, or use default socket:
    my $scanner = ClamAV::Client->new();

    # Use a local Unix domain socket:
    my $scanner = ClamAV::Client->new(
        socket_name     => '/var/run/clamav/clamd.ctl'
    );

    # Use a TCP socket:
    my $scanner = ClamAV::Client->new(
        socket_host     => '127.0.0.1',
        socket_port     => 3310
    );

    die("ClamAV daemon not alive")
        if not defined($scanner) or not $scanner->ping();

Daemon maintenance

    my $version = $scanner->version;
                            # Retrieve the ClamAV version string.

    $scanner->reload();     # Reload the malware pattern database.

    $scanner->quit();       # Terminates the ClamAV daemon.
    $scanner->shutdown();   # Likewise.

Path scanning (lazy)

    # Scan a single file or a whole directory structure,
    # and stop at the first infected file:
    my ($path, $result) = $scanner->scan_path($path);
    my ($path, $result) = $scanner->scan_path(
        $path, ClamAV::Client::SCAN_MODE_NORMAL );
    my ($path, $result) = $scanner->scan_path(
        $path, ClamAV::Client::SCAN_MODE_RAW );

Path scanning (complete)

    # Scan a single file or a whole directory structure,
    # and scan all files without stopping at the first infected one:
    my %results = $scanner->scan_path_complete($path);
    while (my ($path, $result) = each %results) { ... }

Other scanning methods

    # Scan a stream, i.e. read from an I/O handle:
    my $result = $scanner->scan_stream($handle);

    # Scan a scalar value:
    my $result = $scanner->scan_scalar(\$value);

DESCRIPTION

Top

ClamAV::Client is a class acting as a client for a ClamAV clamd virus scanner daemon. The daemon may run locally or on a remote system as ClamAV::Client can use both Unix domain sockets and TCP/IP sockets. The full functionality of the clamd client/server protocol is supported.

Constructor

The following constructor is provided:

new(%options): RETURNS ClamAV::Client

Creates a new ClamAV::Client object. If no socket options are specified, first the socket options from the local clamd.conf configuration file are tried, then the Unix domain socket /var/run/clamav/clamd.ctl is tried, then finally the TCP/IP socket at 127.0.0.1 on port 3310 is tried. If either Unix domain or TCP/IP socket options are explicitly specified, only these are used.

%options is a list of key/value pairs representing any of the following options:

socket_name

A scalar containing the absolute name of the local Unix domain socket. Defaults to '/var/run/clamav/clamd.ctl'.

socket_host

A scalar containing the name or IP address of the TCP/IP socket. Defaults to '127.0.0.1'.

socket_port

A scalar containing the port number of the TCP/IP socket. Defaults to 3310.

Instance methods

The following instance methods are provided:

Daemon maintenance

ping: RETURNS SCALAR; THROWS ClamAV::Client::Error

Returns true ('PONG') if the ClamAV daemon is alive. Throws a ClamAV::Client::Error exception otherwise.

version: RETURNS SCALAR; THROWS ClamAV::Client::Error

Returns the version string of the ClamAV daemon.

reload: RETURNS SCALAR; THROWS ClamAV::Client::Error

Instructs the ClamAV daemon to reload its malware database. Returns true if the reloading succeeds, or throws a ClamAV::Client::Error exception otherwise.

quit: RETURNS SCALAR; THROWS ClamAV::Client::Error
shutdown: RETURNS SCALAR; THROWS ClamAV::Client::Error

Terminates the ClamAV daemon. Returns true if the termination succeeds, or throws a ClamAV::Client::Error exception otherwise.

scan_path($path): RETURNS SCALAR, SCALAR; THROWS ClamAV::Client::Error
scan_path($path, $scan_mode): RETURNS SCALAR, SCALAR; THROWS ClamAV::Client::Error

Scans a single file or a whole directory structure, and stops at the first infected file found. The specified path must be absolute. A scan mode may be specified: a mode of ClamAV::Client::SCAN_MODE_NORMAL (which is the default) causes a normal scan (SCAN) with archive support enabled, a mode of ClamAV::Client::SCAN_MODE_RAW causes a raw scan with archive support disabled.

If an infected file is found, returns a list consisting of the path of the file and the name of the malware signature that matched the file. Otherwise, returns the originally specified path and undef.

scan_path_complete($path): RETURNS HASH; THROWS ClamAV::Client::Error

Scans a single file or a whole directory structure completely, not stopping at the first infected file found. The specified path must be absolute. Only the normal, non-raw mode is supported for complete scans by ClamAV.

Returns a hash with a list of infected files found, with the file paths as the keys and the matched malware signature names as the values.

scan_stream($handle): RETURNS SCALAR; THROWS ClamAV::Client::Error

Scans a stream, that is, reads from an I/O handle. If the stream is found to be infected, returns the name of the matching malware signature, undef otherwise.

scan_scalar(\$value): RETURNS SCALAR; THROWS ClamAV::Client::Error

Scans the value referenced by the given scalarref. If the value is found to be infected, returns the name of the matching malware signature, undef otherwise.

SEE ALSO

Top

The clamd and clamav man-pages.

AVAILABILITY and SUPPORT

Top

The latest version of ClamAV::Client is available on CPAN and at http://www.mehnle.net/software/clamav-client.

Support is usually (but not guaranteed to be) given by the author, Julian Mehnle <julian@mehnle.net>.

AUTHOR and LICENSE

Top

ClamAV::Client is Copyright (C) 2004-2005 Julian Mehnle <julian@mehnle.net>.

ClamAV::Client is free software. You may use, modify, and distribute it under the same terms as Perl itself, i.e. under the GNU GPL or the Artistic License.


ClamAV-Client documentation Contained in the ClamAV-Client distribution.
#
# ClamAV::Client class,
# a client class for the ClamAV clamd virus scanner daemon.
#
# (C) 2004-2005 Julian Mehnle <julian@mehnle.net>
# $Id: Client.pm,v 1.6 2005/01/21 22:50:14 julian Exp $
#
##############################################################################

package ClamAV::Client;

our $VERSION = '0.11';

use warnings;
use strict;

use Error qw(:try);

use Carp;
use IO::Socket;

use ClamAV::Config;

use constant TRUE   => (0 == 0);
use constant FALSE  => not TRUE;

use constant SOCKET_TYPE_AUTO       => 0;
use constant SOCKET_TYPE_UNIX       => 1;
use constant SOCKET_TYPE_TCP        => 2;

use constant DEFAULT_SOCKET_NAME    => '/var/run/clamav/clamd.ctl';
use constant DEFAULT_SOCKET_HOST    => '127.0.0.1';
use constant DEFAULT_SOCKET_PORT    => 3310;

use constant SCAN_MODE_NORMAL       => FALSE;
use constant SCAN_MODE_RAW          => TRUE;

use constant STREAM_BLOCK_SIZE      => 4096;

# Interface:
##############################################################################

sub new;

sub ping;
sub version;
sub reload;
sub quit;

sub scan_path;
sub scan_path_complete;
sub scan_stream;
sub scan_scalar;

# Implementation:
##############################################################################

sub new {
    my ($class, %options) = @_;
    
    if ($options{socket_name}) {
        # Caller explicitly specified local Unix domain socket.
        $options{socket_type} = SOCKET_TYPE_UNIX;
        $options{socket_host} ||= DEFAULT_SOCKET_HOST;
    }
    elsif ($options{socket_host} or $options{socket_port}) {
        # Caller explicitly specified TCP socket.
        $options{socket_type} = SOCKET_TYPE_TCP;
        $options{socket_host} ||= DEFAULT_SOCKET_HOST;
        $options{socket_port} ||= DEFAULT_SOCKET_PORT;
    }
    else {
        # Caller hasn't specified anything.
        
        # Try reading local clamd config file:
        try {
            ClamAV::Config->clamd_config;
        }
        catch ClamAV::Config::Error with {
            # Ignore access problems to clamd configuration file.
        };
        
        # Try local Unix domain socket first...:
        $options{socket_name} = ClamAV::Config->clamd_option('LocalSocket')
            or
        # ...otherwise try TCP socket:
        $options{socket_host} = ClamAV::Config->clamd_option('TCPAddr'),
        $options{socket_port} = ClamAV::Config->clamd_option('TCPSocket');
        
        if ($options{socket_name}) {
            # Local clamd config file has specified local Unix domain socket.
            $options{socket_type} = SOCKET_TYPE_UNIX;
            $options{socket_host} ||= DEFAULT_SOCKET_HOST;
        }
        elsif ($options{socket_host} or $options{socket_port}) {
            # Local clamd config file has speficied TCP socket.
            $options{socket_type} = SOCKET_TYPE_TCP;
            $options{socket_host} ||= DEFAULT_SOCKET_HOST;
            $options{socket_port} ||= DEFAULT_SOCKET_PORT;
        }
        else {
            # Neither caller nor clamd config file have specified anything, set
            # socket auto detection mode.
            $options{socket_type} = SOCKET_TYPE_AUTO;
            $options{socket_host} = DEFAULT_SOCKET_HOST;
            $options{socket_name} = DEFAULT_SOCKET_NAME;
            $options{socket_port} = DEFAULT_SOCKET_PORT;
        }
    }
    
    my $self = {
        socket_type     => $options{socket_type},
        socket_name     => $options{socket_name},
        socket_host     => $options{socket_host},
        socket_port     => $options{socket_port}
    };
    bless($self, $class);
    return $self;
}

sub ping {
    my ($self) = @_;
    return $self->_simple_command("PING");
}

sub version {
    my ($self) = @_;
    return $self->_simple_command("VERSION");
}

sub reload {
    my ($self) = @_;
    return $self->_simple_command("RELOAD");
}

sub quit {
    # Caution, this terminates the ClamAV daemon!
    my ($self) = @_;
    return $self->_simple_command("QUIT");
}

*shutdown = *shutdown = \&quit;

sub scan_path {
    my ($self, $path, $scan_mode_raw) = @_;
    
    my $command = ($scan_mode_raw ? 'RAWSCAN' : 'SCAN');
    my $response = $self->_simple_command("$command $path");
    return $self->_parse_scan_response($response);
}

sub scan_path_complete {
    my ($self, $path, $scan_mode_raw) = @_;

    if ($scan_mode_raw) {
        throw ClamAV::Client::Error("Raw mode not supported for path complete (CONTSCAN) scanning");
    }
    
    my $socket = $self->_socket;
    $socket->print("CONTSCAN $path\n");
    
    my %results;
    while (my $response = $socket->getline()) {
        my ($file_name, $result) = $self->_parse_scan_response($response);
        $results{$file_name} = $result;
    }
    
    $socket->close();
    
    %results = ()
        if values(%results) == 1 and not defined((values(%results))[0]);
    
    return %results;
}

sub scan_stream {
    my ($self, $handle, $scan_mode_raw) = @_;
    
    if ($scan_mode_raw) {
        throw ClamAV::Client::Error("Raw mode not supported for stream (STREAM) scanning");
    }
    
    my $socket = $self->_socket;
    $socket->print("STREAM\n");
    my $port_spec = $socket->getline();
    if (not $port_spec =~ /^PORT (\d+)$/i) {
        throw ClamAV::Client::Error("Invalid server response to STREAM command: \"$port_spec\"");
    }
    
    my $port = $1;
    
    require IO::Socket::INET;
    my $stream_socket = IO::Socket::INET->new(
        Proto       => 'tcp',
        PeerHost    => $self->{socket_host},
        PeerPort    => $port
    );
    
    # If we didn't manage to gain a connection, throw exception:
    if (not defined($stream_socket)) {
        throw ClamAV::Client::Error(
            "Could not establish TCP socket connection on port $port for STREAM scan"
        );
    }
    
    $stream_socket->autoflush(TRUE);
    my $block;
    $stream_socket->print($block)
        while $handle->read($block, STREAM_BLOCK_SIZE);
    $stream_socket->close();
    
    my $response = $self->{'socket'}->getline();

    $socket->close();
    
    my (undef, $result) = $self->_parse_scan_response($response);
    return $result;
}

sub scan_scalar {
    my ($self, $scalar_ref, $scan_mode_raw) = @_;
    
    open(my $handle, '<', $scalar_ref);
    return $self->scan_stream($handle, $scan_mode_raw);
}

sub _socket {
    my ($self) = @_;
    
    # Try to reuse cached socket connection:
    my $socket = $self->{'socket'};
    
    while (not defined($socket) or not $socket->opened) {
        # (Re-)establish socket connection.
        
        # Try to connect through Unix domain socket:
        if (
            $self->{socket_type} == SOCKET_TYPE_UNIX or
            $self->{socket_type} == SOCKET_TYPE_AUTO
        ) {
            require IO::Socket::UNIX;
            $socket = IO::Socket::UNIX->new(
                Peer    => $self->{socket_name}
            );
            last if defined($socket);
        }
        
        # Try to connect through TCP socket:
        if (
            $self->{socket_type} == SOCKET_TYPE_TCP  or
            $self->{socket_type} == SOCKET_TYPE_AUTO
        ) {
            require IO::Socket::INET;
            $socket = IO::Socket::INET->new(
                Proto       => 'tcp',
                PeerHost    => $self->{socket_host},
                PeerPort    => $self->{socket_port}
            );
            last if defined($socket);
        }
        
        # We haven't managed to gain a connection, throw exception:
        throw ClamAV::Client::Error(
            "Could not establish socket connection, tried UNIX domain and TCP sockets"
        );
    }
    
    $socket->autoflush(TRUE);
    return $self->{'socket'} = $socket;
}

sub _simple_command {
    my ($self, $command) = @_;
    
    my $socket = $self->_socket;
    $socket->print("$command\n");
    chomp(my $response = $socket->getline());
    $socket->close();
    return $response;
}

sub _parse_scan_response {
    my ($self, $response) = @_;
    chomp($response);
    if (not $response =~ /^(.*): (?:OK|(.*) FOUND)$/i) {
        throw ClamAV::Client::Error("Invalid server response to scan command: \"$response\"");
    }
    return ($1, $2);  # (<file-name>, <virus-name> | undef)
}

package ClamAV::Client::Error;
use base qw(Error::Simple);

package ClamAV::Client;

TRUE;

# vim:tw=79