File Coverage

blib/lib/File/VirusScan/Engine/Daemon/ClamAV/Clamd.pm
Criterion Covered Total %
statement 33 101 32.6
branch 1 44 2.2
condition 0 17 0.0
subroutine 11 13 84.6
pod 2 2 100.0
total 47 177 26.5


line stmt bran cond sub pod time code
1             package File::VirusScan::Engine::Daemon::ClamAV::Clamd;
2 1     1   54190 use strict;
  1         2  
  1         34  
3 1     1   6 use warnings;
  1         2  
  1         29  
4 1     1   6 use Carp;
  1         1  
  1         65  
5              
6 1     1   396 use File::VirusScan::Engine::Daemon;
  1         2  
  1         10  
7 1     1   48 use vars qw( @ISA );
  1         1  
  1         46  
8             @ISA = qw( File::VirusScan::Engine::Daemon );
9              
10 1     1   548 use IO::Socket::UNIX;
  1         12831  
  1         6  
11 1     1   1286 use IO::Select;
  1         1741  
  1         76  
12 1     1   10 use Scalar::Util 'blessed';
  1         1  
  1         63  
13 1     1   6 use Cwd 'abs_path';
  1         1  
  1         47  
14              
15 1     1   551 use File::VirusScan::Result;
  1         2  
  1         1028  
16              
17             sub new
18             {
19 8     8 1 16651 my ($class, $conf) = @_;
20              
21 8 50       27 if(!$conf->{socket_name}) {
22 8         155 croak "Must supply a 'socket_name' config value for $class";
23             }
24              
25 0 0         if(exists $conf->{zip_fallback}) {
26 0 0 0       unless (blessed($conf->{zip_fallback}) && $conf->{zip_fallback}->isa('File::VirusScan::Engine::Daemon')) {
27 0           croak q{The 'zip_fallback' config value must be an object inheriting from File::VirusScan::Engine::Daemon};
28             }
29             }
30              
31 0   0       my $self = {
      0        
      0        
      0        
32             socket_name => $conf->{socket_name},
33             ping_timeout => $conf->{ping_timeout} || 5,
34             read_timeout => $conf->{read_timeout} || 60,
35             write_timeout => $conf->{write_timeout} || 30,
36             zip_fallback => $conf->{zip_fallback} || undef,
37             };
38              
39 0           return bless $self, $class;
40             }
41              
42             sub _get_socket
43             {
44 0     0     my ($self) = @_;
45              
46 0           my $sock = IO::Socket::UNIX->new(Peer => $self->{socket_name});
47 0 0         if(!defined $sock) {
48 0           croak "Error: Could not connect to clamd daemon at $self->{socket_name}";
49             }
50              
51 0           return $sock;
52             }
53              
54             sub scan
55             {
56 0     0 1   my ($self, $path) = @_;
57              
58 0 0         if(abs_path($path) ne $path) {
59 0           return File::VirusScan::Result->error("Path $path is not absolute");
60             }
61              
62 0           my $sock = eval { $self->_get_socket };
  0            
63 0 0         if($@) {
64 0           return File::VirusScan::Result->error($@);
65             }
66              
67 0           my $s = IO::Select->new($sock);
68              
69 0 0         if(!$s->can_write($self->{ping_timeout})) {
70 0           $sock->close;
71 0           return File::VirusScan::result->error("Timeout waiting to write PING to clamd daemon at $self->{socket_name}");
72             }
73              
74 0 0         if(!$sock->print("nIDSESSION\nnPING\n")) {
75 0           $sock->close;
76 0           return File::VirusScan::Result->error('Could not ping clamd');
77             }
78              
79 0 0         if(!$sock->flush) {
80 0           $sock->close;
81 0           return File::VirusScan::Result->error('Could not flush clamd socket');
82             }
83              
84 0 0         if(!$s->can_read($self->{ping_timeout})) {
85 0           $sock->close;
86 0           return File::VirusScan::Result->error("Timeout reading from clamd daemon at $self->{socket_name}");
87             }
88              
89 0           my $ping_response;
90 0 0         if(!$sock->sysread($ping_response, 256)) {
91 0           $sock->close;
92 0           return File::VirusScan::Result->error('Did not get ping response from clamd');
93             }
94              
95 0 0 0       if(!defined $ping_response || $ping_response ne "1: PONG\n") {
96 0           $sock->close;
97 0           return File::VirusScan::Result->error('Did not get ping response from clamd');
98             }
99              
100 0 0         if(!$s->can_write($self->{write_timeout})) {
101 0           $sock->close;
102 0           return File::VirusScan::result->error("Timeout waiting to write SCAN to clamd daemon at $self->{socket_name}");
103             }
104              
105 0 0         if(!$sock->print("nSCAN $path\n")) {
106 0           $sock->close;
107 0           return File::VirusScan::Result->error("Could not get clamd to scan $path");
108             }
109              
110 0 0         if(!$sock->flush) {
111 0           $sock->close;
112 0           return File::VirusScan::Result->error("Could not get clamd to scan $path");
113             }
114              
115 0 0         if(!$s->can_read($self->{read_timeout})) {
116 0           $sock->close;
117 0           return File::VirusScan::Result->error("Timeout reading from clamd daemon at $self->{socket_name}");
118             }
119              
120             # Discard our IO::Select object.
121 0           undef $s;
122              
123 0           my $scan_response;
124              
125 0 0         if(!$sock->sysread($scan_response, 256)) {
126 0           $sock->close;
127 0           return File::VirusScan::Result->error("Did not get response from clamd while scanning $path");
128             }
129              
130             # End session
131 0           my $rc = $sock->print("nEND\n");
132 0           $sock->close();
133 0 0         if(!$rc) {
134 0           return File::VirusScan::Result->error("Could not get clamd to scan $path");
135             }
136              
137 0           my ($id, $file, $status) = $scan_response =~ m/(\d+):\s+([^:]+):\s+(.*)/;
138 0 0         if( ! $id ) {
139 0           return File::VirusScan::Result->error("IDSESSION response didn't contain an ID");
140             }
141             # TODO: what if more than one virus found?
142             # TODO: can/should we capture infected filenames?
143 0 0         if($status =~ m/(.+) FOUND/) {
    0          
144 0           return File::VirusScan::Result->virus($1);
145             } elsif($scan_response =~ m/(.+) ERROR/) {
146 0           my $err_detail = $1;
147              
148             # The clam daemon may not understand certain zip files,
149             # and cannot use an external decompression tool. The
150             # standalone 'clamscan' utility can, though. So, we
151             # allow another engine to be configured as a fallback.
152             # It's usually File::VirusScan::ClamAV::ClamScan, but
153             # doesn't have to be.
154 0 0 0       if( $self->{zip_fallback}
155             && $err_detail =~ /(?:zip module failure|not supported data format)/i)
156             {
157 0           return $self->{zip_fallback}->scan($path);
158             }
159 0           return File::VirusScan::Result->error("Clamd returned error: $err_detail");
160             }
161              
162 0           return File::VirusScan::Result->clean();
163             }
164              
165             1;
166             __END__