File Coverage

blib/lib/Net/DNSBL/Statistics.pm
Criterion Covered Total %
statement 245 249 98.3
branch 120 144 83.3
condition 58 104 55.7
subroutine 15 16 93.7
pod 3 8 37.5
total 441 521 84.6


'."\n";
line stmt bran cond sub pod time code
1             #!/usr/bin/perl
2             package Net::DNSBL::Statistics;
3              
4 6     6   35824 use strict;
  6         11  
  6         240  
5             #use diagnostics;
6 6     6   3026 use Net::DNS::Codes qw(:all);
  6         7950  
  6         4858  
7 6         496 use Net::DNS::ToolKit qw(
8             newhead
9             gethead
10             inet_ntoa
11 6     6   3514 );
  6         84394  
12 6     6   3498 use Net::DNS::ToolKit::RR;
  6         18200  
  6         192  
13 6         431 use Net::DNS::ToolKit::Utilities qw(
14             id
15             revIP
16 6     6   3000 );
  6         115726  
17 6         498 use Net::DNSBL::Utilities qw(
18             DO
19             list2NetAddr
20             matchNetAddr
21 6     6   4010 );
  6         14723  
22             #use Net::DNS::ToolKit::Debug qw(
23             # print_head
24             # print_buf
25             #);
26              
27 6         16616 use vars qw(
28             $VERSION @ISA @EXPORT_OK
29 6     6   31 );
  6         10  
