harden DXCIDR from router/proxy html measges
[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         my $suffix = shift;
72         my @in = _read($suffix);
73         return 0 unless @in;
74         return scalar add(@in);
75 }
76
77 sub _put
78 {
79         my $suffix = shift;
80         my $fn = _fn() . ".$suffix";
81         my $r = rand;
82         my $fh = IO::File->new (">$fn.$r");
83         my $count = 0;
84         if ($fh) {
85                 for ($ipv4->list, $ipv6->list) {
86                         $fh->print("$_\n");
87                         ++$count;
88                 }
89                 move "$fn.$r", $fn;
90                 LogDbg('cmd', "DXCIDR: put (re-)written $fn");
91         } else {
92                 LogDbg('err', "DXCIDR: cannot write $fn.$r $!");
93         }
94         return $count;
95 }
96
97 sub append
98 {
99         return 0 unless $active;
100         
101         my $suffix = shift;
102         my @in = @_;
103         my @out;
104         
105         if ($suffix) {
106                 my $fn = _fn() . ".$suffix";
107                 my $fh = IO::File->new;
108                 if ($fh->open("$fn", "a+")) {
109                         $fh->seek(0, 2);        # belt and braces !!
110                         print $fh "$_\n" for @in;
111                         $fh->close;
112                 } else {
113                         LogDbg('err', "DXCIDR::append error appending to $fn $!");
114                 }
115         } else {
116                 LogDbg('err', "DXCIDR::append require badip suffix");
117         }
118         return scalar @in;
119 }
120
121 sub add
122 {
123         return 0 unless $active;
124         my $count = 0;
125         
126         for my $ip (@_) {
127                 # protect against stupid or malicious
128                 next unless is_ipaddr($ip);
129                 next if $ip =~ /^127\./;
130                 next if $ip =~ /^::1$/;
131                 if ($ip =~ /\./) {
132                         $ipv4->add_any($ip);
133                         ++$count;
134                         ++$count4;
135                 } elsif ($ip =~ /:/) {
136                         $ipv6->add_any($ip);
137                         ++$count;
138                         ++$count6;
139                 } else {
140                         LogDbg('err', "DXCIDR::add non-ip address '$ip' read");
141                 }
142         }
143         return $count;
144 }
145
146 sub clean_prep
147 {
148         return unless $active;
149
150         if ($ipv4 && $count4) {
151                 $ipv4->clean;
152                 $ipv4->prep_find;
153         }
154         if ($ipv6 && $count6) {
155                 $ipv6->clean;
156                 $ipv6->prep_find;
157         }
158 }
159
160 sub _sort
161 {
162         my @in;
163         my @out;
164         for (@_) {
165                 push @in, [inet_pton(m|:|?AF_INET6:AF_INET, $_), split m|/|];
166         }
167         @out = sort {$a->[0] <=> $b->[0]} @in;
168         return map { "$_->[1]/$_->[2]"} @out;
169 }
170
171 sub list
172 {
173         return () unless $active;
174         my @out;
175         push @out, $ipv4->list if $count4;
176         push @out, $ipv6->list if $count6;
177         return _sort(@out);
178 }
179
180 sub find
181 {
182         return 0 unless $active;
183         return 0 unless $_[0];
184
185         if ($_[0] =~ /\./) {
186                 return $ipv4->find($_[0]) if $count4;
187         }
188         return $ipv6->find($_[0]) if $count6;
189 }
190
191 sub init
192 {
193         eval { require Net::CIDR::Lite };
194         if ($@) {
195                 LogDbg('DXProt', "DXCIDR: load (cpanm) the perl module Net::CIDR::Lite to check for bad IP addresses (or CIDR ranges)");
196                 return;
197         }
198
199         import Net::CIDR::Lite;
200         $active = 1;
201
202         my $fn = _fn();
203         if (-e $fn) {
204                 move $fn, "$fn.base";
205         }
206
207         _touch("$fn.local");
208         
209         reload();
210
211 }
212
213 sub _touch
214 {
215         my $fn = shift;
216         my $now = time;
217         local (*TMP);
218         utime ($now, $now, $fn) || open (TMP, ">>$fn") || LogDbg('err', "DXCIDR::touch: Couldn't touch $fn: $!");
219 }
220
221 sub reload
222 {
223         return 0 unless $active;
224
225         new();
226
227         my $count = 0;
228         my $files = 0;
229
230         LogDbg('DXProt', "DXCIDR::reload reload database" );
231
232         my $dir;
233         opendir($dir, $main::local_data);
234         while (my $fn = readdir $dir) {
235                 next unless my ($suffix) = $fn =~ /^badip\.(\w+)$/;
236                 my $c = _load($suffix);
237                 LogDbg('DXProt', "DXCIDR::reload: $fn read containing $c ip addresses" );
238                 $count += $c;
239                 $files++;
240         }
241         closedir $dir;
242         
243         LogDbg('DXProt', "DXCIDR::reload $count ip addresses found (IPV4: $count4 IPV6: $count6) in $files badip files" );
244
245         return $count;
246 }
247
248 sub new
249 {
250         return 0 unless $active;
251
252         $ipv4 = Net::CIDR::Lite->new;
253         $ipv6 = Net::CIDR::Lite->new;
254         $count4 = $count6 = 0; 
255 }
256
257 1;