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__ |