30             require Exporter;
31             @ISA = qw(Exporter);
32              
33             $VERSION = do { my @r = (q$Revision: 0.14 $ =~ /\d+/g); sprintf "%d."."%02d" x $#r, @r };
34              
35             @EXPORT_OK = qw(
36             run
37             plaintxt
38             htmltxt
39             );
40              
41             # undocumented $DEBUG values
42             #
43             # 1 => return @ip array
44             # 2 => return %dnsbls initialization hash
45             # 3 => return ($iptr,$regexptr) ignore, regexp ptrs
46             # 4 => return %ips union aging hash
47             # 5 => return %qc hash
48             # other => return un-converted %dnsbls hash
49              
50             =head1 NAME
51              
52             Net::DNSBL::Statistics - gather DNSBL Statistics
53              
54             =head1 SYNOPSIS
55              
56             use Net::DNSBL::Statistics qw(
57             run
58             plaintxt
59             htmltxt
60             );
61              
62             %dnsblcounts=run(\%config,$NonBlockSock,$sockaddr_in);
63             $text = plaintxt(\%config,\%dnsblscounts);
64             $html = htmltxt(\%config,\%dnsblcounts);
65              
66             =head1 DESCRIPTION
67              
68             B is the Perl module that collects statistics on the
69             interrogation success for a list of IP addresses against a list of DNSBL's.
70             The module is used to implement the reproting script B.
71              
72             =head1 CONFIGURATION FILE
73              
74             With the addition of a few elements, the configuration file for B
75             shares a common format with the Mail::SpamCannibal sc_BLcheck.pl script,
76             facilitating common maintenance of DNSBL's for your MTA installation.
77              
78             The sample configuration file
79             B is heavily commented with the details for each
80             configuration element.
81              
82             =head1 SYSTEM SIGNALS
83              
84             B responds to the following system signals:
85              
86             =over 2
87              
88             =item * TERM
89              
90             Script is terminated.
91              
92             =back
93              
94             =head1 PERL MODULE DESCRIPTION - Script Implementation
95              
96             B provides most of the functions that implement
97             B which is a script that collects statistics from a list of IP
98             address interrogations against a list of DNSBL's
99              
100             =head1 dnsblstat usage
101              
102             How to use B
103              
104             Syntax: dnsblstat path/to/config.file
105             or
106             dnsblstat -t path/to/config.file
107             dnsblstat -w path/to/config.file
108              
109             Normally dnsblstat prints a sorted list (by count)
110             of the DNSBL's interrogated with their reply count,
111             percentage of the total count, and any comments from
112             the DNSBL's 'comment' key field in the config file.
113             The 'comment' field may contain html markup text.
114              
115             i.e.
116             44 100.0% TOTAL IP's interrogated
117             41 93.2% UNION of all results
118             34 77.3% dnsbl.sorbs.net comment
119             ........
120              
121             The -t switch will print a start and stop time.
122              
123             i.e.
124             # start: Fri Jan 4 17:46:44 2008
125             # stop : Fri Jan 4 17:58:21 2008
126              
127             The -w switch will put the output into an HTML table
128             without the EtableE statement E/tableE>., a commment as above
129             and with an Ea href="..."Ednsbl nameE/aE statement replacing
130             the dnsbl name if the 'url' key is present in the config file.
131              
132             i.e.
133             A one line example corresponding to the text line above:
134              
135             34 77.3% dnsbl.sorbs.net
136              
137             with a 'comment' key of: 127.0.0.2,5,7,8,9,10,12
138             and a 'url' key of: http://www.au.sorbs.net/using.shtml
139              
140            
34
141             77.3%
142            
143             href="http://www.au.sorbs.net/using.shtml">dnsbl.sorbs.net
144             127.0.0.2,5,7,8,9,10,12
145            
146              
147             =head1 Net::DNSBL::Statistics FUNCTIONS
148              
149             =over 4
150              
151             =item * %dnsblscounts=run(\%config,$NonBlockSock,$sockaddr_in);
152              
153             Returns the total number of IP's interrogated (IP list less white listed items) and a hash of DNSBL
154             names and their respective SPAM reply counts or equivalent for 'in-addr.arpa' and GENERIC.
155              
156             input: config pointer,
157             non-blocking socket descriptor,
158             sockaddr_in for local DNS host
159              
160             returns: dnsbl count hash
161              
162             The dnsbl count hash will have two added keys:
163              
164             TOTAL the total number of interrogations less whitelist
165             UNION the total number of spam 'hits'
166              
167             HINTs: use Net::NBsocket qw( open_udbNB sockaddr_in );
168             use Net::DNS::ToolKit qw( get_ns );
169              
170             my $sock = open_udpNB();
171             my $sockaddr_in = sockaddr_in(53, scalar get_ns());
172              
173             =cut
174              
175             my $w = 0;
176             my @w = qw( \ | / - );
177             sub whirl {
178 259     259 0 1383 return;
179 0         0 print STDERR "\r",$w[$w],"\r";
180 0 0       0 $w = 0 if ++$w > $#w;
181             }
182              
183             sub run {
184 13     13 1 110833 my($conf,$Usock,$U_Sin,$DEBUG) = @_;
185 13         29 my %ips;
186 13 100       64 return () unless $conf->{FILES};
187 12 100       75 my @files = (ref $conf->{FILES}) ? @{$conf->{FILES}} : ($conf->{FILES});
  3         8  
188              
189 12         51 local *F;
190 12         36 foreach (@files) {
191 15 100 66     630 next unless -e $_ && open F, $_;
192 10         297 foreach () {
193 167 50       361 next unless $_ =~ /\S/;
194 167 100       281 next if $_ =~ /^\s*#/;
195 155 100       354 next unless $_ =~ /(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})/;
196 149         298 $ips{"$1"} = undef;
197             }
198 10         110 close F;
199             }
200 12         86 my @ips = keys %ips;
201 12         40 %ips = ();
202              
203 12 100 100     103 return @ips if $DEBUG && $DEBUG == 1;
204              
205 9         20 my @NAignor;
206 9 50       36 if ($conf->{IGNORE}) {
207 9         375 list2NetAddr($conf->{IGNORE},\@NAignor);
208             }
209              
210             ############## configure %dnsbl has for accumulating stats ###############
211 9   66     32810 my @DNSBLs = grep( $_ =~ /^[0-9a-z]+\.[0-9a-z]/i && $_ !~ /in-addr/i, keys %{$conf});
  9         269  
212              
213 9         35 my %dnsbls;
214              
215 9         55 foreach(@DNSBLs) {
216 35         115 $dnsbls{"$_"} = {
217             C => 0, # count
218             TO => 0, # timeouts
219             };
220             }
221              
222             #### %dnsbls configuration complete, configure maximum union timeout
223 9         80 my $uto = 0;
224 9         33 foreach(keys %dnsbls) {
225 35 50 33     211 next unless exists $conf->{"$_"} &&
226             exists $conf->{"$_"}->{timeout};
227 35 100       94 next if $conf->{"$_"}->{timeout} < $uto;
228 29         90 $uto = $conf->{"$_"}->{timeout};
229             }
230              
231 9         20 my($iptr,$regexptr);
232 9         15 my $needPTR = 0;
233 9 100       33 if ($conf->{'in-addr.arpa'}) {
234 5         20 $dnsbls{'in-addr.arpa'} = { C => 0 };
235 5   50     25 $needPTR = $conf->{'in-addr.arpa'}->{timeout} || 30;
236             }
237 9 50       35 if ($conf->{GENERIC}) {
238 9         27 $dnsbls{GENERIC} = { C => 0 };
239 9 100 50     43 $needPTR = ($conf->{GENERIC}->{timeout} || 30)
240             unless $needPTR;
241 9 50 33     108 undef $regexptr unless ($regexptr = $conf->{GENERIC}->{regexp}) &&
      33        
242             ref $regexptr eq 'ARRAY' && @$regexptr > 0;
243 9 50 33     105 undef $iptr unless ($iptr = $conf->{GENERIC}->{ignore}) &&
      33        
244             ref $iptr eq 'ARRAY' && @$iptr > 0;
245             }
246             ### adjust $uto to account for generic retries and in-addr.arpa timeouts
247 9 50       26 $uto = $needPTR
248             if $uto < $needPTR;
249 9 50       23 $uto = 30 unless $uto;
250             #### maximum $uto = 2x max delay + a little
251 9         18 $uto *= 2;
252 9         14 $uto += 5;
253              
254 9 100 100     63 return %dnsbls if $DEBUG && $DEBUG == 2;
255              
256 8 100 100     48 return ($iptr,$regexptr) if $DEBUG && $DEBUG == 3;
257              
258 7         48 my %qc = (
259             'in-addr' => 0,
260             'regular' => 0,
261             # retries below
262             'generic' => 0,
263             'retry-r' => 0,
264             );
265              
266 7         13 my %queue;
267 7         28 my $fileno = fileno($Usock);
268 7         20 my $vin = '';
269 7         43 vec($vin,$fileno,1) = 1;
270 7         14 my $Run = 1;
271 7     0   93 local $SIG{TERM} = sub {$Run = 0};
  0         0  
272              
273 7         16 my $qsize = keys %dnsbls;
274 7         13 my $then = time;
275 7         8 my $uage = $then; # union aging every 5 seconds
276 7         100 my ($get,$put,$parse) = new Net::DNS::ToolKit::RR;
277 7         164 my($rin,$rout,$win,$wout,$ip,$packet,$name,$id,$msg,$bl,$now,$revIP,$nfound,$answer,$ttl,$rdl,@rdata,@sndQ);
278              
279             LOOP:
280 7         21 while ($Run) {
281 349         4151 my $Q = keys %queue;
282 349 100 100     1609 if ($qsize > $Q && ($ip = shift @ips)) { # run results for next IP if queue is not double full
283 70 100       2729 next if matchNetAddr($ip,\@NAignor);
284 56         15793 ++$dnsbls{TOTAL}->{C};
285 56         88 $now = time;
286 56         1931 $revIP = revIP($ip);
287 56 50       1244 if ($needPTR) {
288 56         165 $id = makid(\%queue);
289 56         108 $name = $revIP .'.in-addr.arpa';
290 56         228 $packet = makequery($put,$id,$name,T_PTR());
291 56         500 $queue{$id} = {
292             B => 'in-addr.arpa',
293             Q => $packet,
294             T => $now + $needPTR, # timeout
295             R => 0, # retry
296             X => $revIP,
297             };
298 56         150 push @sndQ, $packet;
299 56         99 ++$qc{'in-addr'};
300             }
301            
302 56         113 foreach $bl (@DNSBLs) {
303 168 100       491 next if $dnsbls{"$bl"}->{TO} > 5; # ignore this BL if it timed out to many times
304 162         328 $id = makid(\%queue);
305 162         308 $name = $revIP .'.'. $bl;
306 162         432 $packet = makequery($put,$id,$name,T_A());
307 162   50     1235 $queue{$id} = {
308             B => "$bl",
309             Q => $packet,
310             T => $now + ($conf->{"$bl"}->{timeout} || 30),
311             R => 0,
312             X => $revIP,
313             };
314 162         229 push @sndQ, $packet;
315 162         310 ++$qc{regular};
316             }
317             }
318              
319             # wait for some responses
320 335         940 $rin = $vin;
321 335 100       849 if (@sndQ) {
322 74         101 $win = $vin;
323             } else {
324 261         470 $win = '';
325             }
326 335         77103721 $nfound = select($rout=$rin,$wout=$win,undef,0.5); # tick each second
327 335 100       1370 if ($nfound > 0) {
328 181   100     867 while (vec($wout,$fileno,1) && @sndQ) {
329 300         470 $packet = shift @sndQ;
330             #print STDERR "WRITE\n";
331             #print_buf(\$packet);
332             #print STDERR "\n";
333 300         59941 send($Usock,$packet,0,$U_Sin);
334 300 100       1527 whirl() if $DEBUG;
335             }
336 181 100       442 if (vec($rout,$fileno,1)) {
337 133         212 undef $msg;
338 133 50       896 next unless recv($Usock,$msg,,PACKETSZ,0); # ignore receive errors
339 133 50       1663 next unless length($msg) > HFIXEDSZ; # ignore short packets
340             #print STDERR "RECEIVE\n";
341             #print_buf(\$msg);
342             #print STDERR "\n";
343 133         551 my($off,$qr,$opcode,$aa,$tc,$rd,$ra,$mbz,$ad,$cd,$rcode,$qdcount,$ancount,$nscount,$arcount);
344 133         694 ($off,$id,$qr,$opcode,$aa,$tc,$rd,$ra,$mbz,$ad,$cd,$rcode,$qdcount,$ancount,$nscount,$arcount)= gethead(\$msg);
345             next unless
346 133 50 33     880 $tc == 0 &&
      33        
      66        
      33        
      33        
      33        
347             $qr == 1 &&
348             $opcode == QUERY &&
349             ($rcode == NOERROR || $rcode == NXDOMAIN || $rcode == SERVFAIL) &&
350             $qdcount == 1 &&
351             exists $queue{$id};
352              
353 133         2655 ($off,my($name,$t,$class)) = $get->Question(\$msg,$off);
354 133 50       1633 next unless $class == C_IN; # not my question
355              
356 133         641 $bl = $queue{$id}->{B};
357 133         225 $revIP = $queue{$id}->{X};
358 133         356 delete $queue{$id};
359 133 100       476 $dnsbls{"$bl"}->{TO} = 0 # reset timeout count
360             unless $bl eq 'in-addr.arpa';
361 133 100 66     495 if ($ancount && $rcode == &NOERROR) { # if good response
    100 33        
    100 66        
362 84         693 $name =~ /(?:\d+\.\d+\.\d+\.\d+\.)/i;
363 84 50 66     456 next unless lc $bl eq lc $' &&
      33        
364             ($t == T_A || $t == T_PTR);
365              
366 84         514 undef $answer;
367 84         85 my @generic;
368             ANSWER:
369 84         225 foreach(0..$ancount-1) {
370 126         489 ($off,$name,$t,$class,$ttl,$rdl,@rdata) = $get->next(\$msg,$off);
371 126 100 33     12678 if ($t == T_A) {
    50 33        
372 77         392 while($answer = shift @rdata) {
373 77         2581 $ip = inet_ntoa($answer);
374 77 100       1537 if (grep($ip eq $_,keys %{$conf->{"$bl"}->{accept}})) {
  77         622  
375 35         92 ++$dnsbls{"$bl"}->{C}; # bump dnsbl count
376 35         135 union(\%dnsbls,\%ips,$revIP,$now + $uto);
377             #print STDERR "FAILED $name $ip\n";
378 35         162 next LOOP;
379             }
380             }
381             }
382             elsif ($needPTR && $t == T_PTR && exists $dnsbls{GENERIC}) {
383             # positive in-addr.arpa responses are ignored
384 49         678 push @generic, $rdata[0];
385 49         143 next ANSWER;
386             }
387             }
388 49 100       170 if (@generic) {
389 35         66 foreach my $g (@generic) {
390 49 100 66     1270 next LOOP if $iptr && grep($g =~ /$_/i, @$iptr);
391 35 100 66     4228 next LOOP if $g && ! grep($g =~ /$_/i, @$regexptr);
392             }
393 14         50 ++$dnsbls{GENERIC}->{C}; # bump GENERIC count
394 14         60 union(\%dnsbls,\%ips,$revIP,$now + $uto);
395             #print STDERR "FAILED $name GENERIC\n";
396             }
397             }
398             elsif ($t == T_A) {
399 35         225 next LOOP; # tis a lookup failure or no response... ignore
400             }
401             elsif ($needPTR && $t == T_PTR && exists $conf->{'in-addr.arpa'}) {
402 6         112 ++$dnsbls{'in-addr.arpa'}->{C};
403 6         31 union(\%dnsbls,\%ips,$revIP,$now + $uto);
404             #print STDERR "FAILED $name ERROR\n";
405 6         26 next LOOP;
406             }
407             }
408             }
409             ######################################################################
410             else { # timeout
411 154         469 $now = time;
412 154 100       1126 next unless $now > $then;
413 77         157 $then = $now;
414 77         852 my @queue = sort {$queue{$a}->{T} <=> $queue{$b}->{T}} keys %queue;
  448         1273  
415 77         287 foreach $id (@queue) { # check for DNSBL timeouts
416 204 100       551 last if $now < $queue{$id}->{T};
417 167         388 $bl = $queue{$id}->{B};
418 167 100       377 if ($bl eq 'in-addr.arpa') {
419 11 100       63 if (exists $conf->{'in-addr.arpa'}) {
    50          
420 3         10 $revIP = $queue{$id}->{X};
421 3         17 delete $queue{$id};
422 3         10 ++$dnsbls{'in-addr.arpa'}->{C};
423 3         18 union(\%dnsbls,\%ips,$revIP,$now + $uto);
424             #print STDERR "FAILED $revIP.in-addr.arpa timeout\n";
425             }
426             elsif (exists $dnsbls{GENERIC}) {
427 8 100       35 unless ($queue{$id}->{R}) {
428 4         11 $queue{$id}->{R} = 1; # retry generic queries
429 4         8 $queue{$id}->{T} = $now + $needPTR;
430 4         11 push @sndQ, $queue{$id}->{Q};
431 4         14 ++$qc{generic};
432             }
433             else {
434 4         26 delete $queue{$id};
435             }
436             }
437             }
438             else { # regular DNSBL
439 156 100       353 unless ($queue{$id}->{R}) {
440 78         96 $queue{$id}->{R} = 1;
441 78   50     298 $queue{$id}->{T} = $now + ($conf->{"$bl"}->{timeout} || 30);
442 78         210 push @sndQ, $queue{$id}->{Q};
443 78         166 ++$qc{'retry-r'};
444             }
445             else {
446 78         150 $revIP = $queue{$id}->{X};
447 78         303 delete $queue{$id};
448 78         258 ++$dnsbls{"$bl"}->{TO};
449             }
450             }
451             }
452 77 100 100     477 last LOOP unless @ips || keys %queue; # run through all IP's and remaining queue items
453 70 100       476 next unless $uage < $now;
454 14         32 $uage = $now + 5;
455 14         63 @_ = sort {$ips{"$a"} <=> $ips{"$b"}} keys %ips;
  36         84  
456 14         44 foreach (@_) {
457 12 50       177 last if $ips{"$_"} > $now;
458 0         0 delete $ips{"$_"};
459             }
460             } # else nfound
461             } # while ($Run)
462              
463 7 100       52 close $Usock unless $DEBUG;
464              
465 7 100       27 if ($DEBUG) {
466 6 100       121 return %ips if $DEBUG == 4;
467 4 100       126 return %qc if $DEBUG == 5;
468 2         121 return %dnsbls; # for any other debug value
469             }
470             else {
471 1         9 foreach(keys %dnsbls) {
472 6         21 $dnsbls{$_} = $dnsbls{$_}->{C};
473             }
474             }
475 1         54 return %dnsbls;
476             }
477              
478             sub makequery {
479 218     218 0 873 my($put,$id,$name,$type) = @_;
480 218         239 my $buf;
481 218         527 my $off = newhead(\$buf,
482             $id,
483             BITS_QUERY | RD,
484             1,0,0,0,
485             );
486 218         9698 $off = $put->Question(\$buf,$off,$name,$type,C_IN);
487 218         4846 return $buf;
488             }
489              
490             sub makid {
491 218     218 0 263 my $qp = shift;
492 218         194 my $id;
493 218         209 do {
494 218         6921 $id = id()
495             } while exists $qp->{$id};
496 218         1859 return $id;
497             }
498              
499             sub union {
500 58     58 0 104 my($dnsbls,$union,$rip,$expire) = @_;
501 58         68 $expire += 30; # union cache expiration is alway longer than timeouts
502 58 100       172 if (exists $union->{"$rip"}) {
503 24 50       97 $union->{"$rip"} = $expire
504             if $expire > $union->{"$rip"};
505             } else {
506 34         95 $union->{"$rip"} = $expire;
507 34         149 ++$dnsbls->{UNION}->{C};
508             }
509             }
510              
511             =item * $text = plaintxt($config,\%dnsbls);
512              
513             Generate a plain text report of the form:
514              
515             44 100.0% TOTAL IP's interrogated
516             41 93.2% UNION of all results
517             34 77.3% dnsbl.sorbs.net comment
518             22 50.0% GENERIC comment
519             13 29.5% in-addr.arpa comment
520             11 25.0% cbl.abuseat.org comment
521             9 20.5% list.dsbl.org comment
522             2 4.5% dnsbl.njabl.org comment
523             1 2.3% bl.spamcannibal.org comment
524             0 0.0% dynablock.njabl.org comment
525              
526             input: configuration pointer,
527             dnsbl count hash pointer
528             returns: text buffer
529              
530             The 'comment' comes from the config file 'comment' key field for each
531             specified DNSBL or is blank if there is no 'comment' key.
532              
533             =cut
534              
535             # return 'comment' and 'url' if present
536             # input: $conf, $bl, $nbsp
537             # output: if $nbsp ($comment,$url)
538             # if !$nbsp $comment
539             #
540             sub cmurl {
541 24     24 0 21 my($conf,$bl,$nbsp) = @_;
542 24 100       41 if ($bl eq 'TOTAL') {
    100          
543 2 100       3 if ($nbsp) {
544 1         3 return (q|IP's interrogated|,'');
545             } else {
546 1         4 return q|IP's interrogated|;
547             }
548             } elsif ($bl eq 'UNION') {
549 2 100       4 if ($nbsp) {
550 1         2 return (q|of all results|,'');
551             } else {
552 1         3 return q|of all results|;
553             }
554             }
555 20 100 66     66 my $comment = (exists $conf->{"$bl"}->{comment} && $conf->{"$bl"}->{comment})
    100          
556             ? $conf->{"$bl"}->{comment}
557             : ($nbsp) ? ' ' : '';
558 20 100       30 return $comment unless $nbsp;
559 10 100 66     27 my $url = (exists $conf->{"$bl"}->{url} && $conf->{"$bl"}->{url})
560             ? q|{url} .q|">|
561             : '';
562 10         18 return ($comment,$url);
563             }
564            
565             sub plaintxt {
566 1     1 1 45 my($conf,$dnsbls) = @_;
567 1 50 33     10 return "# ERROR list is empty\n"
568             unless keys %$dnsbls && $dnsbls->{TOTAL};
569 1         1 my $txt = '';
570 1         3 my $tot = $dnsbls->{TOTAL}/100;
571 1         2 my $len = length($dnsbls->{TOTAL});
572 1 50       6 foreach(sort{$dnsbls->{"$b"} <=> $dnsbls->{"$a"}
  31         54  
573             ||
574             $a cmp $b } keys %$dnsbls) {
575 12         17 my $comment = cmurl($conf,$_);
576 12         67 $txt .= sprintf("% ${len}u% 6.1f%% %s %s\n",$dnsbls->{"$_"},$dnsbls->{"$_"}/$tot,$_,$comment);
577             }
578 1         4 return $txt;
579             }
580              
581             =item * $html = htmltxt($config,\%dnsbls);
582              
583             Generate a report as above but with EtrEEtdEE/tdEE/trE table markup. The
584             EtableEE/tableE tags are not generated. If there is a 'url' key field in the
585             respective DNSBL config entry, the DNSBL name is provide with Ea href="url"EDNSBLE/aE
586             tags with the specified 'url' as the 'href' value.
587              
588             input: configuration pointer,
589             dnsbl count hash pointer
590             returns: html text buffer
591              
592             A one line example corresponding to the text line above:
593              
594             34 77.3% dnsbl.sorbs.net
595              
596             with a 'comment' key of: 127.0.0.2,5,7,8,9,10,12
597             and a 'url' key of: http://www.au.sorbs.net/using.shtml
598              
599            
34
600             77.3%
601            
602             href="http://www.au.sorbs.net/using.shtml">dnsbl.sorbs.net
603             127.0.0.2,5,7,8,9,10,12
604            
605              
606             =back
607              
608             =cut
609              
610             sub htmltxt {
611 1     1 1 147 my($conf,$dnsbls) = @_;
612 1 50 33     9 return "\n"
613             unless keys %$dnsbls && $dnsbls->{TOTAL};
614 1         2 my $html = '';
615 1         2 my $tot = $dnsbls->{TOTAL}/100;
616 1         3 my $len = length($dnsbls->{TOTAL});
617 1 50       5 foreach(sort{$dnsbls->{"$b"} <=> $dnsbls->{"$a"}
  31         65  
618             ||
619             $a cmp $b } keys %$dnsbls) {
620 12         14 my($comment,$url) = cmurl($conf,$_,1);
621 12 100       16 my $aa = $url ? '' : '';
622 12         12 my $count = $dnsbls->{"$_"};
623 12         62 $html .= '
'.
624             $count .''.
625             sprintf("%.1f",$count/$tot) .'%'.
626             $url . $_ . $aa .''. $comment .'
627             }
628 1         7 return $html;
629             }
630              
631             =head1 Statistics Web Page HOWTO
632              
633             Read the C document that describes the scripts used with
634             'cron' to auto generate web pages for the statistics reports
635              
636             =head1 EXPORT_OK
637              
638             run
639             plaintxt
640             htmltxt
641              
642             =head1 AUTHOR
643              
644             Michael Robinton, michael@bizsystems.com
645              
646             =head1 COPYRIGHT
647              
648             Copyright 2008-2014, Michael Robinton.
649             This program is free software; you can redistribute it and/or modify
650             it under the terms of the GNU General Public License as published by
651             the Free Software Foundation; either version 2 of the License, or
652             (at your option) any later version.
653              
654             This program is distributed in the hope that it will be useful,
655             but WITHOUT ANY WARRANTY; without even the implied warranty of
656             MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
657             GNU General Public License for more details.
658              
659             You should have received a copy of the GNU General Public License
660             along with this program; if not, write to the Free Software
661             Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
662              
663             =head1 SEE ALSO
664              
665             L,
666             L,
667             L,
668             L
669              
670             =cut
671              
672             1;