File Coverage

blib/lib/Net/MAC/Vendor.pm
Criterion Covered Total %
statement 150 192 78.1
branch 31 58 53.4
condition 3 3 100.0
subroutine 29 32 90.6
pod 16 16 100.0
total 229 301 76.0


line stmt bran cond sub pod time code
1             package Net::MAC::Vendor; # git description: v1.267-2-g8373c47
2             # ABSTRACT: Look up the vendor for a MAC
3              
4 8     8   11595 use strict;
  8         16  
  8         266  
5 8     8   41 use warnings;
  8         11  
  8         177  
6 8     8   134 use 5.010;
  8         23  
7              
8 8     8   6548 use IO::Socket::SSL ();
  8         610633  
  8         235  
9 8     8   62 use Net::SSLeay;
  8         14  
  8         418  
10              
11             #pod =head1 SYNOPSIS
12             #pod
13             #pod use Net::MAC::Vendor;
14             #pod
15             #pod my $mac = "00:0d:93:29:f6:c2";
16             #pod
17             #pod my $array = Net::MAC::Vendor::lookup( $mac );
18             #pod
19             #pod You can also run this as a script with as many arguments as you
20             #pod like. The module realizes it is a script, looks up the information
21             #pod for each MAC, and outputs it.
22             #pod
23             #pod perl Net/MAC/Vendor.pm 00:0d:93:29:f6:c2 00:0d:93:29:f6:c5
24             #pod
25             #pod =head1 DESCRIPTION
26             #pod
27             #pod The Institute of Electrical and Electronics Engineers (IEEE) assigns
28             #pod an Organizational Unique Identifier (OUI) to manufacturers of network
29             #pod interfaces. Each interface has a Media Access Control (MAC) address
30             #pod of six bytes. The first three bytes are the OUI.
31             #pod
32             #pod This module allows you to take a MAC address and turn it into the OUI
33             #pod and vendor information. You can, for instance, scan a network,
34             #pod collect MAC addresses, and turn those addresses into vendors. With
35             #pod vendor information, you can often guess at what what you are looking
36             #pod at (I an Apple product).
37             #pod
38             #pod You can use this as a module as its individual functions, or call it
39             #pod as a script with a list of MAC addresses as arguments. The module can
40             #pod figure it out.
41             #pod
42             #pod The IEEE moves the location of its OUI file. If they do that again, you
43             #pod can set the C environment variable to get the new
44             #pod URL without updating the code.
45             #pod
46             #pod Here are some of the old URLs, which also flip-flop schemes:
47             #pod
48             #pod http://standards.ieee.org/regauth/oui/oui.txt
49             #pod https://standards.ieee.org/regauth/oui/oui.txt
50             #pod http://standards-oui.ieee.org/oui.txt
51             #pod http://standards-oui.ieee.org/oui/oui.txt
52             #pod
53             #pod There are older copies of the OUI file in the GitHub repository.
54             #pod
55             #pod These files are large (about 4MB), so you might want to cache a copy.
56             #pod
57             #pod A different source of information is linuxnet.ca that publishes sanitized
58             #pod and compressed versions of the list, such as:
59             #pod
60             #pod http://linuxnet.ca/ieee/oui.txt.bz2
61             #pod
62             #pod The module can read and decompress compressed versions (as long as the url
63             #pod reflects the compression type in the filename as the linuxnet.ca links do).
64             #pod
65             #pod =head2 Functions
66             #pod
67             #pod =over 4
68             #pod
69             #pod =cut
70              
71 8     8   48 use Exporter qw(import);
  8         14  
  8         285  
72              
73             __PACKAGE__->run( @ARGV ) unless caller;
74              
75 8     8   42 use Carp ();
  8         17  
  8         100  
76 8     8   4902 use Mojo::URL;
  8         1305278  
  8         71  
77 8     8   4216 use Mojo::UserAgent;
  8         1205618  
  8         71  
