File Coverage

blib/lib/Net/DNSBL/Monitor.pm
Criterion Covered Total %
statement 316 323 97.8
branch 137 168 81.5
condition 62 108 57.4
subroutine 19 20 95.0
pod 5 11 45.4
total 539 630 85.5


'."\n"; '; \n"; ';
line stmt bran cond sub pod time code
1             #!/usr/bin/perl
2             package Net::DNSBL::Monitor;
3              
4 7     7   124120 use strict;
  7         15  
  7         378  
5             #use diagnostics;
6 7     7   8127 use Net::DNS::Codes qw(:all);
  7         22552  
  7         47270  
7 7         10030 use Net::DNS::ToolKit qw(
8             newhead
9             gethead
10             inet_ntoa
11             inet_aton
12 7     7   67949 );
  7         282186  
13 7     7   13716 use Net::DNS::ToolKit::RR;
  7         117394  
  7         1477  
14 7         677 use Net::DNS::ToolKit::Utilities qw(
15             id
16             revIP
17 7     7   9983 );
  7         335242  
18 7     7   11815 use NetAddr::IP::Lite;
  7         165250  
  7         48  
19 7         2706 use Net::DNSBL::Utilities qw(
20             DO
21             list2NetAddr
22             matchNetAddr
23 7     7   12317 );
  7         40874  
24             #use Net::DNS::ToolKit::Debug qw(
25             # print_head
26             # print_buf
27             #);
28              
29 7         54749 use vars qw(
30             $VERSION @ISA @EXPORT_OK
31 7     7   72 );
  7         13  
