File Coverage

blib/lib/Parse/DNS/Zone.pm
Criterion Covered Total %
statement 205 214 95.7
branch 59 74 79.7
condition 26 41 63.4
subroutine 26 26 100.0
pod 13 13 100.0
total 329 368 89.4


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 4     4   103343 use 5.010;
  4         18  
60             package Parse::DNS::Zone;
61             our $VERSION = '0.60';
62 4     4   27 use warnings;
  4         8  
  4         183  
63 4     4   27 use strict;
  4         6  
  4         145  
64 4     4   19 use File::Basename;
  4         8  
  4         582  
65 4     4   29 use File::Spec;
  4         13  
  4         162  
66 4     4   26 use Carp;
  4         7  
  4         15856  
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 187 my $class = shift;
135 5         31 my $self = {
136             require_soa => 1,
137             append_origin => 0,
138             @_
139             };
140              
141 5 100 66     43 if (not defined $self->{zonestr} and defined $self->{zonefile}) {
142 4         15 $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       15 if (not exists $self->{basepath}) {
150             $self->{basepath} = dirname($self->{zonefile}) if
151 4 50       260 defined $self->{zonefile};
152             }
153              
154             # append trailing .
155 5 50       51 $self->{origin} .= '.' if($self->{origin}=~/[^[^\.]$/);
156 5         15 bless($self, $class);
157              
158 5         18 _parse($self);
159              
160 5 50 33     51 if($self->{require_soa} &&
161             (!exists $self->{zone}{$self->{origin}}{soa})) {
162 0         0 croak("No SOA in zonefile");
163             }
164              
165 5         17 _parse_soa($self);
166              
167 5         26 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 39     39 1 589 my $self = shift;
192 39         173 my $h = {
193             field=>'rdata',
194             @_,
195             };
196              
197 39         52 my ($name, $rr, $field, $n) = @{$h}{qw(name rr field n)};
  39         137  
198              
199 39         109 $name=~s/^\@$/$self->{origin}/g;
200 39         51 $name=~s/\.\@\./\.$self->{origin}/g;
201 39         49 $name=~s/\.\@$/\.$self->{origin}/g;
202 39         43 $name=~s/\@\.$/\.$self->{origin}/g;
203 39 100 100     234 $name .= ".$self->{origin}" if(($name ne $self->{origin}) &&
204             (!($name=~/\.$/)));
205              
206 39 100       103 return $self->{zone}{lc $name}{lc $rr}{lc $field}[$n] if defined $n;
207 37 100       67 return @{$self->{zone}{lc $name}{lc $rr}{lc $field}} if wantarray;
  1         8  
208 36         344 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         9 my $name = shift;
220              
221 7         9 $name=~s/^\@$/$self->{origin}/g;
222 7         10 $name=~s/\.\@\./\.$self->{origin}/g;
223 7         6 $name=~s/\.\@$/\.$self->{origin}/g;
224 7         7 $name=~s/\@\.$/\.$self->{origin}/g;
225 7 100 66     55 $name .= ".$self->{origin}" if(($name ne $self->{origin}) &&
226             (!($name=~/\.$/)));
227              
228 7         36 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         3 my $name = shift;
240 1         10 my @rrs;
241              
242 1         3 $name=~s/^\@$/$self->{origin}/g;
243 1         2 $name=~s/\.\@\./\.$self->{origin}/g;
244 1         2 $name=~s/\.\@$/\.$self->{origin}/g;
245 1         1 $name=~s/\@\.$/\.$self->{origin}/g;
246 1 50 33     11 $name .= ".$self->{origin}" if(($name ne $self->{origin}) &&
247             (!($name=~/\.$/)));
248              
249 1         3 foreach my $k (keys %{$self->{zone}{lc $name}}) {
  1         14  
250 2         4 push @rrs, $k;
251             }
252              
253 1         3 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 6 my $self = shift;
267 4         14 my $h = {
268             @_,
269             };
270              
271 4         7 my $name = $h->{name};
272 4         7 my $rr = $h->{rr};
273              
274 4         21 $name=~s/^\@$/$self->{origin}/g;
275 4         7 $name=~s/\.\@\./\.$self->{origin}/g;
276 4         7 $name=~s/\.\@$/\.$self->{origin}/g;
277 4         6 $name=~s/\@\.$/\.$self->{origin}/g;
278 4 100 66     23 $name .= ".$self->{origin}" if(($name ne $self->{origin}) &&
279             (!($name=~/\.$/)));
280              
281 4         3 return int(@{$self->{zone}{lc $name}{lc $rr}{rdata}});
  4         30  
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 12 my $self = shift;
292 3         5 my @names;
293              
294 3         4 foreach my $n (keys %{$self->{zone}}) {
  3         20  
295 69         90 push @names, $n;
296             }
297              
298 3         28 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 407 my $self = shift;
311 1         7 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         8 my %p = (
325             parse=>0,
326             @_
327             );
328              
329 2         4 my $ret = $self->{soa}{rname};
330 2 100       6 if($p{parse}) {
331 1         6 my ($user,$host)=$self->{soa}{rname}=~/^([^\.]+)\.(.*)$/;
332 1         4 $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 421 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         4 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 3 my $self = shift;
368 1         7 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 5 my $self = shift;
379 1         9 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 3 my $self = shift;
390 1         8 return $self->{soa}{minimum};
391             }
392              
393             # Is used to populate the zone hash used internally.
394             sub _parse {
395 5     5   8 my $self = shift;
396              
397             my %zone = $self->_parse_zone(
398             zonestr => $self->{zonestr},
399             origin => $self->{origin},
400 5         31 );
401              
402 5         19 undef $self->{zone};
403 5         55 $self->{zone}={%zone};
404             }
405              
406             sub _load_zonefile {
407 8     8   11 my $file = shift;
408 8 50       383 open(my $zonefh, $file) or croak("Could not open $file: $!");
409 8         18 return do { local $/; <$zonefh> }; # slurp
  8         34  
  8         298  
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   14 my $self = shift;
416             # $def_class and $def_ttl are only given when called for included zones
417 9         37 my %opts = @_;
418              
419 9   33     30 my $origin = $opts{origin} // $self->{origin};
420              
421 9         12 my $zonestr = $opts{zonestr};
422 9 50 66     33 if (not defined $zonestr and exists $opts{zonefile}) {
423 4         12 $zonestr = _load_zonefile($opts{zonefile});
424             }
425              
426 9         12 my ($def_class, $def_ttl);
427 9 100       31 if ($opts{included}) {
428 4         10 ($def_class, $def_ttl) = @{\%opts}{qw(default_class default_ttl)};
  4         17  
429              
430             }
431 9         19 my $zonepath = $self->{basepath};
432              
433 9         9 my $mrow;
434             my $prev;
435 0         0 my %zone;
436              
437 9         53 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         92 for (split /\n/, $zonestr) {
450 234         290 chomp;
451              
452             # Strip away any quoted strings; they should not be parsed in
453             # the same way as the rest of the zonefile. Replace the strings
454             # with placeholders. The real strings are kept in @strs, and
455             # we'll replace them with the real strings again soon.
456 234         335 ($_, my @strs) = _strip_quotes($_);
457              
458             # Strip comments.
459 234         455 s/(?
460 234 100       770 next if /^\s*$/;
461              
462             # Normalize in-line whitespace
463 180         769 s/\s+/ /g;
464              
465 180         340 s/^\@ /$origin /g;
466 180         204 s/ \@ / $origin /g;
467 180         150 s/ \@$/ $origin/g;
468              
469             # Re-add the real strings again.
470 180         304 $_ = _unstrip_quotes($_, @strs);
471              
472             # handles mutlirow entries, with ()
473 180 100       511 if($mrow) {
    100          
474 37         48 $mrow.=$_;
475              
476 37 100       112 next if(! /\)/);
477              
478             # End of multirow
479 9         49 $mrow=~s/[\(\)]//g;
480 9         18 $mrow=~s/\n//mg;
481 9         68 $mrow=~s/\s+/ /g;
482 9         15 $mrow .= "\n";
483              
484 9         15 $_ = $mrow;
485 9         15 undef $mrow;
486             } elsif(/^.*\([^\)]*$/) {
487             # Start of multirow
488 9         21 $mrow.=$_;
489 9         17 next;
490             }
491              
492 143 100       248 if(/^ /) {
493 4         9 s/^/$prev/;
494             }
495              
496 143 50       339 $origin = $1, next if(/^\$ORIGIN ([\w\-\.]+)\s*$/i);
497 143 100       283 $def_ttl = $1, next if(/^\$TTL (\d+)\s*$/i);
498 139 100       277 if(/^\$INCLUDE (\S+)(?: (\S+))?\s*(?:;.*)?$/i) {
499 4 50       18 my $subo=defined $2?$2:$origin;
500              
501 4         9 my $zfile = $1;
502 4 50       13 if($1 !~ m/^\//) {
503 4         86 $zfile = File::Spec->catfile($zonepath, $zfile);
504             }
505              
506 4         42 my %subz = $self->_parse_zone(
507             zonefile => $zfile,
508             included => 1,
509             origin => $subo,
510             default_class => $def_class,
511             default_ttl => $def_ttl,
512             );
513              
514 4         14 foreach my $k (keys %subz) {
515 4         11 $zone{$k}=$subz{$k};
516             }
517 4         12 next;
518             }
519              
520 135         1118 my($name,$ttlclass,$type,$rdata) = /$zentry/;
521              
522 135         295 $rdata =~ s/\s+$//g;
523              
524 135         126 my($ttl, $class);
525 135 50       262 if(defined $ttlclass) {
526 135         188 ($ttl) = $ttlclass=~/(\d+)/o;
527 135         197 ($class) = $ttlclass=~/(CH|IN|HS)/io;
528              
529 135         170 $ttlclass=~s/\d+//;
530 135         178 $ttlclass=~s/(?:CH|IN|HS)//;
531 135         155 $ttlclass=~s/\s//g;
532 135 50       270 if($ttlclass) {
533 0         0 carp "bad rr: $_ (ttlclass: $ttlclass)";
534 0         0 next;
535             }
536             }
537              
538 135 100       235 $ttl = defined $ttl ? $ttl : $def_ttl;
539 135 100       190 $class = defined $class ? $class : $def_class;
540 135         131 $def_class = $class;
541              
542 135 50 33     545 next if (!$name || !$type || !$rdata);
      33        
543              
544 135 50       207 if(not defined $def_class) {
545 0         0 carp("no class is set");
546 0         0 next;
547             }
548              
549 135 50       220 if(not defined $ttl) {
550 0         0 carp("no ttl is set");
551 0         0 next;
552             }
553              
554 135         118 $prev=$name;
555 135         249 $name = _fqdnize($name, $origin);
556              
557 135 100 100     548 if($self->{append_origin} and
      66        
      100        
558             $type =~ /^(?:cname|afsdb|mx|ns)$/i and
559             $rdata ne $origin and $rdata !~ /\.$/) {
560 3         11 $rdata.=".$origin";
561             }
562              
563 135         142 push(@{$zone{lc $name}{lc $type}{rdata}}, $rdata);
  135         729  
564 135         166 push(@{$zone{lc $name}{lc $type}{ttl}}, $ttl);
  135         350  
565 135         119 push(@{$zone{lc $name}{lc $type}{class}}, $class);
  135         453  
566             }
567              
568 9         153 return %zone;
569             }
570              
571             sub _strip_quotes {
572 240     240   3566 local $_ = shift;
573 240         583 my $qstr = qr/(".*?(?
574 240         916 my @strs = /$qstr/g;
575              
576 240         545 for my $str (keys @strs) {
577 26         541 s/\Q$strs[$str]\E/"\$str[$str]"/;
578             }
579              
580 240         812 return $_, @strs;
581             }
582              
583             sub _unstrip_quotes {
584 186     186   3660 return shift =~ s/"\$str\[([0-9]+)\]"/$_[$1]/gr;
585             }
586              
587             sub _fqdnize {
588 141     141   3264 my ($name, $origin) = @_;
589              
590 141   100     236 $origin //= '.';
591 141 100       433 $origin .= '.' unless $origin =~ /\.$/;
592              
593 141 100       308 return $name if $name =~ /\.$/;
594 111 100       220 return "$name." if $origin eq '.';
595 110         282 return "$name.$origin";
596             }
597              
598             # Is used to parse the SOA and build the soa hash as used
599             # internally..
600             sub _parse_soa {
601 5     5   9 my $self = shift;
602 5         23 my $soa_rd = get_rdata($self, (name=>"$self->{origin}", rr=>'SOA'));
603 5         45 my($mname,$rname,$serial,$refresh,$retry,$expire,$minimum)=
604             $soa_rd=~/^(\S+) (\S+) (\d+) (\d+) (\d+) (\d+) (\d+)\s*$/;
605              
606 5         21 $self->{soa}{mname}=$mname;
607 5         10 $self->{soa}{rname}=$rname;
608 5         9 $self->{soa}{serial}=$serial;
609 5         12 $self->{soa}{refresh}=$refresh;
610 5         16 $self->{soa}{retry}=$retry;
611 5         10 $self->{soa}{expire}=$expire;
612 5         14 $self->{soa}{minimum}=$minimum;
613             }
614              
615             1;
616              
617             =head1 SEE ALSO
618              
619             RFC 1034, RFC 1035, Bind Administrator's Guide
620              
621             =head1 AVAILABILITY
622              
623             Latest stable version is available on CPAN. Current development
624             version is available on https://github.com/olof/Parse-DNS-Zone, and
625             this is the I place to report issues.
626              
627             =head1 COPYRIGHT
628              
629             Copyright (c) 2009-2011, 2013, 2015 - Olof Johansson
630              
631             All rights reserved.
632              
633             This program is free software; you can redistribute it and/or
634             modify it under the same terms as Perl itself.
635              
636             =cut