fix is_ipaddr? change pc92 A/D default
[spider.git] / perl / DXCIDR.pm
1 #
2 # IP Address block list / checker
3 #
4 # This is a DXSpider compatible, optional skin over Net::CIDR::Lite
5 # If Net::CIDR::Lite is not present, then a find will always returns 0
6 #
7
8 package DXCIDR;
9
10 use strict;
11 use warnings;
12 use 5.16.1;
13 use DXVars;
14 use DXDebug;
15 use DXUtil;
16 use DXLog;
17 use IO::File;
18 use File::Copy;
19
20 use Socket qw(AF_INET AF_INET6 inet_pton inet_ntop);
21
22 our $active = 0;
23 our $badipfn = "badip";
24 my $ipv4;
25 my $ipv6;
26 my $count4 = 0;
27 my $count6 = 0;
28
29 sub _fn
30 {
31         return localdata($badipfn);
32 }
33
34 sub _read
35 {
36         my $suffix = shift;
37         my $fn = _fn();
38         $fn .= ".$suffix" if $suffix;
39         my $fh = IO::File->new($fn);
40         my @out;
41         my $ecount;
42         my $line;
43         
44
45         if ($fh) {
46                 while (<$fh>) {
47                         chomp;
48                         ++$line;
49                         next if /^\s*\#/;
50                         next unless /[\.:]/;
51                         next unless $_;
52                         unless (is_ipaddr($_)) {
53                                 ++$ecount;
54                                 LogDbg('err', qq(DXCIDR: $fn line $line: '$_' not an ip address));
55                                 if ($ecount > 10) {
56                                         LogDbg('err', qq(DXCIDR: More than 10 errors in $fn at/after line $line: '$_' - INVALID INPUT FILE));
57                                         return ();
58                                 }
59                         }
60                         push @out, $_;
61                 }
62                 $fh->close;
63         } else {
64                 LogDbg('err', "DXCIDR: $fn read error ($!)");
65         }
66         return @out;
67 }
68
69 sub _load
70 {
71         return unless $active;
72         my $suffix = shift;
73         my @in = _read($suffix);
74         return 0 unless @in;
75         return scalar add(@in);
76 }
77
78 sub _put
79 {
80         my $suffix = shift;
81         my $fn = _fn() . ".$suffix";
82         my $r = rand;
83         my $fh = IO::File->new (">$fn.$r");
84         my $count = 0;
85         if ($fh) {
86                 for ($ipv4->list, $ipv6->list) {
87                         $fh->print("$_\n");
88                         ++$count;
89                 }
90                 move "$fn.$r", $fn;
91                 LogDbg('cmd', "DXCIDR: put (re-)written $fn");
92         } else {
93                 LogDbg('err', "DXCIDR: cannot write $fn.$r $!");
94         }
95         return $count;
96 }
97
98 sub append
99 {
100         return 0 unless $active;
101         
102         my $suffix = shift;
103         my @in = @_;
104         my @out;
105         
106         if ($suffix) {
107                 my $fn = _fn() . ".$suffix";
108                 my $fh = IO::File->new;
109                 if ($fh->open("$fn", "a+")) {
110                         $fh->seek(0, 2);        # belt and braces !!
111                         print $fh "$_\n" for @in;
112                         $fh->close;
113                 } else {
114                         LogDbg('err', "DXCIDR::append error appending to $fn $!");
115                 }
116         } else {
117                 LogDbg('err', "DXCIDR::append require badip suffix");
118         }
119         return scalar @in;
120 }
121
122 sub add
123 {
124         return 0 unless $active;
125         my $count = 0;
126         my @out;
127         
128         for my $ip (@_) {
129                 # protect against stupid or malicious
130                 next unless is_ipaddr($ip);
131                 next if $ip =~ /^127\./;
132                 next if $ip =~ /^::1$/;
133                 next if find($ip);
134                 if ($ip =~ /\./) {
135                         eval {$ipv4->add_any($ip)};
136                         if ($@) {
137                                 push @out, $@;
138                         } else {
139                                 ++$count;
140                                 ++$count4;
141                         }
142                 } elsif ($ip =~ /:/) {
143                         eval {$ipv6->add_any($ip)};
144                         if ($@) {
145                                 push @out, $@;
146                         } else {
147                                 ++$count;
148                                 ++$count6;
149                         }
150                 } else {
151                         LogDbg('err', "DXCIDR::add non-ip address '$ip' read");
152                 }
153         }
154         return $count;
155 }
156
157 sub clean_prep
158 {
159         return unless $active;
160
161         if ($ipv4 && $count4) {
162                 $ipv4->clean;
163                 $ipv4->prep_find;
164         }
165         if ($ipv6 && $count6) {
166                 $ipv6->clean;
167                 $ipv6->prep_find;
168         }
169 }
170
171 sub _sort
172 {
173         my @in;
174         my @out;
175         for (@_) {
176                 my @ip = split m|/|;
177                 push @in, [inet_pton(m|:|?AF_INET6:AF_INET, $ip[0]), @ip];
178         }
179         @out = sort {$a->[0] <=> $b->[0]} @in;
180         return map { "$_->[1]/$_->[2]"} @out;
181 }
182
183 sub list
184 {
185         return () unless $active;
186         my @out;
187         push @out, $ipv4->list if $count4;
188         push @out, $ipv6->list if $count6;
189         return _sort(@out);
190 }
191
192 sub find
193 {
194         return 0 unless $active;
195         return 0 unless $_[0];
196
197         if ($_[0] =~ /\./) {
198                 return $ipv4->find($_[0]) if $count4;
199         }
200         return $ipv6->find($_[0]) if $count6;
201 }
202
203 sub init
204 {
205         eval { require Net::CIDR::Lite };
206         if ($@) {
207                 LogDbg('DXProt', "DXCIDR: load (cpanm) the perl module Net::CIDR::Lite to check for bad IP addresses (or CIDR ranges)");
208                 return;
209         }
210
211         eval {import Net::CIDR::Lite };
212         if ($@) {
213                 LogDbg('DXProt', "DXCIDR: import Net::CIDR::Lite error $@");
214                 return;
215         }
216
217         $active = 1;
218
219         my $fn = _fn();
220         if (-e $fn) {
221                 move $fn, "$fn.base";
222         }
223
224         _touch("$fn.local");
225         
226         reload();
227
228 }
229
230 sub _touch
231 {
232         my $fn = shift;
233         my $now = time;
234         local (*TMP);
235         utime ($now, $now, $fn) || open (TMP, ">>$fn") || LogDbg('err', "DXCIDR::touch: Couldn't touch $fn: $!");
236 }
237
238 sub reload
239 {
240         return 0 unless $active;
241
242         new();
243
244         my $count = 0;
245         my $files = 0;
246
247         LogDbg('DXProt', "DXCIDR::reload reload database" );
248
249         my $dir;
250         opendir($dir, $main::local_data);
251         while (my $fn = readdir $dir) {
252                 next unless my ($suffix) = $fn =~ /^badip\.(\w+)$/;
253                 my $c = _load($suffix);
254                 LogDbg('DXProt', "DXCIDR::reload: $fn read containing $c ip addresses" );
255                 $count += $c;
256                 $files++;
257         }
258         closedir $dir;
259         
260         LogDbg('DXProt', "DXCIDR::reload $count ip addresses found (IPV4: $count4 IPV6: $count6) in $files badip files" );
261
262         return $count;
263 }
264
265 sub new
266 {
267         return 0 unless $active;
268
269         $ipv4 = Net::CIDR::Lite->new;
270         $ipv6 = Net::CIDR::Lite->new;
271         $count4 = $count6 = 0; 
272 }
273
274 1;