32             require Exporter;
33             @ISA = qw(Exporter);
34              
35             $VERSION = do { my @r = (q$Revision: 0.12 $ =~ /\d+/g); sprintf "%d."."%02d" x $#r, @r };
36              
37             @EXPORT_OK = qw(
38             run
39             plaintxt
40             htmltxt
41             plainresp
42             htmlresp
43             );
44              
45             # undocumented $DEBUG values
46             #
47             # 1 => return @ip array
48             # 2 => return %dnsbls initialization hash
49             # 3 => return ($iptr,$regexptr) ignore, regexp ptrs
50             # 4 => return %ips union aging hash
51             # 5 => return %qc hash
52             # 6 => return %response hash
53             # other => return un-converted %dnsbls hash
54              
55             =head1 NAME
56              
57             Net::DNSBL::Monitor - Monitor DNSBL response
58              
59             =head1 SYNOPSIS
60              
61             use Net::DNSBL::Monitor qw(
62             run
63             plaintxt
64             htmltxt
65             plainresp
66             htmlresp
67             );
68              
69             ($dnsblcnts,$responses)=run(\%config,$NonBlockSock,$sockaddr_in);
70             $text = plaintxt(\%config,\%dnsblcnts);
71             $html = htmltxt(\%config,\%dnsblnts);
72             $text = plainresp(\%responses);
73             $html = htmlresp(\$config,\%responses);
74              
75              
76             =head1 DESCRIPTION
77              
78             B is the Perl module that can be used to check when
79             ranges of IP addresses have been blacklisted. B
80             generates a statistical report for the DNSBL's interrogated and provides an
81             individual report for each IP address found in a DNSBL.
82             The module is used to implement the reporting script B.
83              
84             =head1 CONFIGURATION FILE
85              
86             With the addition of a few elements, the configuration file for B
87             shares a common format with the Mail::SpamCannibal sc_BLcheck.pl script, and
88             Net::DNSBL::Statistics config file,
89             facilitating common maintenance of DNSBL's for your MTA installation.
90              
91             The sample configuration file
92             B is heavily commented with the details for each
93             configuration element.
94              
95             A useful list of DNSBL zones for the config file (put together by
96             "Andrey V. Stolyarov" )
97             maybe found in the B directory C.
98              
99             =head1 SYSTEM SIGNALS
100              
101             B responds to the following system signals:
102              
103             =over 2
104              
105             =item * TERM
106              
107             Script is terminated.
108              
109             =back
110              
111             =head1 PERL MODULE DESCRIPTION - Script Implementation
112              
113             B provides most of the functions that implement
114             B which is a script that collects statistics and responses for a list of IP
115             address interrogated against a list of DNSBL's
116              
117             =head1 dnsblmon usage
118              
119             How to use B
120              
121             Syntax: dnsblmon path/to/config.file
122             or
123             dnsblmon -t path/to/config.file
124             dnsblmon -w path/to/config.file
125              
126             dnsblmon path/to/config.file -r -s
127             dnsblmon path/to/config.file -s -r
128              
129             dnsblmon path/to/config.file \
130             -r path/outfile1 \
131             -s path/outfile2
132              
133             Normally dnsblmon prints an IP report sorted by "comment"
134             and IP of the DNSBL's interrogated with their reply
135             results for each IP address.
136              
137             The 'comment' field may contain html markup text.
138              
139             i.e. commenta
140             1.2.3.4 127.0.0.3 zen.spamhaus.org
141             127.0.0.5 dnsbl.sorbs.net
142             1.2.3.5 127.0.0.5 dnsbl.sorbs.net
143              
144             commentb
145             3.4.5.6 127.0.0.2 bl.spamcannibal.org
146              
147              
148             With the (-s) switch, dnsblmon prints a sorted list
149             (by count) of the DNSBL's interrogated with their
150             reply count, percentage of the total count, and any
151             comments from the DNSBL's 'comment' key field in
152             the config file.
153              
154             The 'comment' field may contain html markup text.
155              
156             i.e.
157             44 100.0% TOTAL IP's interrogated
158             41 93.2% UNION of all results
159             34 77.3% dnsbl.sorbs.net comment
160             ........
161              
162             The -t switch will print a start and stop time to STDOUT
163              
164             i.e.
165             # start: Fri Jan 4 17:46:44 2008
166             # stop : Fri Jan 4 17:58:21 2008
167              
168             The -w switch will put the output into an HTML table
169             without the statement
., a commment as above
170             and with an dnsbl name statement replacing
171             the dnsbl name if the 'url' key is present in the config file.
172              
173             The -r and -s switchs are position dependent output designators.
174              
175             -r REPORT output designator
176             -s STATISTICS output designator
177              
178             as follows:
179              
180             -r -s would produce the report on STDOUT and the statistics
181             on STDERR.
182             -s -r would produce the statistics on STDOUT and the
183             report on STDERR.
184              
185             -s outfile1 -r outfile2 would write the statistics and report
186             to outfile1 and outfile2 respectively.
187              
188             Other combinations are possible. Switches are order dependent
189             but not positionally dependent with respect to other switches
190              
191             =head1 dnsblmon input file format
192              
193             The input file format for B consists of an address element block
194             in the form used by B followed by a 'comment field'. The report
195             generated will be sorted on the 'comment field', then by IP address.
196              
197             i.e. input format
198              
199             1.2.3.4 single host IP belonging to XYZ
200             2.3.4.5/24 A class C belonging to ABC hosting
201             etc...
202              
203              
204             =head1 Net::DNSBL::Monitor FUNCTIONS
205              
206             =over 4
207              
208             =item * ($dnsblcnts,$responses)=run(\%config,$NonBlockSock,$sockaddr_in);
209              
210             Returns the total number of IP's interrogated (IP list less white listed items) and a hash of DNSBL
211             names and their respective SPAM reply counts or equivalent for 'in-addr.arpa' and GENERIC.
212              
213             input: config pointer,
214             non-blocking socket descriptor,
215             sockaddr_in for local DNS host
216              
217             returns: pointer to dnsbl count hash
218             pointer to response hash
219              
220             The dnsbl count hash will have two added keys:
221              
222             TOTAL the total number of interrogations less whitelist
223             UNION the total number of spam 'hits'
224              
225             The response hash will be of the form:
226              
227             $response = {
228             '1.2.3.4' => {
229             'COMMENT' => 'text from comment field for IP range',
230             'bl.xyz.com' => '127.0.0.2',
231             'bl.abc.net' => '127.0.0.5',
232             },
233             etc...
234             };
235              
236             HINTs: use Net::NBsocket qw( open_udbNB sockaddr_in );
237             use Net::DNS::ToolKit qw( get_ns );
238              
239             my $sock = open_udpNB();
240             my $sockaddr_in = sockaddr_in(53, scalar get_ns());
241              
242             =cut
243              
244             my $w = 0;
245             my @w = qw( \ | / - );
246             sub whirl {
247 261     261 0 1732 return;
248 0         0 print STDERR "\r",$w[$w],"\r";
249 0 0       0 $w = 0 if ++$w > $#w;
250             }
251              
252             sub run {
253 13     13 1 459401 my($conf,$Usock,$U_Sin,$DEBUG) = @_;
254 13         3046 my(%ips,%ipin);
255 13 100       113 return () unless $conf->{FILES};
256 12 100       96 my @files = (ref $conf->{FILES}) ? @{$conf->{FILES}} : ($conf->{FILES});
  3         11  
257              
258 12         931 local *F;
259 12         51 foreach (@files) {
260 15 100 66     1127 next unless -e $_ && open F, $_;
261 10         488 foreach () {
262 84 100       27803 next unless $_ =~ /\S/;
263 82 50       253 next if $_ =~ /^\s*#/;
264 82 50       801 next unless $_ =~ /(\d{1,3}[\S]*)\s*/;
265 82         190 my $nad = $1;
266 82   100     356 my $cmt = $' || '';
267 82         1043 while ($cmt =~ /\s$/) { chop $cmt };
  66         306  
268 82         436 my $nip = new NetAddr::IP::Lite($nad);
269 82         13028 my $this = $nip->network();
270 82         1847 my $stop = $nip->network();
271 82         1140 do {
272 129         13716 my $tip = $this->addr();
273 129         30527 $ipin{"$tip"} = $cmt;
274 129         589 ++$this;
275             } while $this != $stop
276             }
277 10         4026 close F;
278             }
279              
280 12         96 my @ips = keys %ipin;
281              
282 12 100 100     160 return @ips if $DEBUG && $DEBUG == 1;
283              
284 9         22 my @NAignor;
285 9 50       41 if ($conf->{IGNORE}) {
286 9         302 list2NetAddr($conf->{IGNORE},\@NAignor);
287             }
288              
289             ############## configure %dnsbl has for accumulating stats ###############
290 9   66     7122 my @DNSBLs = grep( $_ =~ /^[0-9a-z]+\.[0-9a-z]/i && $_ !~ /in-addr/i, keys %{$conf});
  9         356  
291              
292 9         26 my %dnsbls;
293              
294 9         27 foreach(@DNSBLs) {
295 35         151 $dnsbls{"$_"} = {
296             C => 0, # count
297             TO => 0, # timeouts
298             };
299             }
300              
301             #### %dnsbls configuration complete, configure maximum union timeout
302 9         104 my $uto = 0;
303 9         35 foreach(keys %dnsbls) {
304 35 50 33     679 next unless exists $conf->{"$_"} &&
305             exists $conf->{"$_"}->{timeout};
306 35 100       116 next if $conf->{"$_"}->{timeout} < $uto;
307 26         73 $uto = $conf->{"$_"}->{timeout};
308             }
309              
310 9         30 my($iptr,$regexptr);
311 9         17 my $needPTR = 0;
312 9 100       43 if ($conf->{'in-addr.arpa'}) {
313 5         24 $dnsbls{'in-addr.arpa'} = { C => 0 };
314 5   50     25 $needPTR = $conf->{'in-addr.arpa'}->{timeout} || 30;
315             }
316 9 50       46 if ($conf->{GENERIC}) {
317 9         40 $dnsbls{GENERIC} = { C => 0 };
318 9 100 50     43 $needPTR = ($conf->{GENERIC}->{timeout} || 30)
319             unless $needPTR;
320 9 50 33     144 undef $regexptr unless ($regexptr = $conf->{GENERIC}->{regexp}) &&
      33        
321             ref $regexptr eq 'ARRAY' && @$regexptr > 0;
322 9 50 33     106 undef $iptr unless ($iptr = $conf->{GENERIC}->{ignore}) &&
      33        
323             ref $iptr eq 'ARRAY' && @$iptr > 0;
324             }
325             ### adjust $uto to account for generic retries and in-addr.arpa timeouts
326 9 50       28 $uto = $needPTR
327             if $uto < $needPTR;
328 9 50       24 $uto = 30 unless $uto;
329             #### maximum $uto = 2x max delay + a little
330 9         20 $uto *= 2;
331 9         20 $uto += 5;
332              
333 9 100 100     75 return %dnsbls if $DEBUG && $DEBUG == 2;
334              
335 8 100 100     62 return ($iptr,$regexptr) if $DEBUG && $DEBUG == 3;
336              
337 7         40 my %qc = (
338             'in-addr' => 0,
339             'regular' => 0,
340             # retries below
341             'generic' => 0,
342             'retry-r' => 0,
343             );
344              
345 7         12 my %queue;
346 7         41 my $fileno = fileno($Usock);
347 7         22 my $vin = '';
348 7         38 vec($vin,$fileno,1) = 1;
349 7         17 my $Run = 1;
350 7     0   175 local $SIG{TERM} = sub {$Run = 0};
  0         0  
351              
352 7         20 my $qsize = keys %dnsbls;
353 7         14 my $then = time;
354 7         10 my $uage = $then; # union aging every 5 seconds
355 7         184 my ($get,$put,$parse) = new Net::DNS::ToolKit::RR;
356 7         181 my($comment,$rin,$rout,$win,$wout,$ip,$packet,$name,$id,$msg,$bl,$now,$revIP,$nfound,$answer,$ttl,$rdl,@rdata,@sndQ);
357              
358 7         119 my %respons = (
359             # ip => {
360             # COMMENT => 'comment text',
361             # 'dnsbl.name' => 'code',
362             # },
363             );
364              
365             LOOP:
366 7         23 while ($Run) {
367 339         5450 my $Q = keys %queue;
368 339 100 100     1615 if ($qsize > $Q && ($ip = shift @ips)) { # run results for next IP if queue is not double full
369 70 100       6182 next if matchNetAddr($ip,\@NAignor);
370 56         18358 ++$dnsbls{TOTAL}->{C};
371 56         101 $now = time;
372 56         1974 $revIP = revIP($ip);
373 56 50       1313 if ($needPTR) {
374 56         175 $id = makid(\%queue);
375 56         137 $name = $revIP .'.in-addr.arpa';
376 56         267 $packet = makequery($put,$id,$name,T_PTR());
377 56         549 $queue{$id} = {
378             B => 'in-addr.arpa',
379             Q => $packet,
380             T => $now + $needPTR, # timeout
381             R => 0, # retry
382             X => $revIP,
383             };
384 56         123 push @sndQ, $packet;
385 56         123 ++$qc{'in-addr'};
386             }
387            
388 56         142 foreach $bl (@DNSBLs) {
389 168 100       620 next if $dnsbls{"$bl"}->{TO} > 5; # ignore this BL if it timed out to many times
390 163         436 $id = makid(\%queue);
391 163         391 $name = $revIP .'.'. $bl;
392 163         482 $packet = makequery($put,$id,$name,T_A());
393 163   50     1496 $queue{$id} = {
394             B => "$bl",
395             Q => $packet,
396             T => $now + ($conf->{"$bl"}->{timeout} || 30),
397             R => 0,
398             X => $revIP,
399             };
400 163         299 push @sndQ, $packet;
401 163         433 ++$qc{regular};
402             }
403             }
404              
405             # wait for some responses
406 325         920 $rin = $vin;
407 325 100       869 if (@sndQ) {
408 70         157 $win = $vin;
409             } else {
410 255         700 $win = '';
411             }
412 325         72220126 $nfound = select($rout=$rin,$wout=$win,undef,0.5); # tick each second
413 325 100       2071 if ($nfound > 0) {
414 181   100     1699 while (vec($wout,$fileno,1) && @sndQ) {
415 302         739 $packet = shift @sndQ;
416             #print STDERR "WRITE\n";
417             #print_buf(\$packet);
418             #print STDERR "\n";
419 302         68970 send($Usock,$packet,0,$U_Sin);
420 302 100       2041 whirl() if $DEBUG;
421             }
422 181 100       570 if (vec($rout,$fileno,1)) {
423 133         212 undef $msg;
424 133 50       843 next unless recv($Usock,$msg,,PACKETSZ,0); # ignore receive errors
425 133 50       2049 next unless length($msg) > HFIXEDSZ; # ignore short packets
426             #print STDERR "RECEIVE\n";
427             #print_buf(\$msg);
428             #print STDERR "\n";
429 133         763 my($off,$qr,$opcode,$aa,$tc,$rd,$ra,$mbz,$ad,$cd,$rcode,$qdcount,$ancount,$nscount,$arcount);
430 133         814 ($off,$id,$qr,$opcode,$aa,$tc,$rd,$ra,$mbz,$ad,$cd,$rcode,$qdcount,$ancount,$nscount,$arcount)= gethead(\$msg);
431             next unless
432 133 50 33     1395 $tc == 0 &&
      33        
      66        
      33        
      33        
      33        
433             $qr == 1 &&
434             $opcode == QUERY &&
435             ($rcode == NOERROR || $rcode == NXDOMAIN || $rcode == SERVFAIL) &&
436             $qdcount == 1 &&
437             exists $queue{$id};
438              
439 133         3105 ($off,my($name,$t,$class)) = $get->Question(\$msg,$off);
440 133 50       1958 next unless $class == C_IN; # not my question
441              
442 133         762 $bl = $queue{$id}->{B};
443 133         301 $revIP = $queue{$id}->{X};
444            
445 133         958 delete $queue{$id};
446 133 100       643 $dnsbls{"$bl"}->{TO} = 0 # reset timeout count
447             unless $bl eq 'in-addr.arpa';
448 133 100 66     539 if ($ancount && $rcode == &NOERROR) { # if good response
    100 33        
    100 66        
449 84         740 $name =~ /(?:\d+\.\d+\.\d+\.\d+\.)/i;
450 84 50 66     554 next unless lc $bl eq lc $' &&
      33        
451             ($t == T_A || $t == T_PTR);
452              
453 84         644 undef $answer;
454 84         118 my @generic;
455             ANSWER:
456 84         2298 foreach(0..$ancount-1) {
457 126         704 ($off,$name,$t,$class,$ttl,$rdl,@rdata) = $get->next(\$msg,$off);
458 126 100 33     19396 if ($t == T_A) {
    50 33        
459 77         477 foreach $answer (@rdata) {
460 77         3314 $ip = inet_ntoa($answer);
461 77 100       1653 if (grep($ip eq $_,keys %{$conf->{"$bl"}->{accept}})) {
  77         692  
462 35         150 onion(\%dnsbls,\%ips,$revIP,$now + $uto,\%ipin,$bl,\%respons,$ip);
463             #print STDERR "FAILED $name $ip\n";
464 35         198 next LOOP;
465             }
466             }
467 42 50       226 if (exists $conf->{"$bl"}->{acceptany}) {
468 0         0 $ip = inet_ntoa($rdata[0]);
469 0         0 onion(\%dnsbls,\%ips,$revIP,$now + $uto,\%ipin,$bl,\%respons,$ip);
470 0         0 next LOOP;
471             }
472             }
473             elsif ($needPTR && $t == T_PTR && exists $dnsbls{GENERIC}) {
474             # positive in-addr.arpa responses are ignored
475 49         770 push @generic, $rdata[0];
476 49         331 next ANSWER;
477             }
478             }
479 49 100       210 if (@generic) {
480 35         104 foreach my $g (@generic) {
481 49 100 66     1973 next LOOP if $iptr && grep($g =~ /$_/i, @$iptr);
482 35 100 66     7251 next LOOP if $g && ! grep($g =~ /$_/i, @$regexptr);
483             }
484 14         93 onion(\%dnsbls,\%ips,$revIP,$now + $uto,\%ipin,'GENERIC',\%respons,'127.0.0.2');
485             #print STDERR "FAILED $name GENERIC\n";
486             }
487             }
488             elsif ($t == T_A) {
489 35         238 next LOOP; # tis a lookup failure or no response... ignore
490             }
491             elsif ($needPTR && $t == T_PTR && exists $conf->{'in-addr.arpa'}) {
492 6         141 onion(\%dnsbls,\%ips,$revIP,$now + $uto,\%ipin,'in-addr.arpa',\%respons,'127.0.0.2');
493             #print STDERR "FAILED $name ERROR\n";
494 6         27 next LOOP;
495             }
496             }
497             }
498             ######################################################################
499             else { # timeout
500 144         607 $now = time;
501 144 100       1490 next unless $now > $then;
502 72         171 $then = $now;
503 72         1718 my @queue = sort {$queue{$a}->{T} <=> $queue{$b}->{T}} keys %queue;
  484         1838  
504 72         489 foreach $id (@queue) { # check for DNSBL timeouts
505 204 100       705 last if $now < $queue{$id}->{T};
506 169         449 $bl = $queue{$id}->{B};
507 169 100       380 if ($bl eq 'in-addr.arpa') {
508 11 100       86 if (exists $conf->{'in-addr.arpa'}) {
    50          
509 3         9 $revIP = $queue{$id}->{X};
510 3         21 delete $queue{$id};
511 3         29 onion(\%dnsbls,\%ips,$revIP,$now + $uto,\%ipin,'in-addr.arpa',\%respons,'127.0.0.2');
512             #print STDERR "FAILED $revIP.in-addr.arpa timeout\n";
513             }
514             elsif (exists $dnsbls{GENERIC}) {
515 8 100       33 unless ($queue{$id}->{R}) {
516 4         10 $queue{$id}->{R} = 1; # retry generic queries
517 4         11 $queue{$id}->{T} = $now + $needPTR;
518 4         12 push @sndQ, $queue{$id}->{Q};
519 4         13 ++$qc{generic};
520             }
521             else {
522 4         21 delete $queue{$id};
523             }
524             }
525             }
526             else { # regular DNSBL
527 158 100       415 unless ($queue{$id}->{R}) {
528 79         137 $queue{$id}->{R} = 1;
529 79   50     385 $queue{$id}->{T} = $now + ($conf->{"$bl"}->{timeout} || 30);
530 79         194 push @sndQ, $queue{$id}->{Q};
531 79         328 ++$qc{'retry-r'};
532             }
533             else {
534 79         253 $revIP = $queue{$id}->{X};
535 79         302 delete $queue{$id};
536 79         313 ++$dnsbls{"$bl"}->{TO};
537             }
538             }
539             }
540 72 100 100     705 last LOOP unless @ips || keys %queue; # run through all IP's and remaining queue items
541 65 100       479 next unless $uage < $now;
542 14         41 $uage = $now + 5;
543 14         445 @_ = sort {$ips{"$a"} <=> $ips{"$b"}} keys %ips;
  43         155  
544 14         63 foreach (@_) {
545 12 50       107 last if $ips{"$_"} > $now;
546 0         0 delete $ips{"$_"};
547             }
548             } # else nfound
549             } # while ($Run)
550              
551 7 100       84 close $Usock unless $DEBUG;
552              
553 7 100       45 if ($DEBUG) {
554 6 100       223 return %ips if $DEBUG == 4;
555 4 100       212 return %qc if $DEBUG == 5;
556 2 50       8 return %respons if $DEBUG == 6;
557 2         264 return %dnsbls; # for any other debug value
558             }
559             else {
560 1         9 foreach(keys %dnsbls) {
561 6         24 $dnsbls{$_} = $dnsbls{$_}->{C};
562             }
563             }
564 1         71 return (\%dnsbls,\%respons);
565             }
566              
567             sub makequery {
568 219     219 0 1018 my($put,$id,$name,$type) = @_;
569 219         292 my $buf;
570 219         685 my $off = newhead(\$buf,
571             $id,
572             BITS_QUERY | RD,
573             1,0,0,0,
574             );
575 219         16556 $off = $put->Question(\$buf,$off,$name,$type,C_IN);
576 219         5603 return $buf;
577             }
578              
579             sub makid {
580 219     219 0 382 my $qp = shift;
581 219         263 my $id;
582 219         278 do {
583 219         7244 $id = id()
584             } while exists $qp->{$id};
585 219         2003 return $id;
586             }
587              
588             sub union {
589 58     58 0 116 my($dnsbls,$union,$rip,$expire) = @_;
590 58         92 $expire += 30; # union cache expiration is alway longer than timeouts
591 58 100       185 if (exists $union->{"$rip"}) {
592 24 50       87 $union->{"$rip"} = $expire
593             if $expire > $union->{"$rip"};
594             } else {
595 34         103 $union->{"$rip"} = $expire;
596 34         117 ++$dnsbls->{UNION}->{C};
597             }
598             }
599              
600             sub onion {
601 58     58 0 150 my($dnsbls,$union,$rip,$expire,$ipin,$bl,$resp,$code) = @_;
602 58         163 ++$dnsbls->{"$bl"}->{C};
603 58         149 &union;
604 58         2120 my $ip = revIP($rip);
605 58 100       811 if (exists $resp->{"$ip"}) {
606 24         96 $resp->{"$ip"}->{"$bl"} = $code;
607             }
608             else {
609 34         309 $resp->{"$ip"} = {
610             COMMENT => $ipin->{"$ip"},
611             "$bl" => $code,
612             }
613             }
614             }
615              
616              
617             =item * $text = plaintxt($config,\%dnsbls);
618              
619             Generate a plain text report of the form:
620              
621             44 100.0% TOTAL IP's interrogated
622             41 93.2% UNION of all results
623             34 77.3% dnsbl.sorbs.net comment
624             22 50.0% GENERIC comment
625             13 29.5% in-addr.arpa comment
626             11 25.0% cbl.abuseat.org comment
627             9 20.5% list.dsbl.org comment
628             2 4.5% dnsbl.njabl.org comment
629             1 2.3% bl.spamcannibal.org comment
630             0 0.0% dynablock.njabl.org comment
631              
632             input: configuration pointer,
633             dnsbl count hash pointer
634             returns: text buffer
635              
636             The 'comment' comes from the config file 'comment' key field for each
637             specified DNSBL or is blank if there is no 'comment' key.
638              
639             =cut
640              
641             # return 'comment' and 'url' if present
642             # input: $conf, $bl, $nbsp
643             # output: if $nbsp ($comment,$url)
644             # if !$nbsp $comment
645             #
646             sub cmurl {
647 39     39 0 56 my($conf,$bl,$nbsp) = @_;
648 39 100       126 if ($bl eq 'TOTAL') {
    100          
649 2 100       6 if ($nbsp) {
650 1         3 return (q|IP's interrogated|,'');
651             } else {
652 1         4 return q|IP's interrogated|;
653             }
654             } elsif ($bl eq 'UNION') {
655 2 100       6 if ($nbsp) {
656 1         3 return (q|of all results|,'');
657             } else {
658 1         3 return q|of all results|;
659             }
660             }
661 35 100 66     208 my $comment = (exists $conf->{"$bl"}->{comment} && $conf->{"$bl"}->{comment})
    100          
662             ? $conf->{"$bl"}->{comment}
663             : ($nbsp) ? ' ' : '';
664 35 100       72 return $comment unless $nbsp;
665 25 100 66     122 my $url = (exists $conf->{"$bl"}->{url} && $conf->{"$bl"}->{url})
666             ? q|{url} .q|">|
667             : '';
668 25         63 return ($comment,$url);
669             }
670            
671             sub plaintxt {
672 1     1 1 41 my($conf,$dnsbls) = @_;
673 1 50 33     12 return "# ERROR list is empty\n"
674             unless keys %$dnsbls && $dnsbls->{TOTAL};
675 1         3 my $txt = '';
676 1         6 my $tot = $dnsbls->{TOTAL}/100;
677 1         3 my $len = length($dnsbls->{TOTAL});
678 1         41 foreach(sort{$dnsbls->{"$b"} <=> $dnsbls->{"$a"}} keys %$dnsbls) {
  32         54  
679 12         23 my $comment = cmurl($conf,$_);
680 12         100 $txt .= sprintf("% ${len}u% 6.1f%% %s %s\n",$dnsbls->{"$_"},$dnsbls->{"$_"}/$tot,$_,$comment);
681             }
682 1         6 return $txt;
683             }
684              
685             =item * $html = htmltxt($config,\%dnsbls);
686              
687             Generate a report as above but with EtrEEtdEE/tdEE/trE table markup. The
688             EtableEE/tableE tags are not generated. If there is a 'url' key field in the
689             respective DNSBL config entry, the DNSBL name is provide with Ea href="url"EDNSBLE/aE
690             tags with the specified 'url' as the 'href' value.
691              
692             input: configuration pointer,
693             dnsbl count hash pointer
694             returns: html text buffer
695              
696             A one line example corresponding to the text line above:
697              
698             34 77.3% dnsbl.sorbs.net
699              
700             with a 'comment' key of: 127.0.0.2,5,7,8,9,10,12
701             and a 'url' key of: http://www.au.sorbs.net/using.shtml
702              
703            
34
704             77.3%
705            
706             href="http://www.au.sorbs.net/using.shtml">dnsbl.sorbs.net
707             127.0.0.2,5,7,8,9,10,12
708            
709              
710             =cut
711              
712             sub htmltxt {
713 1     1 1 1961 my($conf,$dnsbls) = @_;
714 1 50 33     10 return "\n"
715             unless keys %$dnsbls && $dnsbls->{TOTAL};
716 1         3 my $html = '';
717 1         22 my $tot = $dnsbls->{TOTAL}/100;
718 1         3 my $len = length($dnsbls->{TOTAL});
719 1         6 foreach(sort{$dnsbls->{"$b"} <=> $dnsbls->{"$a"}} keys %$dnsbls) {
  32         54  
720 12         22 my($comment,$url) = cmurl($conf,$_,1);
721 12 100       26 my $aa = $url ? '' : '';
722 12         24 my $count = $dnsbls->{"$_"};
723 12         94 $html .= '
'.
724             $count .''.
725             sprintf("%.1f",$count/$tot) .'%'.
726             $url . $_ . $aa .''. $comment .'
727             }
728 1         6 return $html;
729             }
730              
731             =item * $text = plainresp(\%response);
732              
733             Generate a plain text report of the form:
734              
735             comment string 1
736             ipaddr1 response code dnsbl
737             response code dnsbl
738             response code dnsbl
739             ipaddr2 response code dnsbl
740              
741             comment string 2
742             ipaddr3 response code dnsbl
743              
744             etc...
745            
746             input: response hash pointer
747             returns: text buffer
748              
749             =cut
750              
751             sub plainresp {
752 1     1 1 63 my($r) = @_;
753 1 50       8 return "# RESPONSE list is empty\n"
754             unless keys %$r;
755 1         3 my $txt = '';
756 1         3 my $ck = 'tHiSrAnDoMsTrInG'; # comment key
757 1         2 my($cmt,$ip,$pad);
758 1 50       10 foreach $ip (sort {
  14         238  
759             $r->{"$a"}->{COMMENT} cmp $r->{"$b"}->{COMMENT}
760             ||
761             inet_aton($a) cmp inet_aton($b)
762             } keys %$r) {
763            
764 7 100       86 unless ($ck eq $r->{"$ip"}->{COMMENT}) {
765 4         9 $cmt = $ck = $r->{"$ip"}->{COMMENT};
766 4         11 $txt .= "\n$cmt\n";
767             }
768 7         13 $txt .= " $ip\t";
769 7 50       19 $txt .= "\t" if length("$ip") < 12;
770 7         7 $pad = '';
771 7         9 foreach (sort keys %{$r->{"$ip"}}) {
  7         30  
772 22 100       44 next if $_ eq 'COMMENT';
773 15         27 my $rc = $r->{"$ip"}->{"$_"};
774 15         23 $txt .= $pad . $rc ."\t";
775 15 50       28 $txt .= "\t" if length($rc) < 8;
776 15         21 $txt .= $_ ."\n";
777 15         32 $pad = "\t\t\t";
778             }
779             }
780 1         29 return $txt;
781             }
782              
783             =item * $html = htmlresp(\%config,\%response);
784              
785             Generate a report as above but with EtrEEtdEE/tdEE/trE table markup. The
786             EtableEE/tableE tags are not generated.
787              
788             input: config hash pointer
789             response hash pointer
790             returns: html text buffer
791              
792             A one line example corresponding to the text line above:
793              
794             34 77.3% dnsbl.sorbs.net
795              
796             with a 'comment' key of: 127.0.0.2,5,7,8,9,10,12
797             and a 'url' key of: http://www.au.sorbs.net/using.shtml
798              
799            
34
800             77.3%
801            
802             href="http://www.au.sorbs.net/using.shtml">dnsbl.sorbs.net
803             127.0.0.2,5,7,8,9,10,12
804            
805              
806             =back
807              
808             =cut
809              
810             sub htmlresp {
811 1     1 1 317 my($conf,$r) = @_;
812 1 50       5 return "# RESPONSE list is empty\n"
813             unless keys %$r;
814 1         2 my $txt = '';
815 1         2 my $ck = 'ThIsRaNdOmStRiNg'; # comment key
816 1         2 my $tbl = '';
817 1         2 my $rs = ''; # non numeric 1st time
818 1         2 my($cmt,$ip,$tr);
819 1 50       6 foreach $ip (sort {
  14         189  
820             $r->{"$a"}->{COMMENT} cmp $r->{"$b"}->{COMMENT}
821             ||
822             inet_aton($a) cmp inet_aton($b)
823             } keys %$r) {
824 7 100       87 if ($ck eq $r->{"$ip"}->{COMMENT}) {
825             ;
826             } else {
827 4         18 $txt .= $rs . $tbl . "\n";
828 4         10 $ck = $r->{"$ip"}->{COMMENT};
829 4   100     77 $cmt = $ck || ' ';
830 4         7 $txt .= '
833 4         6 $rs = 0;
834             }
835            
836 7         8 my $pad = (keys %{$r->{"$ip"}}) -1;
  7         24  
837 7         17 $pad = ''. $ip .'
838 7         9 foreach (sort keys %{$r->{"$ip"}}) {
  7         29  
839 22 100       44 next if $_ eq 'COMMENT';
840 15         15 ++$rs;
841 15         30 my $rtc = $r->{"$ip"}->{"$_"};
842 15         31 my($comment,$url) = cmurl($conf,$_,1);
843 15 100       30 my $aa = $url ? '' : '';
844 15         37 $tbl .= $tr . $pad .''. $rtc .''. $url . $_ . $aa ."
845 15         21 $pad = '';
846 15         37 $tr = '
847             }
848             }
849            
850 1         6 return $txt . $rs . $tbl;
851             }
852              
853             =head1 Monitor Web Page HOWTO
854              
855             Read the C document that describes the scripts used with
856             'cron' to auto generate web pages for the statistics reports
857              
858             =head1 EXPORT_OK
859              
860             run
861             plaintxt
862             htmltxt
863             plainresp
864             htmlresp
865              
866             =head1 AUTHOR
867              
868             Michael Robinton, michael@bizsystems.com
869              
870             =head1 COPYRIGHT
871              
872             Copyright 2008-2014, Michael Robinton.
873             This program is free software; you can redistribute it and/or modify
874             it under the terms of the GNU General Public License as published by
875             the Free Software Foundation; either version 2 of the License, or
876             (at your option) any later version.
877              
878             This program is distributed in the hope that it will be useful,
879             but WITHOUT ANY WARRANTY; without even the implied warranty of
880             MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
881             GNU General Public License for more details.
882              
883             You should have received a copy of the GNU General Public License
884             along with this program; if not, write to the Free Software
885             Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
886              
887             =head1 SEE ALSO
888              
889             L,
890             L,
891             L,
892             L
893             L
894              
895             =cut
896              
897             1;