78              
79             our $VERSION = '1.268';
80              
81             #pod =item run( @macs )
82             #pod
83             #pod If I call this module as a script, this class method automatically
84             #pod runs. It takes the MAC addresses and prints the registered vendor
85             #pod information for each address. I can pass it a list of MAC addresses
86             #pod and run() processes each one of them. It prints out what it
87             #pod discovers.
88             #pod
89             #pod This method does try to use a cache of OUI to cut down on the
90             #pod times it has to access the network. If the cache is fully
91             #pod loaded (perhaps using C), it may not even use the
92             #pod network at all.
93             #pod
94             #pod =cut
95              
96             sub run {
97 1     1 1 3765 my $class = shift;
98              
99 1         3 foreach my $arg ( @_ ) {
100 1         5 my $lines = lookup( $arg );
101 1 50       6 return unless defined $lines;
102              
103 1         7 unshift @$lines, $arg;
104              
105 1         18 print join "\n", @$lines, '';
106             }
107              
108 1         27 return 1;
109             }
110              
111             #pod =item ua
112             #pod
113             #pod Return the Mojo::UserAgent object used to fetch resources.
114             #pod
115             #pod =cut
116              
117             sub ua {
118 29     29 1 6231 state $ua = Mojo::UserAgent->new->max_redirects(3);
119 29         406 $ua;
120             }
121              
122             #pod =item lookup( MAC )
123             #pod
124             #pod Given the MAC address, return an anonymous array with the vendor
125             #pod information. The first element is the vendor name, and the remaining
126             #pod elements are the address lines. Different records may have different
127             #pod numbers of lines, although the first two should be consistent.
128             #pod
129             #pod This makes a direct request to the IEEE website for that OUI to return
130             #pod the information for that vendor.
131             #pod
132             #pod The C function explains the possible formats
133             #pod for MAC.
134             #pod
135             #pod =cut
136              
137             sub lookup {
138 1     1 1 2 my $mac = shift;
139              
140 1         14 $mac = normalize_mac( $mac );
141 1         2 my $lines = fetch_oui( $mac );
142              
143 1         6 return $lines;
144             }
145              
146             #pod =item normalize_mac( MAC )
147             #pod
148             #pod Takes a MAC address and turns it into the form I need to
149             #pod send to the IEEE lookup, which is the first six bytes in hex
150             #pod separated by hyphens. For instance, 00:0d:93:29:f6:c2 turns
151             #pod into 00-0D-93.
152             #pod
153             #pod The input string can be a separated by colons or hyphens. They
154             #pod can omit leading 0's (which might make things look odd). We
155             #pod only need the first three bytes
156             #pod
157             #pod 00:0d:93:29:f6:c2 # usual form
158             #pod
159             #pod 00-0d-93-29-f6-c2 # with hyphens
160             #pod
161             #pod 00:0d:93 # first three bytes
162             #pod
163             #pod 0:d:93 # missing leading zero
164             #pod
165             #pod :d:93 # missing all leading zeros
166             #pod
167             #pod The input string can also be a blessed L object.
168             #pod
169             #pod =cut
170              
171             sub normalize_mac {
172 8     8   1824 no warnings 'uninitialized';
  8         19  
  8         14450  
173              
174 29     29 1 7396 my $input = shift;
175              
176 29 50       93 return uc($input->as_microsoft)
177             if ref $input eq 'NetAddr::MAC';
178              
179 29         94 $input = uc $input;
180              
181 29 100       171 do {
182 1         70 Carp::carp "Could not normalize MAC [$input]";
183             return
184 1         4 } if $input =~ m/[^0-9a-f:-]/i;
185              
186             my @bytes =
187 72         200 grep { /^[0-9A-F]{2}$/ }
188 72         274 map { sprintf "%02X", hex }
189 28         162 grep { defined }
  75         151  
190             ( split /[:-]/, $input )[0..2];
191              
192 28 100       94 do {
193 5         549 Carp::carp "Could not normalize MAC [$input]";
194             return
195 5         20 } unless @bytes == 3;
196              
197 23         75 my $mac = join "-", @bytes;
198              
199 23         76 return $mac;
200             }
201              
202             #pod =item fetch_oui( MAC )
203             #pod
204             #pod Looks up the OUI information on the IEEE website, or uses a cached
205             #pod version of it. Pass it the result of C and you
206             #pod should be fine.
207             #pod
208             #pod The C function explains the possible formats for
209             #pod MAC.
210             #pod
211             #pod To avoid multiple calls on the network, use C to preload
212             #pod the entire OUI space into an in-memory cache. This can take a long
213             #pod time over a slow network, though; the file is about 60,000 lines.
214             #pod
215             #pod Also, the IEEE website has been flaky lately, so loading the cache is
216             #pod better. This distribution comes with several versions of the complete
217             #pod OUI data file.
218             #pod
219             #pod =cut
220              
221             sub fetch_oui {
222             # fetch_oui_from_custom( $_[0] ) ||
223 3 50   3 1 2101 fetch_oui_from_cache( $_[0] ) ||
224             fetch_oui_from_ieee( $_[0] );
225             }
226              
227             #pod =item fetch_oui_from_custom( MAC, [ URL ] )
228             #pod
229             #pod Looks up the OUI information from the specified URL or the URL set
230             #pod in the C environment variable.
231             #pod
232             #pod The C function explains the possible formats for
233             #pod MAC.
234             #pod
235             #pod =cut
236              
237             sub fetch_oui_from_custom {
238 3     3 1 5553 my $mac = normalize_mac( shift );
239 3   100     18 my $url = shift // $ENV{NET_MAC_VENDOR_OUI_SOURCE};
240              
241 3 100       11 return unless defined $url;
242              
243 2         14 my $html = __PACKAGE__->_fetch_oui_from_url( $url );
244 2 50       9 unless( defined $html ) {
245 2         161 Carp::carp "Could not fetch data from the IEEE!";
246 2         17 return;
247             }
248              
249             parse_oui(
250 0         0 extract_oui_from_html( $html, $mac )
251             );
252             }
253              
254             #pod =item fetch_oui_from_ieee( MAC )
255             #pod
256             #pod Looks up the OUI information on the IEEE website. Pass it the result
257             #pod of C and you should be fine.
258             #pod
259             #pod The C function explains the possible formats for
260             #pod MAC.
261             #pod
262             #pod =cut
263              
264             sub _search_url_base {
265             # https://services13.ieee.org/RST/standards-ra-web/rest/assignments/download/?registry=MA-L&format=html&text=00-0D-93
266 5     5   35 state $url = Mojo::URL->new(
267             'https://services13.ieee.org/RST/standards-ra-web/rest/assignments/download/?registry=MA-L&format=html'
268             );
269              
270 5         924 $url;
271             }
272              
273             sub _search_url {
274 5     5   14 my( $class, $mac ) = @_;
275 5         17 my $url = $class->_search_url_base->clone;
276 5         359 $url->query->merge( text => $mac );
277 5         1026 $url;
278             }
279              
280             sub fetch_oui_from_ieee {
281 5     5 1 7762 my $mac = normalize_mac( shift );
282              
283 5         31 my @urls = __PACKAGE__->_search_url( $mac );
284              
285 5         10 my $html;
286 5         12 URL: foreach my $url ( @urls ) {
287 5         18 $html = __PACKAGE__->_fetch_oui_from_url( $url );
288 5 100       46 next URL unless defined $html;
289 3         10 last;
290             }
291              
292 5 100       19 unless( defined $html ) {
293 2         256 Carp::carp "Could not fetch data from the IEEE!";
294 2         35 return;
295             }
296              
297             parse_oui(
298 3         18 extract_oui_from_html( $html, $mac )
299             );
300             }
301              
302             sub _fetch_oui_from_url {
303 7     7   13 state $min_ssl = 0x10_00_00_00;
304 7         17 my( $class, $url ) = @_;
305 7         12 my $tries = 0;
306              
307 7         27 my $ssl_version = Net::SSLeay::SSLeay();
308 7         23 my $ssl_version_string = Net::SSLeay::SSLeay_version();
309              
310 7 50       24 if( $ssl_version < $min_ssl ) {
311 0         0 Carp::carp "Fetching OUI might fail with older OpenSSLs. You have [$ssl_version_string] and may need 1.x";
312             }
313              
314 7 50       22 return unless defined $url;
315              
316             TRY: {
317 7         12 my $tx = __PACKAGE__->ua->get( $url );
  26         684  
318 26 100       8575727 if( $tx->error ) {
319 23 100       501 if( $tries > 3 ) {
320 4         16 my $error = $tx->error;
321 4         82 my @messages = (
322             "Failed fetching [$url] HTTP status [$error->{code}]",
323             "message [$error->{message}]"
324             );
325 4 50       1480 push @messages, "You may need to upgrade OpenSSL to 1.x. You have [$ssl_version_string]"
326             if $ssl_version < $min_ssl;
327              
328 4         1239 Carp::carp join "\n", @messages;
329 4         89 return;
330             }
331              
332 19         42 $tries++;
333 19         46005844 sleep 1 * $tries;
334 19         3063 redo TRY;
335             }
336              
337 3         104 my $html = $tx->res->body;
338 3 50       142 unless( defined $html ) {
339 0         0 Carp::carp "No content in response for [$url]!";
340 0         0 return;
341             }
342              
343 3         156 return $html;
344             }
345             }
346              
347             #pod =item fetch_oui_from_cache( MAC )
348             #pod
349             #pod Looks up the OUI information in the cached OUI information (see
350             #pod C).
351             #pod
352             #pod The C function explains the possible formats for
353             #pod MAC.
354             #pod
355             #pod To avoid multiple calls on the network, use C to preload
356             #pod the entire OUI space into an in-memory cache.
357             #pod
358             #pod If it doesn't find the MAC in the cache, it returns nothing.
359             #pod
360             #pod =cut
361              
362             sub fetch_oui_from_cache {
363 4     4 1 769 my $mac = normalize_mac( shift );
364              
365 4         23 __PACKAGE__->get_from_cache( $mac );
366             }
367              
368             #pod =item extract_oui_from_html( HTML, OUI )
369             #pod
370             #pod Gets rid of the HTML around the OUI information. It may still be
371             #pod ugly. The HTML is the search results page of the IEEE ouisearch
372             #pod lookup.
373             #pod
374             #pod Returns false if it could not extract the information. This could
375             #pod mean unexpected input or a change in format.
376             #pod
377             #pod =cut
378              
379             sub extract_oui_from_html {
380 5     5 1 5256 my $html = shift;
381 5         23 my $lookup_mac = normalize_mac( shift );
382              
383 5         158 my( $record ) = $html =~ m|
($lookup_mac.*?)
|is;
384 5         45 $record =~ s|||g;
385              
386 5 100       27 return unless defined $record;
387 4         21 return $record;
388             }
389              
390             #pod =item parse_oui( STRING )
391             #pod
392             #pod Takes a string that looks like this:
393             #pod
394             #pod 00-03-93 (hex) Apple Computer, Inc.
395             #pod 000393 (base 16) Apple Computer, Inc.
396             #pod 20650 Valley Green Dr.
397             #pod Cupertino CA 95014
398             #pod UNITED STATES
399             #pod
400             #pod and turns it into an array of lines. It discards the first
401             #pod line, strips the leading information from the second line,
402             #pod and strips the leading whitespace from all of the lines.
403             #pod
404             #pod With no arguments, it returns an empty anonymous array.
405             #pod
406             #pod =cut
407              
408             sub parse_oui {
409 27016     27016 1 32337 my $oui = shift;
410 27016 100       37708 return [] unless $oui;
411 27008         34490 $oui =~ s|||g;
412 27008 50       413953 my @lines = map { s/^\s+//; $_ ? $_ : () } split /\s*$/m, $oui;
  129330         240272  
  129330         234389  
413 27008         42945 chomp @lines;
414 27008         31149 splice @lines, 1, 1, (); # should have documented this!
415              
416 27008         64366 $lines[0] =~ s/\S+\s+\S+\s+//;
417 27008         56171 return \@lines;
418             }
419              
420             #pod =item oui_url
421             #pod
422             #pod =item oui_urls
423             #pod
424             #pod Returns the URLs of the oui.txt resource. The IEEE likes to move this
425             #pod around. These are the default URL that C will use, but you
426             #pod can also supply your own with the C environment
427             #pod variable.
428             #pod
429             #pod =cut
430              
431 6     6 1 3003738 sub oui_url { (grep { /\Ahttp:/ } &oui_urls)[0] }
  6         68  
432              
433             sub oui_urls {
434 7     7 1 2772 my @urls = 'http://standards-oui.ieee.org/oui.txt';
435              
436             unshift @urls, $ENV{NET_MAC_VENDOR_OUI_URL}
437 7 50       37 if defined $ENV{NET_MAC_VENDOR_OUI_URL};
438              
439 7         18 @urls;
440             }
441              
442             #pod =item load_cache( [ SOURCE[, DEST ] ] )
443             #pod
444             #pod Downloads the current list of all OUIs in SOURCE, parses it with
445             #pod C, and stores it in the cache. The C will
446             #pod use this cache if it exists.
447             #pod
448             #pod By default, this uses the URL from C, but given an argument,
449             #pod it tries to use that.
450             #pod
451             #pod If the url indicates that the data is compressed, the response content
452             #pod is decompressed before being stored.
453             #pod
454             #pod If C cannot load the data, it issues a warning and returns
455             #pod nothing.
456             #pod
457             #pod This previously used DBM::Deep if it was installed, but that was much
458             #pod too slow. Instead, if you want persistence, you can play with
459             #pod C<$Net::MAC::Vendor::Cached> yourself.
460             #pod
461             #pod If you want to store the data fetched for later use, add a destination
462             #pod filename to the request. To fetch from the default location and store,
463             #pod specify C as source.
464             #pod
465             #pod =cut
466              
467             sub load_cache {
468 1     1 1 8089 my( $source, $dest ) = @_;
469              
470 1         10 my $data = do {;
471 1 50       16 if( defined $source ) {
472 1 50       28 unless( -e $source ) {
473 0         0 Carp::carp "Net::Mac::Vendor cache source [$source] does not exist";
474 0         0 return;
475             }
476              
477 1         8 do { local( *ARGV, $/ ); @ARGV = $source; <> }
  1         74  
  1         9  
  1         6607  
478             }
479             else {
480             #say time . " Fetching URL";
481 0         0 my $url = oui_url();
482 0         0 my $tx = __PACKAGE__->ua->get( $url );
483             #say time . " Fetched URL";
484             #say "size is " . $tx->res->headers->header( 'content-length' );
485 0 0       0 ($url =~ /\.bz2/) ? _bunzip($tx->res->body) :
    0          
486             ($url =~ /\.gz/) ? _gunzip($tx->res->body) :
487             $tx->res->body;
488             }
489             };
490              
491 1 50       16 if( defined $dest ) {
492 0 0       0 if( open my $fh, '>:utf8', $dest ) {
493 0         0 print { $fh } $data;
  0         0  
494 0         0 close $fh;
495             }
496             else { # notify on error, but continue
497 0         0 Carp::carp "Could not write to '$dest': $!";
498             }
499             }
500              
501              
502             # The PRIVATE entries fill in a template with no
503             # company name or address, but the whitespace is
504             # still there. We need to split on a newline
505             # followed by some potentially horizontal whitespace
506             # and another newline
507 1         20 my $CRLF = qr/(?:\r?\n)/;
508 1         298008 my @entries = split /[\t ]* $CRLF [\t ]* $CRLF/x, $data;
509 1         17 shift @entries;
510              
511 1         11 my $count = '';
512 1         14 foreach my $entry ( @entries ) {
513 27011         46742 $entry =~ s/^\s+//;
514 27011         34621 my $oui = substr $entry, 0, 8;
515 27011         34972 __PACKAGE__->add_to_cache( $oui, parse_oui( $entry ) );
516             }
517              
518 1         1825 return 1;
519             }
520              
521             sub _bunzip {
522 0     0   0 my $content = shift;
523 0 0       0 if (eval { +require Compress::Bzip2; 1 }) {
  0         0  
  0         0  
524 0         0 return Compress::Bzip2::memBunzip($content);
525             }
526             else {
527 0         0 require File::Temp;
528 0         0 my ($tempfh, $tempfilename) = File::Temp::tempfile( UNLINK => 1 );
529 0         0 binmode $tempfh, ':raw';
530 0         0 print $tempfh $content;
531 0         0 close $tempfh;
532              
533 0 0       0 open my $unzipfh, "bunzip2 --stdout $tempfilename |"
534             or die "cannot pipe to bunzip2: $!";
535 0         0 local $/;
536 0         0 return <$unzipfh>;
537             }
538             }
539              
540             sub _gunzip {
541 0     0   0 my $content = shift;
542 0 0       0 if (eval { +require Compress::Zlib; 1 }) {
  0         0  
  0         0  
543 0         0 return Compress::Zlib::memGunzip($content);
544             }
545             else {
546 0         0 require File::Temp;
547 0         0 my ($tempfh, $tempfilename) = File::Temp::tempfile( UNLINK => 1 );
548 0         0 binmode $tempfh, ':raw';
549 0         0 print $tempfh $content;
550 0         0 close $tempfh;
551              
552 0 0       0 open my $unzipfh, "gunzip --stdout $tempfilename |"
553             or die "cannot pipe to gunzip: $!";
554 0         0 local $/;
555 0         0 return <$unzipfh>;
556             }
557             }
558              
559             #pod =back
560             #pod
561             #pod =head1 Caching
562             #pod
563             #pod Eventually I want people to write their own caching classes so I've
564             #pod created some class methods for this.
565             #pod
566             #pod =over 4
567             #pod
568             #pod =cut
569              
570 0         0 BEGIN {
571 8     8   250 my $Cached = {};
572              
573             #pod =item add_to_cache( OUI, PARSED_DATA )
574             #pod
575             #pod Add to the cache. This is mostly in place for a future expansion to
576             #pod full objects so you can override this in a subclass.
577             #pod
578             #pod =cut
579              
580             sub add_to_cache {
581 27011     27011 1 40157 my( $class, $oui, $parsed ) = @_;
582              
583 27011         78692 $Cached->{ $oui } = $parsed;
584             }
585              
586             #pod =item get_from_cache( OUI )
587             #pod
588             #pod Get from the cache. This is mostly in place for a future expansion to
589             #pod full objects so you can override this in a subclass.
590             #pod
591             #pod =cut
592              
593             sub get_from_cache {
594 4     4 1 9 my( $class, $oui ) = @_;
595              
596 4         28 $Cached->{ $oui };
597             }
598              
599             #pod =item get_cache_hash()
600             #pod
601             #pod Get the hash the built-in cache uses. You should only use this if you
602             #pod were using the old C<$Cached> package variable.
603             #pod
604             #pod =cut
605              
606 0     0 1   sub get_cache_hash { $Cached }
607             }
608              
609             __END__