#
# 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 $
#
##############################################################################
=head1 NAME
ClamAV::Client - A client class for the ClamAV C<clamd> virus scanner daemon
=cut
package ClamAV::Client;
=head1 VERSION
0.11
=cut
our $VERSION = '0.11';
=head1 SYNOPSIS
=head2 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();
=head2 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.
=head2 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 );
=head2 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) { ... }
=head2 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);
=cut
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:
##############################################################################
=head1 DESCRIPTION
B<ClamAV::Client> is a class acting as a client for a ClamAV C<clamd> virus
scanner daemon. The daemon may run locally or on a remote system as
B<ClamAV::Client> can use both Unix domain sockets and TCP/IP sockets. The
full functionality of the C<clamd> client/server protocol is supported.
=cut
sub new;
sub ping;
sub version;
sub reload;
sub quit;
sub scan_path;
sub scan_path_complete;
sub scan_stream;
sub scan_scalar;
# Implementation:
##############################################################################
=head2 Constructor
The following constructor is provided:
=over
=item B<new(%options)>: RETURNS ClamAV::Client
Creates a new C<ClamAV::Client> object. If I<no> socket options are specified,
first the socket options from the local C<clamd.conf> configuration file are
tried, then the Unix domain socket C</var/run/clamav/clamd.ctl> is tried, then
finally the TCP/IP socket at C<127.0.0.1> on port C<3310> is tried. If either
Unix domain or TCP/IP socket options are explicitly specified, only these are
used.
C<%options> is a list of key/value pairs representing any of the following
options:
=over
=item B<socket_name>
A scalar containing the absolute name of the local Unix domain socket.
Defaults to B<'/var/run/clamav/clamd.ctl'>.
=item B<socket_host>
A scalar containing the name or IP address of the TCP/IP socket. Defaults to
B<'127.0.0.1'>.
=item B<socket_port>
A scalar containing the port number of the TCP/IP socket. Defaults to B<3310>.
=back
=cut
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;
}
=back
=head2 Instance methods
The following instance methods are provided:
=head3 Daemon maintenance
=over
=item B<ping>: RETURNS SCALAR; THROWS ClamAV::Client::Error
Returns B<true> ('PONG') if the ClamAV daemon is alive. Throws a
ClamAV::Client::Error exception otherwise.
=cut
sub ping {
my ($self) = @_;
return $self->_simple_command("PING");
}
=item B<version>: RETURNS SCALAR; THROWS ClamAV::Client::Error
Returns the version string of the ClamAV daemon.
=cut
sub version {
my ($self) = @_;
return $self->_simple_command("VERSION");
}
=item B<reload>: RETURNS SCALAR; THROWS ClamAV::Client::Error
Instructs the ClamAV daemon to reload its malware database. Returns B<true> if
the reloading succeeds, or throws a ClamAV::Client::Error exception otherwise.
=cut
sub reload {
my ($self) = @_;
return $self->_simple_command("RELOAD");
}
=item B<quit>: RETURNS SCALAR; THROWS ClamAV::Client::Error
=item B<shutdown>: RETURNS SCALAR; THROWS ClamAV::Client::Error
Terminates the ClamAV daemon. Returns B<true> if the termination succeeds,
or throws a ClamAV::Client::Error exception otherwise.
=cut
sub quit {
# Caution, this terminates the ClamAV daemon!
my ($self) = @_;
return $self->_simple_command("QUIT");
}
*shutdown = *shutdown = \&quit;
=item B<scan_path($path)>: RETURNS SCALAR, SCALAR; THROWS ClamAV::Client::Error
=item B<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 B<ClamAV::Client::SCAN_MODE_NORMAL> (which is the default)
causes a normal scan (C<SCAN>) with archive support enabled, a mode of
B<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 B<undef>.
=cut
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);
}
=item B<scan_path_complete($path)>: RETURNS HASH; THROWS ClamAV::Client::Error
Scans a single file or a whole directory structure I<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.
=cut
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;
}
=item B<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, B<undef>
otherwise.
=cut
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;
}
=item B<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, B<undef>
otherwise.
=cut
sub scan_scalar {
my ($self, $scalar_ref, $scan_mode_raw) = @_;
open(my $handle, '<', $scalar_ref);
return $self->scan_stream($handle, $scan_mode_raw);
}
=back
=cut
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)
}
=head1 SEE ALSO
The L<clamd> and L<clamav> man-pages.
=head1 AVAILABILITY and SUPPORT
The latest version of ClamAV::Client is available on CPAN and at
L<http://www.mehnle.net/software/clamav-client>.
Support is usually (but not guaranteed to be) given by the author, Julian
Mehnle <julian@mehnle.net>.
=head1 AUTHOR and LICENSE
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.
=cut
package ClamAV::Client::Error;
use base qw(Error::Simple);
package ClamAV::Client;
TRUE;
# vim:tw=79