File Coverage

blib/lib/App/DubiousHTTP/Tests/Common.pm
Criterion Covered Total %
statement 56 213 26.2
branch 2 80 2.5
condition 0 16 0.0
subroutine 9 37 24.3
pod 0 12 0.0
total 67 358 18.7


line stmt bran cond sub pod time code
1 1     1   3 use strict;
  1         1  
  1         20  
2 1     1   2 use warnings;
  1         1  
  1         23  
3             package App::DubiousHTTP::Tests::Common;
4 1     1   593 use Compress::Raw::Zlib;
  1         3777  
  1         167  
5 1     1   462 use MIME::Base64 'decode_base64';
  1         432  
  1         49  
6 1     1   4 use Exporter 'import';
  1         1  
  1         41  
7             our @EXPORT = qw(
8             MUSTBE_VALID SHOULDBE_VALID VALID INVALID UNCOMMON_VALID UNCOMMON_INVALID
9             SETUP content html_escape url_encode garble_url ungarble_url bro_compress zlib_compress
10             $NOGARBLE $CLIENTIP $TRACKHDR $FAST_FEEDBACK
11             );
12 1     1   3 use Scalar::Util 'blessed';
  1         1  
  1         100  
13              
14             our $CLIENTIP = undef;
15             our $NOGARBLE = 0;
16             our $FAST_FEEDBACK = 0;
17             use constant {
18 1         957 SHOULDBE_VALID => 3, # simple chunked, gzip.. - note if blocked
19             MUSTBE_VALID => 2, # no browser should fail on this
20             VALID => 1,
21             INVALID => 0,
22             UNCOMMON_VALID => -1,
23             UNCOMMON_INVALID => -2,
24 1     1   4 };
  1         1  
