File Coverage

blib/lib/Net/IP/Identifier.pm
Criterion Covered Total %
statement 136 176 77.2
branch 48 76 63.1
condition 11 33 33.3
subroutine 22 24 91.6
pod 5 8 62.5
total 222 317 70.0


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