File Coverage

blib/lib/Parse/DNS/Zone.pm
Criterion Covered Total %
statement 196 205 95.6
branch 60 74 81.0
condition 26 41 63.4
subroutine 24 24 100.0
pod 13 13 100.0
total 319 357 89.3


line stmt bran cond sub pod time code
1             #!/usr/bin/perl
2             # Parse::DNS::Zone - DNS Zone File Parser
3             #
4             # Copyright (c) 2009-2011, 2013, 2015 - Olof Johansson
5             # All rights reserved.
6             #
7             # This program is free software; you can redistribute it and/or
8             # modify it under the same terms as Perl itself.
9              
10             =pod
11              
12             =head1 NAME
13              
14             Parse::DNS::Zone - DNS Zone File Parser
15              
16             =head1 SYNOPSIS
17              
18             use Parse::DNS::Zone;
19              
20             my $pdz = Parse::DNS::Zone->new(
21             zonefile=>'db.example',
22             origin=>'example.org.',
23             );
24              
25             my $a_rr = $pdz->get_rdata(name=>'foo', rr=>'A');
26             my $mx_rr = $pdz->get_rdata(name=>'@', rr=>'MX'); # Get the origin's MX
27              
28             # Getting SOA values
29             my $mname = $pdz->get_mname();
30             my $rname = $pdz->get_rname();
31             my $serial = $pdz->get_serial();
32             # ... etc ...
33              
34             =head1 DESCRIPTION
35              
36             B parses a zonefile, used to define a DNS Zone
37             and gives you the information therein, via an object oriented
38             interface. Parse::DNS::Zone doesn't validate rrdata, except for
39             SOA, and is used to 1) validate the basic structure of the file
40             and 2) extract rdata so you can parse it and validate it yourself.
41              
42             Parse::DNS::Zone supports the zone file format as described in
43             RFC 1034:
44              
45             =over 4
46              
47             =item * $INCLUDE
48              
49             =item * $ORIGIN
50              
51             =item * $TTL (as described in RFC 2308)
52              
53             =back
54              
55             Parse::DNS::Zone does not support $GENERATE in this version.
56              
57             =cut
58              
59 3     3   58904 use 5.010;
  3         11  
  3         187  
60             package Parse::DNS::Zone;
61             our $VERSION = '0.51';
62 3     3   16 use warnings;
  3         9  
  3         159  
63 3     3   16 use strict;
  3         4  
  3         114  
64 3     3   14 use File::Basename;
  3         5  
  3         306  
65 3     3   16 use File::Spec;
  3         4  
  3         59  
66 3     3   13 use Carp;
  3         6  
  3         8806  
