File Coverage

blib/lib/IP/Tools.pm
Criterion Covered Total %
statement 44 183 24.0
branch 10 84 11.9
condition 0 3 0.0
subroutine 6 12 50.0
pod 7 9 77.7
total 67 291 23.0


line stmt bran cond sub pod time code
1             package IP::Tools;
2              
3             # See the following for the meaning of "dl_load_flags" and why it uses
4             # DynaLoader and not XSLoader.
5              
6             # http://www.perlmonks.org/?node_id=691130
7              
8             require DynaLoader;
9             require Exporter;
10              
11             @ISA = qw(Exporter DynaLoader);
12              
13             our $VERSION = 0.04;
14              
15             __PACKAGE__->bootstrap ($VERSION);
16              
17             sub dl_load_flags
18             {
19 2     2 1 791 return 0x01;
20             }
21              
22              
23             @EXPORT_OK = qw/
24             ip_to_int
25             int_to_ip
26             get_ip_range
27             get_cidr
28             cidr_to_ip_range
29             read_whitelist
30             search_whitelist
31             ip_range_to_cidr
32             $cidr_re
33             $ip_re
34             $ipr_re
35             /;
36              
37             %EXPORT_TAGS = (
38             all => \@EXPORT_OK,
39             );
40 2     2   95845 use warnings;
  2         4  
  2         80  
41 2     2   11 use strict;
  2         5  
  2         68  
42 2     2   10 use Carp;
  2         7  
  2         5942  