25              
26             my $basedir = 'static/';
27 0     0 0 0 sub basedir { $basedir = pop }
28              
29             {
30             my %bro = (
31             "Don't be afraid to look at this message. It is completely harmless. Really!"
32             => decode_base64('G0oAAIyUq+1oRZSkJ0v1kiZ2hk1hs4NDDti/XVogkErgISv5M41kDrdKRMH7fRK8YAmyXwFNYppR3EBMbVhyBA=='),
33             'X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*'
34             => decode_base64('G0MAABQhyezgvJQnNVXciUrtsAEHrvlk0bTzGSRPqOdwPRhITMNtn+G6LB8+EYrC/LjqijSZFRhTlo5XllmqeTHxsABuVSsB'),
35             );
36 0     0 0 0 sub bro_compress { return $bro{shift()} }
37             }
38              
39             my %builtin = (
40             'novirus.txt' => [
41             "Content-type: application/octet-stream\r\n".
42             "Content-disposition: attachment; filename=\"novirus.txt\"\r\n",
43             "Don't be afraid to look at this message. It is completely harmless. Really!",
44             ],
45             'eicar.txt' => [
46             "Content-type: application/octet-stream\r\n".
47             "Content-disposition: attachment; filename=\"eicar.txt\"\r\n",
48             'X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*',
49             'EICAR test virus',
50             ],
51             # zipped eicar, zip prefixed with dummy gzip
52             'eicar-gz-zip.zip' => [
53             "Content-type: application/octet-stream\r\n".
54             "Content-disposition: attachment; filename=\"eicar.zip\"\r\n",
55             pack("H*",'1f8b08006d718255000373492c56c82c2e5148cdcc5308492d2e01008b9f3a4b10000000504b03040a0000000000a84ad2463ccf5168440000004400000009001c0065696361722e636f6d55540900036b7182556b71825575780b000104e903000004e903000058354f2150254041505b345c505a58353428505e2937434329377d2445494341522d5354414e444152442d414e544956495255532d544553542d46494c452124482b482a504b01021e030a0000000000a84ad2463ccf51684400000044000000090018000000000001000000b4810000000065696361722e636f6d55540500036b71825575780b000104e903000004e9030000504b050600000000010001004f000000870000000000'),
56             'EICAR test virus in zip file, prefixed with gzip junk',
57             ],
58             'warn.png' => [ "Content-type: image/png\r\n", decode_base64( <<'IMAGE' ) ],
59             iVBORw0KGgoAAAANSUhEUgAAABkAAAAZCAIAAABLixI0AAAAI0lEQVQ4y2N8fkObgUqAiYF6YNSs
60             UbNGzRo1a9SsUbOGi1kA82oCHFP7+koAAAAASUVORK5CYII=
61             IMAGE
62             'ok.png' => [ "Content-type: image/png\r\n", decode_base64( <<'IMAGE' ) ],
63             iVBORw0KGgoAAAANSUhEUgAAABkAAAAZCAIAAABLixI0AAAAIklEQVQ4y2Nk+MZALcDEwDBq1qhZ
64             o2aNmjVq1qhZo2ahAQDhPQEogMYUlwAAAABJRU5ErkJggg==
65             IMAGE
66             'bad.png' => [ "Content-type: image/png\r\n", decode_base64( <<'IMAGE' ) ],
67             iVBORw0KGgoAAAANSUhEUgAAABkAAAAZCAIAAABLixI0AAAAI0lEQVQ4y2N8zKfDQCXAxEA9MGrW
68             qFmjZo2aNWrWqFnDxSwAAzgBT9lsF30AAAAASUVORK5CYII=
69             IMAGE
70             'chunked.gif' => [ "Content-type: image/gif\r\n", decode_base64( <<'IMAGE' ) ],
71             R0lGODlhFAAUAKUrAAAAAAQEBAUFBQsLCxMTExYWFhcXFxwcHB0dHSAgICEhISwsLDExMTMzMzY2
72             Njo6OkFBQUJCQkZGRkhISGhoaGlpaZiYmJmZmZqamp6enqCgoKKiosfHx9bW1tfX19/f3+Dg4OTk
73             5Obm5ujo6Onp6erq6vX19fb29vn5+fr6+vv7+///////////////////////////////////////
74             /////////////////////////////////////////////yH5BAEKAD8ALAAAAAAUABQAAAZgwJVw
75             SCwaj8ikcslEqjQTg2CwoHCUIgdgy90mUw3AwfJJmTqYRxIDSJCawwhgAx8SAKO6MABQ6Vd3eXoQ
76             c38XAAoleigMAAgXICknHmlKIWFdXEspGRIFAVQVV3+kpUpBADs=
77             IMAGE
78             'clen.gif' => [ "Content-type: image/gif\r\n", decode_base64( <<'IMAGE' ) ],
79             R0lGODlhFAAUAKEBAAAAAP///////////yH5BAEKAAIALAAAAAAUABQAAAIhjI+py+0PFwAxzYou
80             Nnp3/FVhNELlczppM7Wt6b7bTGMFADs=
81             IMAGE
82             'ok.html' => sub {
83             my $spec = shift;
84             return [ "Content-type: text/html\r\n",
85             "
HTML
" ]
86             },
87             'bad.html' => sub {
88             my $spec = shift;
89             return [ "Content-type: text/html\r\n",
90             "
HTML
" ]
91             },
92             'warn.html' => sub {
93             my $spec = shift;
94             return [ "Content-type: text/html\r\n",
95             "
HTML
" ]
96             },
97              
98             # we hide javascript behind GIF87a to work around content filters :)
99             'ok.js' => sub {
100             my $spec = shift;
101             return [ "Content-type: application/javascript\n",
102             "GIF87a=1;try { document.getElementById('$spec').style.backgroundColor = '#00e800'; } catch(e) {} ping_back('/ping?OK:$spec');" ]
103             },
104             'bad.js' => sub {
105             my $spec = shift;
106             return [ "Content-type: application/javascript\r\n",
107             "GIF87a=1;try { document.getElementById('$spec').style.backgroundColor = '#e30e2c'; } catch(e) {} ping_back('/ping?BAD:$spec');" ]
108             },
109             'warn.js' => sub {
110             my $spec = shift;
111             return [ "Content-type: application/javascript\r\n",
112             "GIF87a=1;try { document.getElementById('$spec').style.backgroundColor = '#e7d82b'; } catch(e) {} ping_back('/ping?WARN:$spec');" ]
113             },
114             'ping' => [ "Content-type: text/plain\r\n", "pong" ],
115             'ping.js' => [
116             "Content-type: application/javascript\r\n".
117             "Expires: Tue, 30 Jul 2033 20:04:02 GMT\r\n",
118             <<'PING_JS' ],
119             GIF87a=1;
120             function ping_back(url) {
121             var xmlHttp = null;
122             try { xmlHttp = new XMLHttpRequest(); }
123             catch(e) {
124             try { xmlHttp = new ActiveXObject("Microsoft.XMLHTTP"); }
125             catch(e) {
126             try { xmlHttp = new ActiveXObject("Msxml2.XMLHTTP"); }
127             catch(e) { xmlHttp = null; }
128             }
129             }
130             if (xmlHttp) {
131             xmlHttp.open('GET', url, true);
132             xmlHttp.send(null);
133             }
134             }
135             PING_JS
136             'set_success.js' => sub {
137             my $spec = shift;
138             return [ "Content-type: application/javascript\n", "set_success('$spec','js');" ]
139             },
140             'parent_set_success.html' => sub {
141             my $spec = shift;
142             return [ "Content-type: text/html\n", "" ]
143             },
144             'stylesheet.css' => [
145             "Content-type: text/css\r\n".
146             "Expires: Tue, 30 Jul 2033 20:04:02 GMT\r\n",
147             <<'STYLESHEET' ],
148             body { max-width: 55em; line-height: 140%; margin-left: 2em; }
149             ul { list-style-type: square; padding-left: 2em; }
150             h1 { font-variant: small-caps; font-size: x-large; }
151             h2,h3 { font-size: large; }
152             .runtest { text-align: right; margin-right: 5em; margin-top: 2em; }
153             .runtest a {
154             text-decoration: none;
155             background-color: #bfbfbf;
156             color: #333333;
157             padding: 4px 6px;
158             white-space: nowrap;
159             }
160             #test_novirus a { background-color: #70e270; padding: 8px 10px; }
161             #test_virus a { background-color: #ff4d4d; padding: 8px 10px; }
162              
163             h1,h2,h3 { border: 1px; border-style: solid; padding: 5px 10px 5px 10px; }
164             h1 { color: #000; background: #eee; padding-top: 10px; padding-bottom: 10px; }
165             h2 { color: #444; background: #eee; }
166             h3 { color: #444; background: #fff; }
167             h2,h3 { margin-top: 2em; }
168              
169             * { font-size: medium; font-family: Verdana,sans-serif; }
170              
171             pre { font-family: Monospace,monospace; }
172              
173             .button {
174             text-decoration: none;
175             background-color: #EEEEEE;
176             color: #333333;
177             padding: 2px 6px 2px 6px;
178             border-top: 1px solid #CCCCCC;
179             border-right: 1px solid #333333;
180             border-bottom: 1px solid #333333;
181             border-left: 1px solid #CCCCCC;
182             white-space: nowrap;
183             }
184             STYLESHEET
185              
186             # give the bots something to play with
187             'robots.txt' => [
188             "Content-type: text/plain\r\n",
189             "User-Agent: *\nDisallow: /have-fun-looking-for-goodies/\n"
190             ],
191             # and a nice favicon
192             'favicon.ico' => [ "Content-type: image/vnd.microsoft.icon\r\n", decode_base64(<<'FAVICON') ],
193             AAABAAEAEBAQAAEABAAoAQAAFgAAACgAAAAQAAAAIAAAAAEABAAAAAAAgAAAAAAAAAAAAAAAEAAA
194             AAAAAAAAAAAASB3MAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
195             AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAERAAAAAAAAAREAAAAAAAABAQAAAAAAARER
196             EQAAAAARAAABEAAAAAAAAAAAAAAAAAERAAAAAAAAABAAAAAAAAAAAAAAAAAAAQAAAQAAAAAREAAR
197             EAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//wAA//8AAP4/AAD+PwAA/r8AAPgPAADz5wAA//8A
198             AP4/AAD/fwAA//8AAPvvAADxxwAA//8AAP//AAD//wAA
199             FAVICON
200             );
201              
202              
203             sub content {
204 0     0 0 0 my ($page,$spec) = @_;
205 0         0 $page =~s{^/+}{};
206 0         0 my ($hdr,$data);
207 0 0       0 if ( my $builtin = $builtin{$page} ) {
208 0 0       0 $builtin = $builtin->($spec,"/$page") if ref($builtin) eq 'CODE';
209 0         0 return @$builtin;
210             }
211 0 0 0     0 if ( $basedir && open( my $fh,'<',"$basedir/$page" )) {
212 0 0       0 $hdr =
    0          
    0          
    0          
213             $page =~m{\.js$} ? "Content-type: application/javascript\r\n" :
214             $page =~m{\.css$} ? "Content-type: text/css\r\n" :
215             $page =~m{\.html?$} ? "Content-type: text/html\r\n" :
216             $page =~m{\.(gif|png|jpeg)$} ? "Content-type: image/$1\r\n" :
217             "";
218 0         0 $data = do { local $/; <$fh> };
  0         0  
  0         0  
219 0         0 return ($hdr,$data);
220             }
221 0         0 return;
222             }
223              
224             sub html_escape {
225 0     0 0 0 local $_ = shift;
226 0         0 s{\&}{&}g;
227 0         0 s{<}{<}g;
228 0         0 s{>}{>}g;
229 0         0 return $_
230             }
231              
232             sub url_encode {
233 0     0 0 0 local $_ = shift;
234 0         0 s{([^\w\-&/?=!$~.,;])}{ sprintf("%%%02X",ord($1)) }esg;
  0         0  
235 0         0 return $_;
236             }
237              
238             sub SETUP {
239 1     1 0 13 my ($id,$desc,$ldesc,@tests) = @_;
240 1         3 my $pkg = caller();
241 1         4 my @tests_only;
242 1         2 for my $t (@tests) {
243             # title | valid,spec,desc
244 171 100       182 if (@$t>1) {
245 159         91 $t = bless [ @{$t}[1,2,0] ], $pkg.'::Test';
  159         221  
246 159         132 push @tests_only, $t;
247             }
248             }
249              
250 1     1   5 no strict 'refs';
  1         2  
  1         1475  
251 1     0   4 *{$pkg.'::ID'} = sub { $id };
  1         5  
  0         0  
252 1     0   3 *{$pkg.'::SHORT_DESC'} = sub { $desc };
  1         4  
  0         0  
253 1     0   1 *{$pkg.'::LONG_DESC_HTML'} = sub { $ldesc };
  1         4  
  0         0  
254 1     0   4 *{$pkg.'::TESTS'} = sub { @tests_only };
  1         15  
  0         0  
255 1         3 *{$pkg.'::make_index_page'} = sub {
256 0     0   0 my ($self,$page,$spec,$rqhdr) = @_;
257 0 0       0 return make_index_page($pkg,@tests) if ! $spec;
258 0 0       0 return make_index_page($pkg,undef,grep { $_->[0] && $_->[0] eq $spec } @tests);
  0         0  
259 1         3 };
260              
261 1     0   1 *{$pkg.'::Test::ID'} = sub { shift->[0] };
  1         3  
  0         0  
262 1     0   6 *{$pkg.'::Test::LONG_ID'} = sub { "$id-" . shift->[0] };
  1         3  
  0         0  
263 1     0   3 *{$pkg.'::Test::NUM_ID'} = sub { _path2num("$id/".shift->[0]) };
  1         3  
  0         0  
264 1     0   3 *{$pkg.'::Test::DESCRIPTION'} = sub { shift->[1] };
  1         4  
  0         0  
265 1     0   2 *{$pkg.'::Test::VALID'} = sub { shift->[2] };
  1         3  
  0         0  
266 1         3 *{$pkg.'::Test::url'} = sub {
267 0     0   0 my ($self,$page) = @_;
268 0         0 return garble_url("/$id/$page/$self->[0]");
269 1         2 };
270 1         4 *{$pkg.'::Test::make_response'} = sub {
271 0     0     my ($self,$page,$spec,$rqhdr) = @_;
272 0           return $pkg->make_response($page,$self->[0],$rqhdr);
273 1         2 };
274             }
275              
276             sub make_index_page {
277 0     0 0   my ($class,@tests) = @_;
278 0           my $body = <<'BODY';
279            
280            
281            
282             BODY
283 0 0         if ($tests[0]) {
284 0           $body .= "

".$class->SHORT_DESC."

";
285 0           $body .= $class->LONG_DESC_HTML()."
";
286             } else {
287             # skip header
288             shift @tests
289 0           }
290 0           $body .= ''; "; "; "; "; "; "; "; "; "; ";
291 0           for my $test (@tests) {
292 0 0         if (!blessed($test)) {
293 0           $body .= "

$test->[0]

294 0           next;
295             }
296 0           my $valid = $test->VALID;
297 0 0         my $base = $valid>0 ? 'ok' : $valid<0 ? 'warn' : 'bad';
    0          
298 0 0         my $bg = $valid>0 ? '#e30e2c' : $valid<0 ? '#d0cfd1' : '#00e800';
    0          
299 0           $body .= "
300 0           $body .= "". html_escape($test->DESCRIPTION) ."
301 0           $body .= "
IMAGE
302 0           $body .= "
SCRIPT
303 0           $body .= "
304 0           $body .= " load EICAR 
305             # $body .= " load gzjunk+eicar.zip 
306 0           $body .= "
307 0           $body .= "";
308 0           $body .= "

309             }
310 0           $body .= "
";
311 0           $body .= "";
312 0           return "HTTP/1.0 200 Ok\r\n".
313             "Content-type: text/html\r\n".
314             "Content-length: ".length($body)."\r\n\r\n".
315             $body;
316             }
317              
318             sub garble_url {
319 0     0 0   my $url = shift;
320 0 0         return $url if $NOGARBLE;
321 0 0         my ($keep,$garble) = $url =~m{^((?:https?://[^/]+)?/)(.+)}
322             or return $url;
323 0 0         my $xor = $CLIENTIP ? _ip2bin($CLIENTIP): pack('L',rand(2**32));
324 0 0         my $g = ($CLIENTIP ? pack('C',length($xor)):'') . $xor . _xorall($garble,$xor);
325             # url safe base64
326 0           my $pad = ( 3 - length($g) % 3 ) % 3;
327 0           $g = pack('u',$g);
328 0           $g =~s{(^.|\n)}{}mg;
329 0           $g =~tr{` -_}{AA-Za-z0-9\-_};
330 0 0         substr($g,-$pad) = '=' x $pad if $pad;
331 0 0         return $keep . ($CLIENTIP?'-':'=') . $g;
332             }
333              
334             sub ungarble_url {
335 0     0 0   my $url = shift;
336 0 0         my ($keep,$type,$u,$rest) = $url =~m{^(.*/)([=-])([0-9A-Za-z_\-]+={0,2})([/? ].*)?$}
337             or return $url;
338             # url safe base64 -d
339 0           $u =~s{=+$}{};
340 0           $u =~tr{A-Za-z0-9\-_}{`!-_};
341 0           $u =~s{(.{1,60})}{ chr(32 + length($1)*3/4) . $1 . "\n" }eg;
  0            
342 0           $u = unpack("u",$u);
343 0 0         my $size = ($type eq '=') ? 4: unpack('C',substr($u,0,1,''));
344 0           my $xor = substr($u,0,$size,'');
345 0 0 0       ${$_[0]} = _bin2ip($xor) if $type ne '=' && @_;
  0            
346 0           $u = _xorall($u,$xor);
347             # make sure we only have valid stuff here
348 0 0         $u = 'some-binary-junk' if $u =~m{[\x00-\x1f\x7f-\xff]};
349 0   0       return $keep . $u . ($rest || '');
350             }
351              
352              
353             sub zlib_compress {
354 0     0 0   my ($data,$w) = @_;
355 0 0         my $zlib = Compress::Raw::Zlib::Deflate->new(
    0          
356             -WindowBits => $w eq 'gzip' ? WANT_GZIP : $w eq 'zlib' ? +MAX_WBITS() : -MAX_WBITS(),
357             -AppendOutput => 1,
358             );
359 0           my $newdata = '';
360 0           $zlib->deflate( $data, $newdata);
361 0           $zlib->flush($newdata,Z_FINISH);
362 0           return $newdata;
363             }
364              
365             {
366             my ($path2num,$num2path);
367             sub load_nummap {
368 0 0   0 0   my $maxold = @_>1 ? pop(@_) : 9999;
369 0   0       $num2path = eval(
370             "require App::DubiousHTTP::Tests::TestID;".
371             "App::DubiousHTTP::Tests::TestID->num2path"
372             ) || {};
373 0           $path2num = { reverse %$num2path };
374 0           my @new;
375 0           for my $mod ( App::DubiousHTTP::Tests->categories ) {
376 0           my $catid = $mod->ID;
377 0           for ($mod->TESTS) {
378 0           my $path = "$catid/".$_->ID;
379 0 0         if (my $n = $path2num->{$path}) {
380 0 0 0       $maxold = $n if !defined $maxold || $maxold<$n;
381             } else {
382 0           push @new,$path;
383             }
384             }
385             }
386 0           for(@new) {
387 0           $maxold++;
388 0           $num2path->{$maxold} = $_;
389 0           $path2num->{$_} = $maxold;
390             }
391 0           return $num2path;
392             }
393             sub _path2num {
394 0     0     my $path = shift;
395 0 0         $path2num || load_nummap;
396 0           return $path2num->{$path};
397             }
398 0     0 0   sub num2path { _num2path($_[1]) }
399             sub _num2path {
400 0     0     my $num = shift;
401 0 0         $num2path || load_nummap;
402 0           return $num2path->{$num};
403             }
404             }
405              
406             sub _xorall {
407 0     0     my ($data,$xor) = @_;
408 0           my @x = unpack('a' x length($xor),$xor);
409 0           my @c = split('',$data);
410 0           $data = '';
411 0           while (@c) {
412 0           $data .= shift(@c) ^ $x[0];
413 0           push @x, shift(@x);
414             }
415 0           return $data;
416             }
417              
418             sub _ip2bin {
419 0     0     my $ip = shift;
420              
421             # inet_ntop(AF_INET,...)
422 0 0         return pack("CCCC",split(m{\.},$1))
423             if $ip =~m{^(?:::ffff:)?(\d+\.\d+\.\d+\.\d+)$};
424              
425             # inet_ntop(AF_INET6,...)
426 0           my @p = split(m{:},$ip);
427 0           $ip = '';
428 0           for(my $i=0;$i<@p;$i++) {
429 0 0         if ($p[$i] eq '') {
430 0           $p[$i] = '0';
431 0           splice(@p,$i,0,'0') while @p<8;
432             }
433 0           $ip .= pack("n",hex($p[$i]));
434             }
435 0           return $ip;
436             }
437              
438             sub _bin2ip {
439 0     0     my $ip = shift;
440 0 0         return join('.',unpack('CCCC',$ip)) if length($ip) == 4;
441 0           my @part = unpack("n8",$ip);
442 0           my (@null,$null,$maxnull);
443 0           for( my $i=0;$i<@part;$i++) {
444 0 0         if (!$part[$i]) {
445 0           $part[$i] = '0';
446 0 0         if ($null) {
447 0           $$null++;
448 0 0 0       $maxnull = $#null if !$maxnull || $$null>$maxnull;
449             } else {
450 0           push @null,[$i,1];
451 0           $null = \$null[-1][1];
452             }
453             } else {
454 0           $part[$i] = sprintf("%x",$part[$i]);
455 0           $null = undef;
456             }
457             }
458 0 0         return join(':',@part) if !defined $maxnull;
459 0           my $begin = $null[$maxnull][0];
460 0           my $end = $begin + $null[$maxnull][1]-1;
461 0           return join(':', @part[0 .. $begin-1]). '::'. join(':',@part[$end+1 .. $#part]);
462             }
463              
464             1;