use strict; package Net::ClamAV::Client; # ABSTRACT: A client class for the ClamAV C virus scanner daemon use warnings; use Moose; use IO::Socket; use IO::Handle; use IO::File; use Net::ClamAV::Exception::Connect; use Net::ClamAV::Exception::Command; use Net::ClamAV::Exception::Result; use Net::ClamAV::Exception::Other; use Net::ClamAV::Exception::Unsupported; =head1 SYNOPSIS =head2 Creating a scanner client use Net::ClamAV::Client; # Use a TCP inet domain socket my $scanner = Net::ClamAV::Client->new(url => "localhost:3310"); # Use a local Unix domain socket: $scanner = Net::ClamAV::Client->new(url => "/var/run/clamav/clamd.ctl"); die("ClamAV daemon not alive") if not defined($scanner) or not $scanner->ping(); =head2 Daemon maintenance my $scanner = Net::ClamAV::Client->new(url => "localhost:3310"); 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 # Scan a single file or a whole directory structure, # and stop at the first infected file. For this to work # the clamd has to run on the local host: my $scanner = Net::ClamAV::Client->new(url => "localhost:3310"); my @results = $scanner->scanLocalPath("/etc/groups"); =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 $scanner = Net::ClamAV::Client->new(url => "localhost:3310"); my @results2 = $scanner->scanLocalPathContinous("/etc/"); =head2 Other scanning methods my $handle; my $scanner = Net::ClamAV::Client->new(url => "localhost:3310"); # Scan a stream, i.e. read from an I/O handle: my $result = $scanner->scanStream($handle); # Scan a scalar value: my $value; # some file in a scalar my $result2 = $scanner->scanScalar(\$value); =head1 DESCRIPTION B is a class acting as a client for a ClamAV C virus scanner daemon. The daemon may run locally or on a remote system as B can use both Unix domain sockets and TCP/IP sockets. The full functionality of the C client/server protocol is supported. This Module is based on the B class written by Julian Mehnle which is not developed anymore but everything has been written from scratch. =head1 Methods =head2 Constructor The following constructor is provided: =over =item B: RETURNS Net::ClamAV::Client Creates a new C object. C<%options> is a list of key/value pairs representing any of the following options: =over =item B A scalar containing the url to the clamd server (e.g. localhost:3310 or /var/run/clamav/clamd.ctl) =back =back =cut has 'socket' => (is => 'rw'); has 'url' => (is => 'rw', required => 1); has 'runningIDsession' => (is => 'rw', isa=>'Int', default=>0); has 'streamBlockSize' => (is => 'rw', isa=>'Int', default=>4096); sub _connect { my $self = shift; if ($self->url() =~/:/) { my @url = split /\:/, $self->url(); $self->socket(IO::Socket->new( Domain => IO::Socket::AF_INET, Type => SOCK_STREAM, Proto => "tcp", PeerHost => $url[0], PeerPort => $url[1] )) || throw Net::ClamAV::Exception::Connect("Can't open socket: $@"); } else { $self->socket(IO::Socket->new( Domain => IO::Socket::AF_UNIX, Type => SOCK_STREAM, Peer => $self->url(), )) || throw Net::ClamAV::Exception::Connect("Can't open socket: $@"); } $self->socket()->autoflush(1); } sub _basicCommand { my $self = shift; my $command = shift; $self->_connect() unless $self->runningIDsession(); $self->socket()->send($command); my $reply = $self->socket()->getline(); chomp($reply); if (length($reply) < 3) { throw Net::ClamAV::Exception::Command("unknown reply to command \"$reply\""); } $self->socket()->close() unless $self->runningIDsession(); return $reply; } sub _multiCommand { my $self = shift; my $command = shift; my @reply; $self->_connect() unless $self->runningIDsession(); $self->socket()->send($command); while(defined(my $line = $self->socket()->getline())) { chomp($line); if (length($line) < 3) { throw Net::ClamAV::Exception::Command("unknown reply to command \"$line\""); } push(@reply,$line); } $self->socket()->close() unless $self->runningIDsession(); return @reply; } sub _parse_multi_result { my ($self,@reply) = @_; my @status; for my $r (@reply) { if ( $r !~ /^(.*):\s+(OK|(\S+)\s+FOUND)$/ ) { throw Net::ClamAV::Exception::Result("Invalid server scanning result \"$r\""); } my $file = $1; my $res = $2; if ($res !~ /OK/) { $res = $3; } my $stat = { file => $file, result => $res }; push (@status,$stat); } return @status; } sub _parse_result { my $self = shift; my $result = shift; if ( $result !~ /^(.*):\s+(OK|(\S+)\s+FOUND)$/ ) { throw Net::ClamAV::Exception::Result("Invalid server scanning result \"$result\""); } my $file = $1; my $res = $2; if ($res !~ /OK/) { $res = $3; } return ($file, $res); } =head2 Public Instance Methods The following public methods are provided: =head3 B : RETURNS SCALAR Returns B ('PONG') if the ClamAV daemon is alive. Throws a Net::ClamAV::Exception otherwise. =cut sub ping { my $self = shift; my $reply = $self->_basicCommand("PING"); throw Net::ClamAV::Exception::Result("No PONG reply") unless $reply eq "PONG"; return $reply; } =head3 B : RETURNS SCALAR Returns the Version String of the clamd server. Throws a Net::ClamAV::Exception otherwise. =cut sub version { my $self = shift; return $self->_basicCommand("VERSION"); } =head3 B : RETURNS SCALAR Reloads the clamd virus databases and returns B ('RELOADING') when successfull. Throws a Net::ClamAV::Exception otherwise. =cut sub reload { my $self = shift; return $self->_basicCommand("RELOAD"); } =head3 B : RETURNS SCALAR Shutdowns the clamd server. Throws a Net::ClamAV::Exception when unseccessfull. =cut sub shutdown { my $self = shift; return $self->_basicCommand("SHUTDOWN"); } =head3 B : RETURNS SCALAR śame as B =cut sub quit { my $self = shift; return $self->shutdown(); } =head3 B : RETURNS HASH Scan a file or directory given as path. B The used clamd has to run on the local host for this method to work. Clamd will directly access the given path. Make sure the user running clamd has access rights to it. Scanning stops when the first virus is found or all files within path has been scanned. The Method returns a Hash with attributes B and B. my $hash = { file => "the filename a virus was found in", result => "the result of file" }; Throws a Net::ClamAV::Exception on error. =cut sub scanLocalPath { my $self = shift; my $file = shift; throw Net::ClamAV::Exception::Other("file \"$file\" not found") unless ( -e $file ); my @reply = $self->_multiCommand("nSCAN $file\n"); my @result= $self->_parse_multi_result(@reply); return $result[0]; } =head3 B : RETURNS HASH Scan a file or directory given as path and do B stop on first virus found. B The used clamd has to run on the local host for this method to work. Clamd will directly access the given path. Make sure the user running clamd has access rights to it. Scanning stops when the first virus is found or all files within path has been scanned. The Method returns an array of hashes with attributes B and B. my $hash = { file => "the filename a virus was found in", result => "the result of file" }; Throws a Net::ClamAV::Exception on error. =cut sub scanLocalPathContinous { my $self = shift; my $file = shift; throw Net::ClamAV::Exception::Other("file \"$file\" not found") unless ( -e $file ); my @reply = $self->_multiCommand("nCONTSCAN $file\n"); return $self->_parse_multi_result(@reply); } =head3 B : RETURNS HASH Scan a file or directory given as path concurrently. B The used clamd has to run on the local host for this method to work. Clamd will directly access the given path. Make sure the user running clamd has access rights to it. Scanning stops when the first virus is found or all files within path has been scanned. The Method returns an array of hashes with attributes B and B. my $hash = { file => "the filename a virus was found in", result => "the result of file" }; Throws a Net::ClamAV::Exception on error. =cut sub scanLocalPathMulti { my $self = shift; my $file = shift; throw Net::ClamAV::Exception::Other("file \"$file\" not found") unless ( -e $file ); my @reply = $self->_multiCommand("nMULTISCAN $file\n"); return $self->_parse_multi_result(@reply); } =head3 B : RETURNS HASH Scan B file. B The used clamd has to run on the local host for this method to work. Clamd will directly access the given path. Make sure the user running clamd has access rights to it. Scanning stops when the first virus is found or all files within path has been scanned. The Method returns a hashe with attributes B and B. s my $hash = { file => "the filename a virus was found in", result => "the result of file" }; Throws a Net::ClamAV::Exception on error. =cut sub scanLocalFile { my $self = shift; my $file = shift; return $self->scanLocalPath($file); } =head3 B : RETURNS HASH Return the stats of the clamd. B Throws a Net::ClamAV::Exception on error. =cut sub stats { throw Net::ClamAV::Exception::Unsupported("stats command not supported"); } =head3 B : RETURNS HASH Scans a file given by a file descriptor. B Throws a Net::ClamAV::Exception on error. =cut sub scanFileDescriptor { throw Net::ClamAV::Exception::Unsupported("FILDES command not supported"); } =head3 B Starts a session with the clamd server within multiple scan commands can be issued. Throws a Net::ClamAV::Exception on error. =cut sub startSession { my $self = shift; $self->_connect(); $self->socket()->send("nIDSESSION\n"); $self->runningIDsession(1); } =head3 B : RETURNS SCALAR Checks if a session is running with the clamd server. Returns 1 if yes, else 0. =cut sub runningSession { my $self = shift; return $self->runningIDsession(); } =head3 B Ends a session with the clamd server within multiple scan commands can be issued. Throws a Net::ClamAV::Exception on error. =cut sub endSession { my $self = shift; $self->socket()->send("nEND\n"); $self->socket()->close(); $self->runningIDsession(0); } =head3 B : RETURNS SCALAR Scans a file by transmitting it as a stream to the clamd server. The file is given as a IO::Handle. The Method returns a SCALAR with attributes B or B. Throws a Net::ClamAV::Exception on error. =cut sub scanStreamFH { my $self = shift; my $handle = shift; throw Net::ClamAV::Exception::Other("no file handle given") unless $handle; throw Net::ClamAV::Exception::Other("handle is not a IO::Handle") unless ref($handle) eq "IO::Handle"; $self->_connect() unless $self->runningIDsession(); $self->socket()->send("nINSTREAM\n"); my $block; while (my $nr=$handle->read($block, $self->streamBlockSize())) { my $size = pack("N",$nr); $self->socket()->send($size); $self->socket()->send($block); } my $size = pack("N",0); $self->socket()->send($size); my $status=$self->socket()->getline() . "\n"; if ($status!~/^stream:\s*(.*)\s+FOUND/) { return; } my $ret=$1; $self->socket()->close() unless $self->runningIDsession(); return $ret; } =head3 B : RETURNS SCALAR Scans a file by transmitting it as a stream to the clamd server. The file is given as a path. The Method returns a SCALAR with attributes B or B. Throws a Net::ClamAV::Exception on error. =cut sub scanStreamFile { my $self = shift; my $file = shift; my $fh = IO::File->new($file, "r"); my $handle = IO::Handle->new_from_fd($fh, "r"); my $status = $self->scanStreamFH($handle); $handle->close(); return $status; } =head3 B : RETURNS SCALAR Scans a SCALAR by transmitting it as a stream to the clamd server. The file is given as a path. The Method returns a SCALAR with attributes B or B. Throws a Net::ClamAV::Exception on error. =cut sub scanScalar { my $self = shift; my $data = shift; my $fh = IO::File->new($data, "r"); my $handle = IO::Handle->new_from_fd($fh, "r"); return $self->scanStreamFH($handle); } 1;