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