File Coverage

blib/lib/File/VirusScan/Engine/Daemon/ClamAV/Clamd.pm
Criterion Covered Total %
statement 40 102 39.2
branch 4 44 9.0
condition 4 20 20.0
subroutine 12 13 92.3
pod 2 2 100.0
total 62 181 34.2


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