File Coverage

blib/lib/Apache/ProxyScan.pm
Criterion Covered Total %
statement 16 18 88.8
branch n/a
condition n/a
subroutine 6 6 100.0
pod n/a
total 22 24 91.6


line stmt bran cond sub pod time code
1              
2             package Apache::ProxyScan;
3              
4 1     1   739 use strict;
  1         2  
  1         35  
5 1     1   6 use vars qw($VERSION);
  1         2  
  1         848  
6              
7 1     1   1384 use LWP::UserAgent ();
  1         75953  
  1         28  
8 1     1   974 use URI::URL;
  1         5120  
  1         57  
9 1     1   1474 use File::MMagic;
  1         40533  
  1         37  
10              
11 1     1   1511 use Apache::Const qw(OK DECLINED :log);
  0            
  0            
12             use APR::Const qw(:error SUCCESS);
13             use APR::Table;
14             use Apache::RequestRec;
15             use Apache::RequestUtil;
16             use Apache::RequestIO;
17             use Apache::Log;
18             use Apache::Response ();
19              
20             $VERSION = "0.92";
21             # create a mime type detector once.
22             # You need File::Magic even if you don't use it
23             my $MIME = File::MMagic::new('/etc/magic');
24              
25             sub handler {
26             my($r) = @_;
27             return DECLINED unless $r->proxyreq;
28             return DECLINED if ($r->method eq "CONNECT");
29              
30             # If there are Trusted Extensions DECLINE the requests here
31             my $filetype = $r->dir_config("ProxyScanTrustedExtension");
32             if (defined $filetype) {
33             my %extension;
34             foreach (split(/\s+/, $filetype)) {
35             s/^\.//igs;
36             $extension{lc("$_")} = 1;
37             }
38             my @pc = (URI::URL->new($r->uri))->path_components;
39             my $ext = pop @pc;
40             if ($ext =~ s/^.*\.([^.]+)/$1/igs) {
41             if (defined $extension{lc("$ext")}) {
42             $r->log->warn($r, "Trusted File Extension: ".$r->uri);
43             return DECLINED;
44             }
45             }
46             }
47             $r->handler("perl-script"); #ok, let's do it
48             $r->push_handlers(PerlHandler => \&proxy_handler);
49             return OK;
50             }
51              
52             sub proxy_handler {
53             my($r) = @_;
54             # get the configuration variables
55             my $scanner = $r->dir_config("ProxyScanScanner");
56             my $tmpdir = $r->dir_config("ProxyScanTempDir") || '/tmp/';
57             my $presendsize = $r->dir_config("ProxyScanPredeliverSize") || 102400;
58             my $trustmime = $r->dir_config("ProxyScanTrustedMIME");
59             if (defined $trustmime) {
60             $trustmime =~ s/\*/.*/igs;
61             $trustmime = join('|', split(/\s+/, $trustmime));
62             }
63              
64             # create the request
65             my $request = new HTTP::Request $r->method, $r->uri;
66            
67             # copy request headers
68             my $table = $r->headers_in;
69             foreach my $key (keys %{$table}) {
70             $request->header($key,$table->{$key});
71             }
72            
73             # transfer request if it's POST
74             # try to handle without content length
75             if ($r->method eq 'POST') {
76             my $len = $r->headers_in->{'Content-length'};
77             if (defined $len) {
78             my $buf;
79             $r->read($buf, $len);
80             $request->content($buf);
81             } else {
82             $request->content(scalar $r->content);
83             }
84             }
85            
86             # do a predeliver
87             # if you do predelivering there are several problems with the
88             # http protocol. For this reason we do it only for large files.
89             # This makes downloading easier, because the save-as window still
90             # appears.
91             my $callcount = 0;
92             my $delivered = 0;
93             my $headersent = 0;
94             my $trustworthy = 0;
95             my $file;
96             my $outfile = undef;
97            
98             my $fetchref = sub {
99             my($data, $res, $protocol) = @_;
100             if ($callcount == 0) {
101             my $mime = $MIME->checktype_contents($data);
102             if ((defined $trustmime ) && ($mime =~ m§^($trustmime)$§i)) {
103             $trustworthy = 1;
104             $r->log->warn($r, "Trusted MIME Type: ".$r->uri);
105             prepareheaders(\$r,\$res);
106             $r->rflush();
107             } else {
108             # make a nice filename
109             my @chars = ( "A" .. "Z", "a" .. "z", 0 .. 9 );
110             $file = substr($r->uri , 0, 200);
111             $file =~ s/[^A-Z0-9]+/_/igs;
112             $file .= join("", @chars[ map { rand @chars } ( 1 .. 16 ) ] );
113             open($outfile, ">$tmpdir/$file");
114             my $len = $res->header('Content-Length');
115             if ($len > $presendsize) {
116             $r->log->warn($r,"started predelivery on: ".$r->uri);
117             $res->remove_header('Content-Length');
118             prepareheaders(\$r,\$res);
119             $r->rflush();
120             $headersent=1;
121             print substr $data,0,5;
122             $delivered += 5;
123             $r->rflush;
124             }
125             }
126             }
127             $callcount++;
128             if ($trustworthy) {
129             print $data;
130             } else {
131             print $outfile $data;
132             }
133             return;
134             };
135            
136             # download request in unique directory
137            
138             my $res = (LWP::UserAgent->new(parse_head => 0))->simple_request($request,$fetchref,4096);
139             if (defined $outfile) {
140             close($outfile);
141             }
142            
143             # if an error occurs, res->content contains server error
144             # we are paraniod so we scan the server message too
145             # DNS Errors are reported by LWP::UA as Code 500 with empty content
146             if (!$res->is_success) {
147             open(my $fh, ">$tmpdir/$file");
148             my $msg = $res->content;
149             if (($res->code == 500) && ($msg eq "")) {
150             $msg = $res->message;
151             }
152             print $fh $msg;
153             close($fh);
154             }
155            
156             # try to scan file
157             if (!$trustworthy) {
158             open(my $fh,"$scanner '$tmpdir/$file' |");
159             my @msg=<$fh>;
160             close($fh);
161             my $scanrc = $?;
162            
163             # feed reponse back into our request_rec*
164             if (!$headersent) {
165             prepareheaders(\$r,\$res);
166             }
167              
168             # The following return code combinations from scanner
169             # rc file
170             # 0 exists clean, return file
171             # 0 deleted not allowed, fixed error Message
172             # !0 exists scan failed, fixed error Message
173             # !0 deleted infected, return stdout
174            
175             if ($scanrc == 0) {
176             if (-e "$tmpdir/$file") {
177             if (!$headersent) {
178             $r->rflush();
179             }
180             $r->sendfile("$tmpdir/$file", $delivered);
181             } else {
182             if ($res->is_error) {
183             if (!$headersent) {
184             $r->rflush();
185             }
186             $r->print($res->error_as_HTML);
187             } else {
188             my $msg=join("\n", @msg);
189             generateError(\$r, "Scanner Error", "Scanning ".$r->uri.":\n$msg");
190             }
191             }
192             } else {
193             if (-e "$tmpdir/$file") {
194             my $msg=join("\n", @msg);
195             generateError(\$r, "Scanner Error", "Scanning ".$r->uri.":\n$msg");
196             } else {
197             $r->headers_out->set("content-length" => undef);
198             $r->send_cgi_header(join('', @msg));
199             my $entry = join('', @msg);
200             $entry =~ s/<.*?>//igs;
201             $r->log_error("Virus Alert: ".$r->uri."\n$entry");
202             }
203             }
204             unlink "$tmpdir/$file" if (-e "$tmpdir/$file");
205             }
206             return OK;
207             }
208              
209             sub generateError {
210             my $r = shift @_;
211             my $title = shift @_;
212             my $text = shift @_;
213              
214             $$r->log_error("$title: $text");
215              
216             $text =~ s/[^A-Z0-9_\s\n]/sprintf("&#%d;", ord($&))/eigs;
217             $text =~ s/\n/
/igs;
218            
219             my $msg = "\n\n$title\n\n

$title

\n$text\n\n";
220            
221             $$r->content_type("text/html");
222             $$r->headers_out->set("content-length" => length($msg));
223             $$r->rflush();
224             $$r->print("$msg");
225            
226             return 1;
227             }
228              
229             sub prepareheaders {
230             my $r = shift @_;
231             my $res = shift @_;
232             $$r->content_type($$res->header('Content-type'));
233             $$r->status($$res->code);
234             $$r->status_line($$res->status_line);
235             my $table = $$r->headers_out;
236             $$res->scan(sub {
237             $table->add(@_) if ($_[0] !~ m/^Client[_-]/i);
238             });
239             return 1;
240             }
241              
242              
243             1;
244              
245             __END__