67              
68             =head1 CONSTRUCTOR
69              
70             =head2 Parse::DNS::Zone->new( ARGS )
71              
72             =over 4
73              
74             =item B
75              
76             =over 4
77              
78             =item * B
79              
80             Origin
81              
82             =back
83              
84             And additionally, exactly one of the following:
85              
86             =over 4
87              
88             =item * B
89              
90             Path to the zonefile being parsed
91              
92             =item * B
93              
94             The zone, as a string.
95              
96             =back
97              
98             =item B
99              
100             =over 4
101              
102             =item * B
103              
104             If set to a true value, the parser will whine and die if
105             the zonefile doesn't contain a SOA record. (Default: yes)
106              
107             =item * B
108              
109             Specify a basepath, from which included relative zonefiles
110             should be available. If used with the B parameter,
111             this defaults to the directory in which the zonefile is in.
112             For $INCLUDEs to work when passing the zone in as a string,
113             this needs to be specified.
114              
115             =item * B
116              
117             If set to a true value, the parser will append the origin
118             to all unqualified domain names (in certain record types,
119             currently: CNAME, MX, NS, AFSDB, PTR). If some record
120             types are missing from this list, please report that as a
121             bug. (Default: no)
122              
123             This feature do run the risk of becoming stale if new
124             record types are introduced. But if you run into problems,
125             don't hesitate to report it!
126              
127             =back
128              
129             =back
130              
131             =cut
132              
133             sub new {
134 5     5 1 221 my $class = shift;
135 5         27 my $self = {
136             require_soa => 1,
137             append_origin => 0,
138             @_
139             };
140              
141 5 100 66     36 if (not defined $self->{zonestr} and defined $self->{zonefile}) {
142 4         12 $self->{zonestr} = _load_zonefile($self->{zonefile});
143             }
144 5 50       22 if (not defined $self->{zonestr}) {
145 0         0 croak("You need to specify either zonestr or zonefile");
146             }
147              
148             # default basepath is dirname($zonefile)
149 5 100       19 if (not exists $self->{basepath}) {
150 4 50       236 $self->{basepath} = dirname($self->{zonefile}) if
151             defined $self->{zonefile};
152             }
153              
154             # append trailing .
155 5 50       28 $self->{origin} .= '.' if($self->{origin}=~/[^[^\.]$/);
156 5         15 bless($self, $class);
157              
158 5         17 _parse($self);
159              
160 5 50 33     54 if($self->{require_soa} &&
161             (!exists $self->{zone}{$self->{origin}}{soa})) {
162 0         0 croak("No SOA in zonefile");
163             }
164              
165 5         14 _parse_soa($self);
166              
167 5         17 return $self;
168             }
169              
170             =head1 METHODS
171              
172             =head2 General
173              
174             =head3 $pdz->get_rdata(name=>$name, rr=>$rr, n=>$n, field=>$field)
175              
176             Is used to get the data associated with a specific name and rr
177             type. The $name can be as the name appears in the zonefile, or a
178             fqdn (with trailing .) as long as it is tracked by the zonefile.
179             If the n argument is specified, the n:th RR in the RRset is
180             returned. Otherwise, you'll get a complete list of the RRset if
181             you're in list context, or the first RR if you're in scalar
182             context.
183              
184             The $field is the particular component of the resource record to
185             return. It defaults to 'val', which is the actual value of the
186             record. Other possibilities are 'class' (e.g. "IN") and 'ttl'.
187              
188             =cut
189              
190             sub get_rdata {
191 31     31 1 595 my $self = shift;
192 31         102 my $h = {
193             field=>'rdata',
194             @_,
195             };
196              
197 31         34 my ($name, $rr, $field, $n) = @{$h}{qw(name rr field n)};
  31         71  
198              
199 31         79 $name=~s/^\@$/$self->{origin}/g;
200 31         40 $name=~s/\.\@\./\.$self->{origin}/g;
201 31         32 $name=~s/\.\@$/\.$self->{origin}/g;
202 31         33 $name=~s/\@\.$/\.$self->{origin}/g;
203 31 100 100     133 $name .= ".$self->{origin}" if(($name ne $self->{origin}) &&
204             (!($name=~/\.$/)));
205              
206 31 100       60 return $self->{zone}{lc $name}{lc $rr}{lc $field}[$n] if defined $n;
207 29 100       47 return @{$self->{zone}{lc $name}{lc $rr}{lc $field}} if wantarray;
  1         8  
208 28         186 return $self->{zone}{lc $name}{lc $rr}{lc $field}[0];
209             }
210              
211             =head3 $pdz->exists($name)
212              
213             Returns a true value if the name exists, and false otherwise.
214              
215             =cut
216              
217             sub exists {
218 7     7 1 10 my $self = shift;
219 7         7 my $name = shift;
220              
221 7         10 $name=~s/^\@$/$self->{origin}/g;
222 7         9 $name=~s/\.\@\./\.$self->{origin}/g;
223 7         8 $name=~s/\.\@$/\.$self->{origin}/g;
224 7         7 $name=~s/\@\.$/\.$self->{origin}/g;
225 7 100 66     50 $name .= ".$self->{origin}" if(($name ne $self->{origin}) &&
226             (!($name=~/\.$/)));
227              
228 7         31 return exists $self->{zone}{lc $name};
229             }
230              
231             =head3 $pdz->get_rrs($name)
232              
233             Returns a list with all RR types for a specific name
234              
235             =cut
236              
237             sub get_rrs {
238 1     1 1 2 my $self = shift;
239 1         2 my $name = shift;
240 1         1 my @rrs;
241              
242 1         2 $name=~s/^\@$/$self->{origin}/g;
243 1         38 $name=~s/\.\@\./\.$self->{origin}/g;
244 1         2 $name=~s/\.\@$/\.$self->{origin}/g;
245 1         2 $name=~s/\@\.$/\.$self->{origin}/g;
246 1 50 33     13 $name .= ".$self->{origin}" if(($name ne $self->{origin}) &&
247             (!($name=~/\.$/)));
248              
249 1         2 foreach my $k (keys %{$self->{zone}{lc $name}}) {
  1         5  
250 2         4 push @rrs, $k;
251             }
252              
253 1         4 return @rrs;
254             }
255              
256             =head3 $pdz->get_dupes(name=>$name, rr=>$rr)
257              
258             Returns how many RRs of a given type is defined for $name. For a simple
259             setup with a single RR for $name, this will return 1. If you have some
260             kind of load balancing or other scheme using multiple RRs of the same
261             type this sub will return the number of "dupes".
262              
263             =cut
264              
265             sub get_dupes {
266 4     4 1 9 my $self = shift;
267 4         14 my $h = {
268             @_,
269             };
270              
271 4         9 my $name = $h->{name};
272 4         7 my $rr = $h->{rr};
273              
274 4         25 $name=~s/^\@$/$self->{origin}/g;
275 4         8 $name=~s/\.\@\./\.$self->{origin}/g;
276 4         6 $name=~s/\.\@$/\.$self->{origin}/g;
277 4         8 $name=~s/\@\.$/\.$self->{origin}/g;
278 4 100 66     24 $name .= ".$self->{origin}" if(($name ne $self->{origin}) &&
279             (!($name=~/\.$/)));
280              
281 4         5 return int(@{$self->{zone}{lc $name}{lc $rr}{rdata}});
  4         38  
282             }
283              
284             =head3 $pdz->get_names( )
285              
286             Returns a list with all names specified in the zone
287              
288             =cut
289              
290             sub get_names {
291 3     3 1 11 my $self = shift;
292 3         4 my @names;
293              
294 3         7 foreach my $n (keys %{$self->{zone}}) {
  3         19  
295 45         63 push @names, $n;
296             }
297              
298 3         23 return @names;
299             }
300              
301             =head2 SOA
302              
303             =head3 $pdz->get_mname( )
304              
305             Returns the MNAME part of the SOA.
306              
307             =cut
308              
309             sub get_mname {
310 1     1 1 489 my $self = shift;
311 1         6 return $self->{soa}{mname};
312             }
313              
314             =head3 $pdz->get_rname( parse=>{0,1} )
315              
316             Return the RNAME part of the SOA. If parse is set to a value
317             other than 0, the value will be interpreted to show an
318             emailaddress. (default: 0)
319              
320             =cut
321              
322             sub get_rname {
323 2     2 1 4 my $self = shift;
324 2         7 my %p = (
325             parse=>0,
326             @_
327             );
328              
329 2         5 my $ret = $self->{soa}{rname};
330 2 100       7 if($p{parse}) {
331 1         9 my ($user,$host)=$self->{soa}{rname}=~/^([^\.]+)\.(.*)$/;
332 1         5 $ret = "$user\@$host";
333             }
334              
335 2         9 return $ret;
336             }
337              
338             =head3 $pdz->get_serial( )
339              
340             Return the SERIAL value of a SOA.
341              
342             =cut
343              
344             sub get_serial {
345 1     1 1 564 my $self = shift;
346 1         6 return $self->{soa}{serial};
347             }
348              
349             =head3 $pdz->get_refresh( )
350              
351             Return the REFRESH value of a SOA
352              
353             =cut
354              
355             sub get_refresh {
356 1     1 1 2 my $self = shift;
357 1         6 return $self->{soa}{refresh};
358             }
359              
360             =head3 $pdz->get_retry( )
361              
362             Return the RETRY value of a SOA
363              
364             =cut
365              
366             sub get_retry {
367 1     1 1 2 my $self = shift;
368 1         5 return $self->{soa}{retry};
369             }
370              
371             =head3 $pdz->get_expire( )
372              
373             Return the EXPIRE value of a SOA
374              
375             =cut
376              
377             sub get_expire {
378 1     1 1 2 my $self = shift;
379 1         5 return $self->{soa}{expire};
380             }
381              
382             =head3 $pdz->get_minimum( )
383              
384             Return the MINIMUM value of a SOA
385              
386             =cut
387              
388             sub get_minimum {
389 1     1 1 2 my $self = shift;
390 1         7 return $self->{soa}{minimum};
391             }
392              
393             # Is used to populate the zone hash used internally.
394             sub _parse {
395 5     5   10 my $self = shift;
396              
397 5         30 my %zone = $self->_parse_zone(
398             zonestr => $self->{zonestr},
399             origin => $self->{origin},
400             );
401              
402 5         14 undef $self->{zone};
403 5         33 $self->{zone}={%zone};
404             }
405              
406             sub _load_zonefile {
407 8     8   12 my $file = shift;
408 8 50       281 open(my $zonefh, $file) or croak("Could not open $file: $!");
409 8         14 return do { local $/; <$zonefh> }; # slurp
  8         28  
  8         244  
410             }
411              
412             # Is used internally to parse a zone from a filename. will do some
413             # recursion for the $include, so a procedural implementation is needed
414             sub _parse_zone {
415 9     9   11 my $self = shift;
416             # $def_class and $def_ttl are only given when called for included zones
417 9         31 my %opts = @_;
418              
419 9   33     26 my $origin = $opts{origin} // $self->{origin};
420              
421 9         12 my $zonestr = $opts{zonestr};
422 9 100 66     49 if (not defined $zonestr and exists $opts{zonefile}) {
423 4         10 $zonestr = _load_zonefile($opts{zonefile});
424             }
425              
426 9         13 my ($def_class, $def_ttl);
427 9 100       22 if ($opts{included}) {
428 4         8 ($def_class, $def_ttl) = @{\%opts}{qw(default_class default_ttl)};
  4         14  
429              
430             }
431 9         19 my $zonepath = $self->{basepath};
432              
433 9         12 my $mrow;
434             my $prev;
435 0         0 my %zone;
436              
437 9         35 my $zentry = qr/^
438             (\S+)\s+ # name
439             (
440             (?: (?: IN | CH | HS ) \s+ \d+ \s+ ) |
441             (?: \d+ \s+ (?: IN | CH | HS ) \s+ ) |
442             (?: (?: IN | CH | HS ) \s+ ) |
443             (?: \d+ \s+ ) |
444             )? # or
445             (\S+)\s+ # type
446             (.*) # rdata
447             $/ix;
448              
449 9         67 for (split /\n/, $zonestr) {
450 174         186 chomp;
451 174         234 s/;.*$//;
452 174 100       534 next if /^\s*$/;
453 136         547 s/\s+/ /g;
454              
455 136         678 s/^\@ /$origin /g;
456 136         162 s/ \@ / $origin /g;
457 136         125 s/ \@$/ $origin/g;
458              
459             # handles mutlirow entries, with ()
460 136 100       361 if($mrow) {
    100          
461 25         21 $mrow.=$_;
462              
463 25 100       61 next if(! /\)/);
464              
465             # End of multirow
466 5         24 $mrow=~s/[\(\)]//g;
467 5         11 $mrow=~s/\n//mg;
468 5         54 $mrow=~s/\s+/ /g;
469 5         8 $mrow .= "\n";
470              
471 5         9 $_ = $mrow;
472 5         8 undef $mrow;
473             } elsif(/^.*\([^\)]*$/) {
474             # Start of multirow
475 5         9 $mrow.=$_;
476 5         9 next;
477             }
478              
479 111 100       184 if(/^ /) {
480 4         10 s/^/$prev/;
481             }
482              
483 111 50       214 $origin = $1, next if(/^\$ORIGIN ([\w\-\.]+)\s*$/i);
484 111 100       214 $def_ttl = $1, next if(/^\$TTL (\d+)\s*$/i);
485 107 100       200 if(/^\$INCLUDE (\S+)(?: (\S+))?\s*(?:;.*)?$/i) {
486 4 50       15 my $subo=defined $2?$2:$origin;
487              
488 4         9 my $zfile = $1;
489 4 50       14 if($1 !~ m/^\//) {
490 4         85 $zfile = File::Spec->catfile($zonepath, $zfile);
491             }
492              
493 4         25 my %subz = $self->_parse_zone(
494             zonefile => $zfile,
495             included => 1,
496             origin => $subo,
497             default_class => $def_class,
498             default_ttl => $def_ttl,
499             );
500              
501 4         12 foreach my $k (keys %subz) {
502 4         12 $zone{$k}=$subz{$k};
503             }
504 4         11 next;
505             }
506              
507 103         1359 my($name,$ttlclass,$type,$rdata) = /$zentry/;
508              
509 103         108 my($ttl, $class);
510 103 50       194 if(defined $ttlclass) {
511 103         162 ($ttl) = $ttlclass=~/(\d+)/o;
512 103         176 ($class) = $ttlclass=~/(CH|IN|HS)/io;
513              
514 103         128 $ttlclass=~s/\d+//;
515 103         118 $ttlclass=~s/(?:CH|IN|HS)//;
516 103         130 $ttlclass=~s/\s//g;
517 103 50       174 if($ttlclass) {
518 0         0 carp "bad rr: $_ (ttlclass: $ttlclass)";
519 0         0 next;
520             }
521             }
522              
523 103 100       161 $ttl = defined $ttl ? $ttl : $def_ttl;
524 103 100       140 $class = defined $class ? $class : $def_class;
525 103         91 $def_class = $class;
526              
527 103 50 33     377 next if (!$name || !$type || !$rdata);
      33        
528              
529 103 50       163 if(not defined $def_class) {
530 0         0 carp("no class is set");
531 0         0 next;
532             }
533              
534 103 50       141 if(not defined $ttl) {
535 0         0 carp("no ttl is set");
536 0         0 next;
537             }
538              
539 103         92 $prev=$name;
540 103         180 $name = _fqdnize($name, $origin);
541              
542 103 100 100     339 if($self->{append_origin} and
      66        
      100        
543             $type =~ /^(?:cname|afsdb|mx|ns)$/i and
544             $rdata ne $origin and $rdata !~ /\.$/) {
545 3         6 $rdata.=".$origin";
546             }
547              
548 103         99 push(@{$zone{lc $name}{lc $type}{rdata}}, $rdata);
  103         468  
549 103         122 push(@{$zone{lc $name}{lc $type}{ttl}}, $ttl);
  103         305  
550 103         92 push(@{$zone{lc $name}{lc $type}{class}}, $class);
  103         284  
551             }
552              
553 9         104 return %zone;
554             }
555              
556             sub _fqdnize {
557 109     109   2668 my ($name, $origin) = @_;
558              
559 109   100     177 $origin //= '.';
560 109 100       341 $origin .= '.' unless $origin =~ /\.$/;
561              
562 109 100       254 return $name if $name =~ /\.$/;
563 79 100       139 return "$name." if $origin eq '.';
564 78         195 return "$name.$origin";
565             }
566              
567             # Is used to parse the SOA and build the soa hash as used
568             # internally..
569             sub _parse_soa {
570 5     5   7 my $self = shift;
571 5         18 my $soa_rd = get_rdata($self, (name=>"$self->{origin}", rr=>'SOA'));
572 5         42 my($mname,$rname,$serial,$refresh,$retry,$expire,$minimum)=
573             $soa_rd=~/^(\S+) (\S+) (\d+) (\d+) (\d+) (\d+) (\d+)\s*$/;
574              
575 5         18 $self->{soa}{mname}=$mname;
576 5         9 $self->{soa}{rname}=$rname;
577 5         8 $self->{soa}{serial}=$serial;
578 5         13 $self->{soa}{refresh}=$refresh;
579 5         9 $self->{soa}{retry}=$retry;
580 5         8 $self->{soa}{expire}=$expire;
581 5         11 $self->{soa}{minimum}=$minimum;
582             }
583              
584             1;
585              
586             =head1 SEE ALSO
587              
588             RFC 1034, RFC 1035, Bind Administrator's Guide
589              
590             =head1 AVAILABILITY
591              
592             Latest stable version is available on CPAN. Current development
593             version is available on https://github.com/olof/Parse-DNS-Zone, and
594             this is the I place to report issues.
595              
596             =head1 COPYRIGHT
597              
598             Copyright (c) 2009-2011, 2013, 2015 - Olof Johansson
599              
600             All rights reserved.
601              
602             This program is free software; you can redistribute it and/or
603             modify it under the same terms as Perl itself.
604              
605             =cut