File Coverage

blib/lib/Net/IP/Identifier.pm
Criterion Covered Total %
statement 128 168 76.1
branch 43 70 61.4
condition 11 33 33.3
subroutine 21 23 91.3
pod 4 7 57.1
total 207 301 68.7


line stmt bran cond sub pod time code
1             #!/usr/bin/perl
2             #===============================================================================
3             # PODNAME: Net::IP::Identifier
4             # ABSTRACT: Identify IPs that fall within collections of network blocks
5             #
6             # AUTHOR: Reid Augustin (REID)
7             # EMAIL: reid@hellosix.com
8             # CREATED: Mon Oct 6 10:20:33 PDT 2014
9             #===============================================================================
10              
11 3     3   3532 use 5.002;
  3         12  
  3         121  
12 3     3   16 use strict;
  3         5  
  3         117  
13 3     3   49 use warnings;
  3         3  
  3         151  
14              
15             {
16             package Local::Payload;
17 3     3   1958 use Moo;
  3         48487  
  3         22  
18              
19             has entity => (
20             is => 'rw',
21             isa => sub { die "Not a Net::IP::Identifier::Plugin\n"
22             if (not $_[0]->does('Net::IP::Identifier_Role')); },
23             );
24             has ip => (
25             is => 'rw',
26             isa => sub { die "Not a Net::IP::Identifier::Net\n"
27             if (not $_[0]->isa('Net::IP::Identifier::Net')); },
28             );
29             }
30              
31              
32             package Net::IP::Identifier;
33 3     3   8047 use Getopt::Long qw(:config pass_through);
  3         31922  
  3         31  
34 3     3   592 use File::Spec;
  3         5  
  3         66  
35 3     3   817 use Net::IP::Identifier::Net;
  3         5  
  3         114  
36 3     3   1554 use Net::IP::Identifier::Binode;
  3         8  
  3         118  
37 3     3   1933 use Net::IP::Identifier::Regex;
  3         7  
  3         122  
38 3     3   21 use Carp;
  3         4  
  3         197  
39 3     3   13 use Moo;
  3         4  
  3         20  
40 3     3   944 use namespace::clean;
  3         6  
  3         25  
41 3     3   2842 use Module::Pluggable;
  3         27615  
  3         27  