43              
44             our $ip_re = qr/
45             (?:\d+\.){3}
46             \d+
47             /x;
48              
49             our $cidr_re = qr!
50             \s*
51             (
52             $ip_re
53             )
54             /
55             (\d+)
56             \s*
57             !x;
58              
59             our $ipr_re = qr!
60             \s*
61             (
62             $ip_re
63             )
64             \s*
65             -
66             \s*
67             (
68             $ip_re
69             )
70             \s*
71             !x;
72              
73             sub split_ip
74             {
75 0     0 0 0 my ($ip) = @_;
76 0         0 return split /\./, $ip;
77             }
78              
79             sub int_to_ip
80             {
81 1     1 1 13 my ($int) = @_;
82 1         4 my @bytes;
83 1         6 while ($int) {
84 3         6 push @bytes, $int % 0x100;
85 3         7 $int /= 0x100;
86 3         10 $int = int $int;
87             }
88 1         7 my $ip = join ".", reverse @bytes;
89 1         10 return $ip;
90             }
91              
92             sub cidr_to_ip_range
93             {
94 0     0 1 0 my ($ip, $log_mask) = @_;
95 0         0 my $log_mask_max = 32;
96 0         0 my $ip_int = ip_to_int ($ip);
97 0 0       0 if ($log_mask > $log_mask_max) {
98 0         0 croak "The mask value of $log_mask is too big; the maximum is $log_mask_max";
99             }
100 0         0 my $add = (1 << ($log_mask_max - $log_mask)) - 1;
101 0         0 my $lower = $ip_int;
102 0         0 my $zeroed = $lower - ($lower % ($add + 1));
103 0 0       0 if ($lower != $zeroed) {
104 0         0 croak "cannot handle CIDR address $ip: $lower != $zeroed";
105             }
106 0         0 my $upper = $lower + $add;
107 0         0 return ($lower, $upper);
108             }
109              
110             sub get_ip_range
111             {
112 0     0 1 0 my ($cidr) = @_;
113             # Return value container.
114 0         0 my $ip_range;
115             # Error container.
116             my $error;
117 0         0 my $log_mask_max = 32;
118             # Error tolerance for floating point calculation.
119 0         0 my $eps = 0.0001;
120 0         0 my $ip_re = qr/(?:\d+\.)+\d+/;
121 0 0       0 if ($cidr =~ m!^\s*($ip_re)\s*/\s*(\d+)\s*$!) {
122 0         0 my $ip = $1;
123 0         0 my $log_mask = $2;
124 0         0 my $ip_int;
125 0 0       0 if ($log_mask > $log_mask_max) {
126 0         0 $error = <
127             The mask value of $log_mask is too big; the maximum is $log_mask_max.
128             EOF
129 0         0 return undef, $error;
130             }
131 0         0 ($ip_int, $error) = ip_to_int ($ip);
132 0 0       0 if ($error) {
133 0         0 return undef, $error;
134             }
135 0         0 my $add = (1 << ($log_mask_max - $log_mask)) - 1;
136 0         0 my $lower = $ip_int;
137 0         0 my $zeroed = $lower - ($lower % ($add + 1));
138 0 0       0 if ($lower != $zeroed) {
139 0         0 my $zeroed_ip = int_to_ip ($zeroed);
140 0         0 $error = <
141             This CIDR address doesn't look right: maybe it should be $zeroed_ip/$log_mask?
142             EOF
143 0         0 $lower = $zeroed;
144             }
145 0         0 my $upper = $lower + $add;
146 0         0 $ip_range = int_to_ip ($lower) . " - " . int_to_ip ($upper);
147             # Zero out lower bits.
148             }
149 0         0 return ($ip_range, $error);
150             }
151              
152             sub get_cidr
153             {
154 0     0 0 0 my ($ip_range) = @_;
155             # Return value container.
156 0         0 my $cidr;
157             # Error container.
158             my $error;
159             # Error tolerance for floating point calculation.
160 0         0 my $eps = 0.0001;
161 0         0 my $ip_re = qr/(?:\d+\.)+\d+/;
162 0 0       0 if ($ip_range =~ m!^\s*($ip_re)\D+($ip_re)\s*$!) {
163 0         0 my $ip1 = $1;
164 0         0 my $ip2 = $2;
165 0         0 my $ip1_int;
166             my $ip2_int;
167 0         0 ($ip1_int, $error) = ip_to_int ($ip1);
168 0 0       0 if ($error) {
169 0         0 return undef, $error;
170             }
171 0         0 ($ip2_int, $error) = ip_to_int ($ip2);
172 0 0       0 if ($error) {
173 0         0 return undef, $error;
174             }
175 0         0 my $base = $ip1_int;
176            
177 0         0 my $mask = $ip2_int - $ip1_int;
178 0 0       0 if ($mask < 0) {
179 0         0 $error = <
180             The first IP, $ip1, is greater than the second IP address, $ip2,
181             by $mask, so the range cannot be calculated.
182             EOF
183 0         0 return "$ip1/32", $error;
184             }
185 0 0       0 if ($mask == 0) {
186 0         0 return "$ip1/32", undef;
187             }
188 0         0 my $log2mask = log ($mask + 1) / log (2);
189 0 0       0 if (abs ($log2mask) > abs (int $log2mask) + $eps) {
190 0         0 $error = <
191             The difference between $ip1 and $ip2, $mask, is not a power of two minus one,
192             so there is probably an error in your inputs.
193              
194             EOF
195             }
196              
197 0         0 $cidr = "$ip1/" . int (32 - $log2mask);
198             }
199             else {
200 0         0 $error = <
201             Sorry, I could not parse that. The range should be in a format
202            
 
203             123.45.6.7 - 123.45.6.8
204            
205             EOF
206             }
207 0         0 return ($cidr, $error);
208             }
209              
210             sub ip_range_to_cidr
211             {
212 0     0 1 0 my ($ip1, $ip2) = @_;
213             # Return value container.
214 0         0 my $cidr;
215             # Error container.
216             my $error;
217             # Error tolerance for floating point calculation.
218 0         0 my $eps = 0.0001;
219 0         0 my $ip1_int = ip_to_int ($ip1);
220 0         0 my $ip2_int = ip_to_int ($ip2);
221 0         0 my $base = $ip1_int;
222 0         0 my $mask = $ip2_int - $ip1_int;
223 0 0       0 if ($mask < 0) {
224 0         0 croak "$ip1 is greater than $ip2";
225             }
226 0 0       0 if ($mask == 0) {
227 0         0 return "$ip1/32";
228             }
229 0         0 my $log2mask = log ($mask + 1) / log (2);
230 0 0       0 if (abs ($log2mask) > abs (int $log2mask) + $eps) {
231 0         0 croak "Cannot handle non-power-of-two mask $mask";
232             }
233 0         0 $cidr = "$ip1/" . int (32 - $log2mask);
234 0         0 return $cidr;
235             }
236              
237             sub read_whitelist
238             {
239 1     1 1 5 my ($infile, %options) = @_;
240 1         4 my $verbose = $options{verbose};
241 1 50       84 open my $in, "<", $infile or die $!;
242 1         3 my @ips;
243 1         4 my $comment = '';
244 1         36 while (<$in>) {
245 2         7 chomp;
246 2 100       12 if (/^\s*#\s*(.*)/) {
247 1         3 $comment = $1;
248 1 50       4 if ($verbose) {
249 1         273 print "$infile:$.: Comment '$comment'.\n";
250             }
251 1         9 next;
252             }
253             # Skip blank lines
254 1 50       7 if (/^\s*$/) {
255 0 0       0 if ($verbose) {
256 0         0 print "$infile:$.: Skipping whitespace.\n";
257             }
258 0         0 next;
259             }
260 1 50       28 if (/$cidr_re/) {
261             # I do not know the correct terms here.
262 0         0 my $base = $1;
263 0         0 my $bits = $2;
264 0 0       0 if ($verbose) {
265 0         0 print "$infile:$.: base = $base, bits = $bits.\n";
266             }
267 0         0 my ($lower, $upper) = cidr_to_ip_range ($base, $bits);
268 0 0       0 if ($verbose) {
269 0         0 printf "$infile:$.: %X - %X\n", $lower, $upper;
270             }
271 0         0 push @ips, {
272             lower => $lower,
273             upper => $upper,
274             line => $.,
275             comment => $comment,
276             };
277 0         0 next;
278             }
279 1 50       63 if (/^$ipr_re$/) {
280 1         6 my $lower = ip_to_int ($1);
281 1         6 my $upper = ip_to_int ($2);
282 1 50       5 if ($verbose) {
283 1         239 printf "$infile:$.: %X - %X\n", $lower, $upper;
284             }
285 1         10 push @ips, {
286             lower => $lower,
287             upper => $upper,
288             line => $.,
289             comment => $comment,
290             };
291 1         12 next;
292             }
293 0         0 die "$infile:$.: Unparseable line '$_'.\n"
294             }
295 1 50       16 close $in or die $!;
296              
297             # Sort the addresses from lowest to highest.
298              
299 1         4 @ips = sort {$a->{lower} <=> $b->{lower}} @ips;
  0         0  
300              
301 1 50       6 if ($options{ignoredups}) {
302 0         0 my @nodups;
303 0         0 for my $i (0..$#ips - 1) {
304 0 0       0 if ($ips[$i]->{upper} <= $ips[$i + 1]->{lower}) {
305 0         0 push @nodups, $ips[$i + 1];
306             }
307             }
308 0         0 @ips = @nodups;
309             }
310             # Check they are not overlapping.
311              
312 1         5 for my $i (0..$#ips - 1) {
313 0 0       0 if ($ips[$i]->{upper} > $ips[$i + 1]->{lower}) {
314 0         0 my $error = "$infile:$ips[$i]->{line}: upper range overlaps with $ips[$i + 1]->{line}";
315 0 0       0 if ($options{ignoredups}) {
316 0         0 warn "$error\n";
317             }
318             else {
319 0         0 die $error;
320             }
321             }
322             }
323 1         8 return @ips;
324             }
325              
326             sub search_whitelist
327             {
328 0     0 1   my ($ips, $ip, $verbose) = @_;
329 0           my $int = ip_to_int ($ip);
330 0 0         if ($verbose) {
331 0           printf "%s corresponds to %X.\n", $ip, $int;
332             }
333 0           my $n_ips = scalar (@$ips);
334 0 0         if ($verbose) {
335 0           printf "There are %d IPs.\n", $n_ips;
336             }
337 0           my $count = 0;
338 0           my $division = int ($n_ips / 2);
339 0           my $i = $division;
340 0           while (1) {
341 0           $count++;
342 0 0         if ($count > 100) {
343 0           die "There is bad logic in the search.\n";
344             }
345 0           $division = int ($division/2);
346 0 0         if ($division == 0) {
347 0           $division = 1;
348             }
349 0 0         if ($i > $n_ips - 1) {
    0          
350             # $i is greater than the biggest entry, so we cannot
351             # find it in the list.
352 0           return undef;
353             }
354             elsif ($i < 0) {
355             # $i is smaller than the smallest entry, so we cannot find
356             # it in the list.
357 0           return undef;
358             }
359 0 0         if ($int >= $ips->[$i]->{lower}) {
360 0 0         if ($verbose) {
361 0           printf ("%X: checking within %X-%X.\n", $int, $ips->[$i]->{lower}, $ips->[$i]->{upper});
362             }
363 0 0 0       if ($i == $n_ips - 1 || $int <= $ips->[$i + 1]->{lower}) {
364 0 0         if ($int <= $ips->[$i]->{upper}) {
365             # The IP lies between the lower and upper bounds of
366             # this range.
367 0           return $ips->[$i];
368             }
369             else {
370             # The IP lies between the upper bound of $i and the
371             # lower bound of $i+1, so it is unknown.
372 0           return undef;
373             }
374             }
375             else {
376             # $i is less than or equal to $n_ips - 1, so we
377             # increase $i by $division and check again.
378 0 0         if ($verbose) {
379 0           printf ("%X: going up from %X, i = %d, division = %d.\n", $int, $ips->[$i]->{lower}, $i + $division, $division);
380             }
381 0           $i += $division;
382             }
383             }
384             else {
385             # $i is greater than zero, so go down by $division steps
386             # and check again.
387 0 0         if ($verbose) {
388 0           printf "%X: Going down from %X, i = %d.\n", $int,
389             $ips->[$i]->{lower},
390             $i - $division;
391             }
392 0           $i -= $division;
393             }
394             }
395             }
396              
397             1;
398