File Coverage

blib/lib/Net/IP/Identifier.pm
Criterion Covered Total %
statement 39 39 100.0
branch n/a
condition n/a
subroutine 13 13 100.0
pod n/a
total 52 52 100.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   3465 use 5.002;
  3         12  
  3         147  
12 3     3   20 use strict;
  3         5  
  3         127  
13 3     3   60 use warnings;
  3         5  
  3         153  
14              
15             {
16             package Local::Payload;
17 3     3   1998 use Moo;
  3         44898  
  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   7239 use Getopt::Long qw(:config pass_through);
  3         35694  
  3         31  
34 3     3   675 use File::Spec;
  3         5  
  3         87  
35 3     3   641 use Net::IP::Identifier::Net;
  3         8  
  3         143  
36 3     3   1758 use Net::IP::Identifier::Binode;
  3         12  
  3         120  
37 3     3   1920 use Net::IP::Identifier::Regex;
  3         10  
  3         130  
38 3     3   29 use Carp;
  3         4  
  3         245  
39 3     3   18 use Moo;
  3         4  
  3         25  
40 3     3   1094 use namespace::clean;
  3         6  
  3         26  
41 3     3   2802 use Module::Pluggable;
  3         35589  
  3         21  
42              
43             our $VERSION = '0.106'; # 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             my ($class) = @_;
93              
94             my %opts;
95             my $overlaps;
96             my $filename;
97             my $help;
98              
99             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             if ($help) {
110             print $help_msg;
111             exit;
112             }
113              
114             my $identifier = __PACKAGE__->new(%opts);
115              
116             unshift @ARGV, $filename if ($filename);
117             if (not @ARGV) {
118             $identifier->parse_fh(\*STDIN);
119             }
120              
121             while (@ARGV) {
122             my $arg = shift @ARGV;
123             if (-f $arg) {
124             open my $fh, '<', $arg;
125             croak "Can't open $arg for reading\n" if not $fh;
126             $identifier->parse_fh($fh);
127             close $fh;
128             next
129             }
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             $arg .= shift @ARGV . shift @ARGV;
134             }
135              
136             print $identifier->identify($arg) || $arg, "\n";
137             }
138              
139             if ($overlaps) {
140             for my $return (@{$identifier->tree_overlaps}) {
141             my @r = map { join $identifier->joiner, $_->payload->entity, $_->payload->ip; } @{$return};
142             warn join(' => ', @r), "\n";
143             }
144             }
145             }
146              
147             sub import {
148             my ($class, @imports) = @_;
149              
150             $imports = \@imports if (@imports); # save import list in class variable
151             }
152              
153             sub parse_fh {
154             my ($self, $fh) = @_;
155              
156             my $ip_any = $self->re->IP_any;
157             while(<$fh>) {
158             my (@ips) = m/($ip_any)/;
159             for my $ip (@ips) {
160             print $self->identify($ip) || $ip, "\n";
161             }
162             }
163             }
164              
165             sub load_entities {
166             my ($self, @plugins) = @_;
167              
168             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             delete $self->{parent_of};
172             delete $self->{entities};
173             for my $plugin (@{$plugins}) {
174             #print "requiring $plugin\n";
175             if (not $plugin =~ m/::/) {
176             $plugin = __PACKAGE__ . "::Plugin::$plugin";
177             }
178             eval "CORE::require $plugin"; ## no critic # attempt to read in the plugin
179             warn $@ if $@;
180             my $p = $plugin && $plugin->new;
181             next if not $p;
182             if (not $p->does('Net::IP::Identifier_Role')) {
183             print "$plugin doesn't satisfy the Net::IP::Identifier_Role - skipping\n";
184             next;
185             }
186             push @{$self->{entities}}, $p;
187             for my $child ($p->children) {
188             $self->{parent_of}{$child} = $p;
189             }
190             }
191             if ( @$plugins and
192             (not $self->{entities} or
193             not @{$self->{entities}})) {
194             croak "No plugins installed\n";
195             }
196             delete $self->{ip_tree};
197             }
198              
199             sub entities {
200             my ($self, @plugins) = @_;
201              
202             if (@_ > 1) {
203             undef $imports; # override imports with @plugins
204             #print "load args: ", join(' ', @plugins), "\n";
205             $self->load_entities(@plugins);
206             }
207              
208             if (not $self->{entities} or
209             not @{$self->{entities}}) {
210             # if no plugins yet loaded, check import list
211             # no import list? load everything we can find
212             if ($imports) {
213             #print "load imports ", join(' ', @{$imports}), "\n";
214             $self->load_entities($imports);
215             undef $imports; # only the first time
216             }
217             else {
218             #print "load imports ", join(' ', $self->plugins), "\n";
219             $self->load_entities([ $self->plugins ]);
220             }
221              
222             if (not $self->{entities} or
223             not @{$self->{entities}}) {
224             croak "No entity Plugins found\n";
225             }
226             }
227              
228             return wantarray
229             ? @{$self->{entities}}
230             : $self->{entities};
231             }
232              
233             sub ip_tree {
234             my ($self, $version) = @_;
235              
236             croak "ip_tree(\$version) error: no version\n" if not $version;
237              
238             if (not $self->{ip_tree}) {
239             my $root_v6 = Net::IP::Identifier::Binode->new;
240             # Place the IPv4 block in the IPv6 tree (IPv4 mapped IPv6)
241             my $root_v4 = $root_v6->construct(Net::IP::Identifier::Net->new('::ffff:0:0/96')->masked_ip);
242              
243             for my $entity ($self->entities) {
244             for my $ip ($entity->ips) {
245             my @ips = ($ip);
246             if (not defined $ip->prefixlen) {
247             @ips = $ip->range_to_cidrs;
248             }
249             for my $ip (@ips) {
250             my $root = ($ip->version == 6) ? $root_v6 : $root_v4;
251             $root->construct($ip->masked_ip)->payload(
252             Local::Payload->new(
253             entity => $entity,
254             ip => $ip,
255             ),
256             );
257             }
258             }
259             }
260             $self->{ip_tree}{6} = $root_v6;
261             $self->{ip_tree}{4} = $root_v4;
262             }
263             return $self->{ip_tree}{$version};
264             }
265              
266             sub identify {
267             my ($self, $ip) = @_;
268              
269             $ip = Net::IP::Identifier::Net->new($ip);
270             my @ips = ($ip);
271             if (not defined $ip->prefixlen) {
272             @ips = $ip->range_to_cidrs;
273             }
274              
275             my @return;
276             for my $ip (@ips) {
277             $self->ip_tree($ip->version)->follow($ip->masked_ip, sub {
278             push @return, $_[0] if ($_[0]->payload);
279             return 0; # always continue
280             },
281             );
282             }
283             if (not @return) {
284             return; # not found.
285             }
286              
287             if (not $self->parents) {
288             @return = ($return[-1]); # just the last child
289             }
290              
291             @return = map { $_->payload } @return; # remove the Binode layer
292              
293             if (wantarray) {
294             return $self->cidr
295             ? map { $_->entity, $_->ip } @return
296             : @return;
297             }
298              
299             if ($self->cidr) {
300             my @e = map { join ( $self->joiner, $_->entity, $_->ip) } @return;
301             return join ' => ', @e;
302             }
303             my $r = join (' => ', map {
304             $_->entity->name
305             } @return);
306             return $r;
307             }
308              
309             sub tree_overlaps {
310             my ($self) = @_;
311              
312             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             my ($node, $level) = @_;
318              
319             my @overlap; # a single overlap array, parent then children
320             if ($node->payload and
321             ($node->zero or $node->one)) {
322             $node->traverse_width_first(
323             sub {
324             if ($_[0]->payload) {
325             push @overlap, $_[0];
326             }
327             return 0; # always continue
328             }
329             );
330             }
331             push @overlaps, \@overlap if (@overlap > 1);
332             return @overlap > 1; # stop if we found overlap
333             },
334             );
335              
336             return wantarray
337             ? @overlaps
338             : \@overlaps;
339             }
340              
341             1;
342              
343             __END__