File Coverage

blib/lib/File/VirusScan/Engine/Daemon/FPROT/V4.pm
Criterion Covered Total %
statement 35 109 32.1
branch 3 60 5.0
condition 1 11 9.0
subroutine 11 12 91.6
pod 2 2 100.0
total 52 194 26.8


line stmt bran cond sub pod time code
1             package File::VirusScan::Engine::Daemon::FPROT::V4;
2 1     1   50744 use strict;
  1         1  
  1         25  
3 1     1   3 use warnings;
  1         1  
  1         18  
4 1     1   3 use Carp;
  1         1  
  1         72  
5              
6 1     1   294 use File::VirusScan::Engine::Daemon;
  1         2  
  1         6  
7 1     1   31 use vars qw( @ISA );
  1         1  
  1         75  
8             @ISA = qw( File::VirusScan::Engine::Daemon );
9              
10 1     1   407 use IO::Socket::INET;
  1         10865  
  1         9  
11 1     1   509 use Cwd 'abs_path';
  1         2  
  1         54  
12 1     1   496 use HTML::TokeParser;
  1         6701  
  1         30  
13 1     1   280 use File::VirusScan::Result;
  1         1  
  1         911  
14              
15             sub new
16             {
17 7     7 1 9094 my ($class, $conf) = @_;
18              
19 7 100       18 if(!$conf->{host}) {
20 1         17 croak "Must supply a 'host' config value for $class";
21             }
22              
23 6   50     34 my $self = {
24             host => $conf->{host},
25             base_port => $conf->{base_port} || 10200
26             };
27              
28 6         25 return bless $self, $class;
29             }
30              
31             sub scan
32             {
33 1     1 1 744 my ($self, $path) = @_;
34              
35 1 50       27 if(abs_path($path) ne $path) {
36 1         8 return File::VirusScan::Result->error("Path $path is not absolute");
37             }
38              
39 0           my @files = eval { $self->list_files($path) };
  0            
40 0 0         if($@) {
41 0           return File::VirusScan::Result->error($@);
42             }
43              
44 0           foreach my $file_path (@files) {
45 0           my $result = $self->_scan($file_path);
46              
47 0 0         if(!$result->is_clean()) {
48 0           return $result;
49             }
50             }
51              
52             }
53              
54             # TODO FIXME
55             # This is unbelievably ugly code, but as I have no way of testing it
56             # against an F-PROT daemon, it's been ported nearly verbatim from
57             # MIMEDefang. It is in desperate need of cleanup!
58             sub _scan
59             {
60 0     0     my ($self, $item) = @_;
61              
62 0           my $host = $self->{host};
63 0           my $baseport = $self->{base_port};
64              
65             # Default error message when reaching end of function
66 0           my $errmsg = "Could not connect to F-Prot Daemon at $host:$baseport";
67              
68             # Try 5 ports in order to find an active scanner; they may
69             # change the port when they find and spawn an updated demon
70             # executable
71 0           SEARCH_DEMON: foreach my $port ($baseport .. ($baseport + 4)) {
72              
73             # TODO: Timeout value?
74             # TODO: Why aren't we using a HTTP client instead of
75             # rolling our own HTTP?
76 0           my $sock = IO::Socket::INET->new(
77             PeerAddr => $host,
78             PeerPort => $port
79             );
80              
81 0 0         next if !defined $sock;
82              
83             # The arguments (following the '?' sign in the HTTP
84             # request) are the same as for the command line F-Prot,
85             # the additional -remote-dtd suppresses the unuseful
86             # XML DTD prefix
87 0           my @args = qw( -dumb -archive -packed -remote-dtd );
88 0           my $uri = "$item?" . join('%20', @args);
89 0 0         if(!$sock->print("GET $uri HTTP/1.0\n\n")) {
90 0           my $err = $!;
91 0           $sock->close;
92 0           return File::VirusScan::Result->error("Could not write to socket: $err");
93             }
94              
95 0 0         if(!$sock->flush) {
96 0           my $err = $!;
97 0           $sock->close;
98 0           return File::VirusScan::Result->error("Could not flush socket: $err");
99             }
100              
101             # Fetch HTTP Header
102             ## Maybe dropped, if no validation checks are to be made
103 0           while (my $output = $sock->getline) {
104 0 0         if($output =~ /^\s*$/) {
    0          
    0          
105 0           last; # End of headers
106             #### Below here: Validating the protocol
107             #### If the protocol is not recognized, it's assumed that the
108             #### endpoint is not an F-Prot demon, hence,
109             #### the next port is probed.
110             } elsif($output =~ /^HTTP(.*)/) {
111 0           my $h = $1;
112 0 0         next SEARCH_DEMON unless $h =~ m!/1\.0\s+200\s!;
113             } elsif($output =~ /^Server:\s*(\S*)/) {
114 0 0         next SEARCH_DEMON if $1 !~ /^fprotd/;
115             }
116             }
117              
118             # Parsing XML results
119 0           my $xml = HTML::TokeParser->new($sock);
120 0           my $t = $xml->get_tag('fprot-results');
121 0 0         unless ($t) { # This is an essential tag --> assume a broken demon
122 0           $errmsg = 'Demon did not return tag';
123 0           last SEARCH_DEMON;
124             }
125              
126 0 0         if($t->[1]{'version'} ne '1.0') {
127 0           $errmsg = "Incompatible F-Protd results version: " . $t->[1]{'version'};
128 0           last SEARCH_DEMON;
129             }
130              
131 0           my $curText; # temporarily accumulated information
132 0           my $virii = ''; # name(s) of virus(es) found
133 0           my $code; # overall exit code
134 0           my $msg = ''; # accumulated message of virus scanner
135 0           while ($t = $xml->get_token) {
136 0           my $tag = $t->[1];
137 0 0         if($t->[0] eq 'S') { # Start tag
    0          
138             # Accumulate the information temporarily
139             # into $curText until the tag is found
140 0           my $text = $xml->get_trimmed_text;
141              
142             # $tag 'filename' of no use in MIMEDefang
143 0 0 0       if($tag eq 'name') {
    0 0        
    0          
    0          
144 0 0         $virii .= (length $virii ? " " : "") . $text;
145 0           $curText .= "Found the virus: '$text'\n";
146             } elsif($tag eq 'accuracy' || $tag eq 'disinfectable' || $tag eq 'message') {
147 0           $curText .= "\t$tag: $text\n";
148             } elsif($tag eq 'error') {
149 0           $msg .= "\nError: $text\n";
150             } elsif($tag eq 'summary') {
151 0 0         $code = $t->[2]{'code'} if defined $t->[2]{'code'};
152             }
153             } elsif($t->[0] eq 'E') { # End tag
154 0 0         if($tag eq 'detected') {
    0          
155              
156             # move the cached information to the
157             # accumulated message
158 0 0         $msg .= "\n$curText" if $curText;
159 0           undef $curText;
160             } elsif($tag eq 'fprot-results') {
161 0           last; # security check
162             }
163             }
164             }
165 0           $sock->close;
166              
167             ## Check the exit code (man f-protd)
168             ## NOTE: These codes are different from the ones of the command line version!
169             # 0 Not scanned, unable to handle the object.
170             # 1 Not scanned due to an I/O error.
171             # 2 Not scanned, as the scanner ran out of memory.
172             # 3 X The object is not of a type the scanner knows. This
173             # may either mean it was misidentified or that it is
174             # corrupted.
175             # 4 X The object was valid, but encrypted and could not
176             # be scanned.
177             # 5 Scanning of the object was interrupted.
178             # 7 X The object was identified as an "innocent" object.
179             # 9 X The object was successfully scanned and nothing was
180             # found.
181             # 11 The object is infected.
182             # 13 The object was disinfected.
183 0 0         unless (defined $code) {
184 0           $errmsg = "No summary code found";
185 0           last SEARCH_DEMON;
186             }
187              
188             # I/O error, unable to handle, out of mem,
189             # any filesystem error less than zero,
190             # interrupted
191 0 0 0       if($code < 3 || $code == 5) {
192             #w
193             ## assume this a temporary failure
194 0           $errmsg = "Scan error #$code: $msg";
195 0           last SEARCH_DEMON;
196             }
197              
198 0 0         if($code > 10) { # infected; (disinfected: Should never happen!)
199 0           my $virus_name = '';
200 0 0         if(length $virii) {
    0          
201 0           $virus_name = $virii;
202             } elsif($msg =~ /^\tmessage:\s+(\S.*)/m) {
203 0           $virus_name = $1;
204             } else {
205              
206             # no virus name found, log message returned by fprot
207 0           $virus_name = 'unknown-FPROTD-virus';
208             }
209              
210 0           return File::VirusScan::Result->virus($virus_name);
211             }
212             ###### These codes are left to be handled:
213             # 3 X The object is not of a type the scanner knows. This
214             # may either mean it was misidentified or that it is
215             # corrupted.
216             # 4 X The object was valid, but encrypted and could not
217             # be scanned.
218             # 7 X The object was identified as an "innocent" object.
219             # 9 X The object was successfully scanned and nothing was
220              
221             # 9 is trival; 7 is probably trival
222             # 4 & 3 we can't do anything really, because if the attachement
223             # is some unknown archive format, the scanner wouldn't had known
224             # this issue anyway, hence, I consider it "clean"
225              
226 0           return File::VirusScan::Result->clean();
227             } # End SEARCH_DEMON
228              
229             # Could not connect to daemon or some error occured during the
230             # communication with it
231 0           $errmsg =~ s/\s*\.*\s*\n+\s*/\. /g;
232 0           return File::VirusScan::Result->error($errmsg);
233             }
234              
235             1;
236             __END__