42              
43             our $VERSION = '0.110'; # VERSION
44              
45             has joiner => (
46             is => 'rw',
47             isa => sub { die "Not a string\n" if (ref $_[0]); },
48             default => sub { ':' },
49             );
50             has cidr => (
51             is => 'rw',
52             );
53             has parents => (
54             is => 'rw',
55             );
56             has overlaps => (
57             is => 'rw',
58             );
59             has re => ( # regular expressions for IP addresses
60             is => 'lazy',
61             default => sub { Net::IP::Identifier::Regex->new },
62             );
63              
64             my $imports;
65              
66             my (undef, undef, $myName) = File::Spec->splitpath($0);
67              
68             my $help_msg = <
69              
70             $myName [ options ] IP [ IP... ]
71              
72             If IP belongs to a known entity (a Net::IP::Identifier::Plugin),
73             print the entity.
74              
75             IP may be dotted decimal format: N.N.N.N, range format: N.N.N.N - N.N.N.N,
76             CIDR format: N.N.N.N/W, or a filename from which IPs will be extracted. If
77             no IP or filename is found on the command line, STDIN is opened.
78              
79             Options (may be abbreviated):
80             parents => prepend Net::IP::Identifier objects of parent entities
81             cidr => append Net::IP::Identifier::Net objects to entities
82             filename => read from file(s) instead of command line args
83             overlaps => show overlapping netblocks during binary tree construction
84             help => this message
85              
86             EO_HELP
87             ;
88              
89             __PACKAGE__->run unless caller; # modulino
90              
91             sub run {
92 0     0 1 0 my ($class) = @_;
93              
94 0         0 my %opts;
95             my $overlaps;
96 0         0 my $filename;
97 0         0 my $help;
98              
99 0 0       0 exit 0 if (not
100             GetOptions(
101             'parents' => \$opts{parents},
102             'cidr' => \$opts{cidr},
103             'overlaps' => \$overlaps,
104             'filename=s' => \$filename,
105             'help' => \$help,
106             )
107             );
108              
109 0 0       0 if ($help) {
110 0         0 print $help_msg;
111 0         0 exit;
112             }
113              
114 0         0 my $identifier = __PACKAGE__->new(%opts);
115              
116 0 0       0 unshift @ARGV, $filename if ($filename);
117 0 0       0 if (not @ARGV) {
118 0         0 $identifier->parse_fh(\*STDIN);
119             }
120              
121 0         0 while (@ARGV) {
122 0         0 my $arg = shift @ARGV;
123 0 0 0     0 if (-f $arg) {
    0 0        
124 0         0 open my $fh, '<', $arg;
125 0 0       0 croak "Can't open $arg for reading\n" if not $fh;
126 0         0 $identifier->parse_fh($fh);
127 0         0 close $fh;
128             next
129 0         0 }
130             elsif ($ARGV[0] and # accept N.N.N.N - N.N.N.N for network blocks too
131             $ARGV[0] eq '-' and
132             $ARGV[1]) {
133 0         0 $arg .= shift(@ARGV) . shift(@ARGV);
134             }
135              
136 0   0     0 print $identifier->identify($arg) || $arg, "\n";
137             }
138              
139 0 0       0 if ($overlaps) {
140 0         0 for my $return (@{$identifier->tree_overlaps}) {
  0         0  
141 0         0 my @r = map { join $identifier->joiner, $_->payload->entity, $_->payload->ip; } @{$return};
  0         0  
  0         0  
142 0         0 warn join(' => ', @r), "\n";
143             }
144             }
145             }
146              
147             sub import {
148 3     3   52 my ($class, @imports) = @_;
149              
150 3 50       87 $imports = \@imports if (@imports); # save import list in class variable
151             }
152              
153             sub parse_fh {
154 0     0 0 0 my ($self, $fh) = @_;
155              
156 0         0 my $ip_any = $self->re->IP_any;
157 0         0 while(<$fh>) {
158 0         0 my (@ips) = m/($ip_any)/;
159 0         0 for my $ip (@ips) {
160 0   0     0 print $self->identify($ip) || $ip, "\n";
161             }
162             }
163             }
164              
165             sub load_entities {
166 4     4 0 17406 my ($self, @plugins) = @_;
167              
168 4 100       22 my $plugins = ref $plugins[0] eq 'ARRAY' # accept array or ref
169             ? $plugins[0] # a ref was passed in
170             : \@plugins; # convert array to ref
171 4         17 delete $self->{parent_of};
172 4         68 delete $self->{entities};
173 4         9 for my $plugin (@{$plugins}) {
  4         12  
174             #print "requiring $plugin\n";
175 37 100       267 if (not $plugin =~ m/::/) {
176 2         7 $plugin = __PACKAGE__ . "::Plugin::$plugin";
177             }
178 37         5229 eval "CORE::require $plugin"; ## no critic # attempt to read in the plugin
179 37 50       292 warn $@ if $@;
180 37   33     238 my $p = $plugin && $plugin->new;
181 37 50       152 next if not $p;
182 37 50       236 if (not $p->does('Net::IP::Identifier_Role')) {
183 0         0 print "$plugin doesn't satisfy the Net::IP::Identifier_Role - skipping\n";
184 0         0 next;
185             }
186 37         1079 push @{$self->{entities}}, $p;
  37         657  
187 37         243 for my $child ($p->children) {
188 2         15 $self->{parent_of}{$child} = $p;
189             }
190             }
191 4 50 33     31 if ( @$plugins and
      66        
192             (not $self->{entities} or
193             not @{$self->{entities}})) {
194 0         0 croak "No plugins installed\n";
195             }
196 4         12692 delete $self->{ip_tree};
197             }
198              
199             sub entities {
200 17     17 1 894 my ($self, @plugins) = @_;
201              
202 17 100       77 if (@_ > 1) {
203 2         6 undef $imports; # override imports with @plugins
204             #print "load args: ", join(' ', @plugins), "\n";
205 2         12 $self->load_entities(@plugins);
206             }
207              
208 17 100 66     127 if (not $self->{entities} or
  15         69  
209             not @{$self->{entities}}) {
210             # if no plugins yet loaded, check import list
211             # no import list? load everything we can find
212 2 100       8 if ($imports) {
213             #print "load imports ", join(' ', @{$imports}), "\n";
214 1         5 $self->load_entities($imports);
215 1         2 undef $imports; # only the first time
216             }
217             else {
218             #print "load imports ", join(' ', $self->plugins), "\n";
219 1         9 $self->load_entities([ $self->plugins ]);
220             }
221              
222 2 50 33     26 if (not $self->{entities} or
  2         8  
223             not @{$self->{entities}}) {
224 0         0 croak "No entity Plugins found\n";
225             }
226             }
227              
228             return wantarray
229 17 100       64 ? @{$self->{entities}}
  15         82  
230             : $self->{entities};
231             }
232              
233             sub ip_tree {
234 57     57 0 286 my ($self, $version) = @_;
235              
236 57 50       153 croak "ip_tree(\$version) error: no version\n" if not $version;
237              
238 57 100       169 if (not $self->{ip_tree}) {
239 15         490 my $root_v6 = Net::IP::Identifier::Binode->new;
240             # Place the IPv4 block in the IPv6 tree (IPv4 mapped IPv6)
241 15         5216 my $root_v4 = $root_v6->construct(Net::IP::Identifier::Net->new('::ffff:0:0/96')->masked_ip);
242              
243 15         228 for my $entity ($self->entities) {
244 66         1713 for my $ip ($entity->ips) {
245 1268         22405 my @ips = ($ip);
246 1268 100       5555 if (not defined $ip->prefixlen) {
247 142         1605 @ips = $ip->range_to_cidrs;
248             }
249 1268         9913 for my $ip (@ips) {
250 1507 100       9543 my $root = ($ip->version == 6) ? $root_v6 : $root_v4;
251 1507         16799 $root->construct($ip->masked_ip)->payload(
252             Local::Payload->new(
253             entity => $entity,
254             ip => $ip,
255             ),
256             );
257             }
258             }
259             }
260 15         307 $self->{ip_tree}{6} = $root_v6;
261 15         52 $self->{ip_tree}{4} = $root_v4;
262             }
263 57         345 return $self->{ip_tree}{$version};
264             }
265              
266             sub identify {
267 46     46 1 11865 my ($self, $ip) = @_;
268              
269 46         230 $ip = Net::IP::Identifier::Net->new($ip);
270 46         108 my @ips = ($ip);
271 46 50       130 if (not defined $ip->prefixlen) {
272 0         0 @ips = $ip->range_to_cidrs;
273             }
274              
275 46         236 my @return;
276 46         175 for my $ip (@ips) {
277             $self->ip_tree($ip->version)->follow($ip->masked_ip, sub {
278 2102 100   2102   3930 push @return, $_[0] if ($_[0]->payload);
279 2102         3772 return 0; # always continue
280             },
281 46         168 );
282             }
283 46 100       153 if (not @return) {
284 21         196 return; # not found.
285             }
286              
287 25 50       111 if (not $self->parents) {
288 25         61 @return = ($return[-1]); # just the last child
289             }
290              
291 25         52 @return = map { $_->payload } @return; # remove the Binode layer
  25         93  
292              
293 25 100       52 if (wantarray) {
294 1         20 return $self->cidr
295 1 50       7 ? map { $_->entity, $_->ip } @return
296             : @return;
297             }
298              
299 24 100       100 if ($self->cidr) {
300 8         17 my @e = map { join ( $self->joiner, $_->entity, $_->ip) } @return;
  8         181  
301 8         573 return join ' => ', @e;
302             }
303 16         273 my $r = join (' => ', map {
304 16         20 $_->entity->name
305             } @return);
306 16         783 return $r;
307             }
308              
309             sub tree_overlaps {
310 11     11 1 74 my ($self) = @_;
311              
312 11         12 my @overlaps; # collect overlaps here. each overlap is an array
313             # starting with the parent, followed by children.
314              
315             $self->ip_tree(6)->traverse_width_first(
316             sub {
317 1179     1179   1065 my ($node, $level) = @_;
318              
319 1179         893 my @overlap; # a single overlap array, parent then children
320 1179 100 66     2453 if ($node->payload and
      66        
321             ($node->zero or $node->one)) {
322             $node->traverse_width_first(
323             sub {
324 138 100       340 if ($_[0]->payload) {
325 26         53 push @overlap, $_[0];
326             }
327 138         301 return 0; # always continue
328             }
329 10         208 );
330             }
331 1179 100       1934 push @overlaps, \@overlap if (@overlap > 1);
332 1179         3256 return @overlap > 1; # stop if we found overlap
333             },
334 11         47 );
335              
336             return wantarray
337             ? @overlaps
338 11 50       228 : \@overlaps;
339             }
340              
341             1;
342              
343             __END__