File Coverage

blib/lib/Net/DNS/Zone/Parser.pm
Criterion Covered Total %
statement 28 30 93.3
branch n/a
condition n/a
subroutine 10 10 100.0
pod n/a
total 38 40 95.0


line stmt bran cond sub pod time code
1             # $Id: Parser.pm 788 2008-12-30 17:49:48Z olaf $
2             # Net::DNS::Zone::Parser
3             #
4             # O-O package that implements an RFC complient zone file (pre)parser.
5             #
6             # perldoc Net::DNS::Zone::Parser for details
7             #
8              
9              
10             package Net::DNS::Zone::Parser;
11              
12 5     5   120656 use strict;
  5         13  
  5         206  
13 5         505 use vars qw (
14             $VERSION
15             $REVISION
16             @ISA
17             @EXPORT_OK
18             $ZONE_RR_REGEX
19             $NAMED_COMPILEZONE
20 5     5   26 );
  5         11  
21              
22              
23 5     5   30 use File::Basename;
  5         14  
  5         736  
24 5     5   27 use Carp;
  5         11  
  5         401  
25 5     5   1729398 use File::Temp;
  5         177478  
  5         440  
26 5     5   4551 use IO::File;
  5         5130  
  5         845  
27 5     5   30 use IO::Handle;
  5         11  
  5         165  
28 5     5   4451 use Net::DNS;
  5         4331837  
  5         620  
29 5     5   49 use Net::DNS::RR;
  5         12  
  5         105  
30 5     5   6137 use Shell qw (which);
  0            
  0            
31              
32              
33             BEGIN{
34             require Exporter;
35             @ISA = qw(Exporter);
36             $VERSION = '0.02' ;
37             $REVISION = (qw$LastChangedRevision: 788 $)[1];
38              
39             @EXPORT_OK = qw (
40             processGENERATEarg
41             );
42             }
43              
44              
45              
46              
47             BEGIN
48             {
49              
50             $NAMED_COMPILEZONE = eval {
51             which("named-compilezone") || return(0);
52             open(NAMEDV, "named-compilezone -v |") || return (0);
53             my $namedv=;
54             $namedv=~ /^(\d+)\.(\d+)\./;
55              
56             (($1>=9) && ($2 >= 4)) || return(0);
57              
58             $namedv;
59             };
60             }
61              
62              
63              
64              
65              
66             # Debugging during code development ... Anything greater than 0 will
67             # cause debugging output.
68             use constant DEBUG => 0;
69              
70             my $debug=DEBUG;
71              
72             my $MaxIncludeDepth=20; # maximum time $INCLUDE recursion.
73              
74             ############
75             #
76             # The ZONE_RR_REGEX all classes and types known by Net::DNS::RR and creates
77             # a regexp to match input against.
78             #
79             # This way we match against all know RRs at least those known to
80             # Net::DNS
81             #
82              
83              
84             # the classes regexp component we need elsewhere in the code as well
85             my $classes = join('|', keys %Net::DNS::classesbyname, 'CLASS\\d+');
86              
87              
88             build_regex() unless $ZONE_RR_REGEX;
89              
90             # This code is simalar but not equal to the Net::DNS::RR function.
91             # The resulting regexp is just slightly different.
92             sub build_regex {
93              
94             # Longest ones go first, so the regex engine will match AAAA before A.
95             my $types = join('|', sort { length $b <=> length $a } keys %Net::DNS::typesbyname);
96              
97             $types .= '|TYPE\\d+';
98            
99             $ZONE_RR_REGEX = " ^
100             \\s*
101             (\\S+)+ # name anything non-space will do
102             \\s*
103             (\\d+)?
104             \\s*
105             ($classes)?
106             \\s*
107             ($types) # There must be a type specified.
108             \\s*
109             (.*)
110             \$";
111              
112             print STDERR "Regex: $ZONE_RR_REGEX\n" if DEBUG;
113             }
114              
115              
116             ###################
117             #
118             # new constructor method.
119             # See perldoc for arguments
120             # returns blesssed hash or dies.
121              
122             sub new
123             {
124             my ($caller, $argument)=@_;
125             my $class=ref($caller)||$caller;
126             my $self={};
127             bless ($self,$class);
128              
129             if ($argument){
130             print "new called with an argument\n" if $debug;
131             if ($argument->isa("IO::Handle")){
132             $self->{"fh"}=$argument;
133             }else{
134             die 'Failure: supplied argument is not an instance of IO::File, IO::Handle or related i.o.w. isa( IO::Handle) failed';
135             }
136             }else{
137             $self->{"fh"}=IO::File->new_tmpfile;
138             }
139             return $self;
140             }
141              
142              
143              
144              
145             #
146              
147             ############################
148             # read method
149             # See perldoc for details
150              
151             sub read {
152             my $self=shift;
153             my $possible_filename=shift;
154             my $arghash=shift;
155              
156             my $origin=$arghash->{"ORIGIN"};
157              
158             if ($arghash->{"CREATE_RR"}){
159             $self->{create_rr}=[];
160             }
161              
162             if ($arghash->{"STRIP_SEC"}){
163             $self->{strip_dnskey}=1;
164             $self->{strip_nsec}=1;
165             $self->{strip_rrsig}=1;
166             }
167              
168             if ($arghash->{"STRIP_DNSKEY"}){
169             $self->{strip_dnskey}=1;
170              
171             }
172              
173             if ($arghash->{"STRIP_NSEC"}){
174             $self->{strip_nsec}=1;
175              
176             }
177             if ($arghash->{"STRIP_RRSIG"}){
178             $self->{strip_rrsig}=1;
179              
180             }
181              
182              
183             if ($arghash->{"STRIP_OLD"}){
184             $self->{strip_old}=1;
185              
186             }
187              
188             $self->{bump_soa}=0;
189             if ($arghash->{"BUMP_SOA"}){
190             $self->{bump_soa}=1;
191              
192             }
193              
194              
195              
196              
197             my $fh=$self->{"fh"};
198              
199             my @filename=glob($possible_filename);
200             return "READ FAILURE: ambigueous input: ". join " ", @filename ."\n" if (@filename > 1);
201             $self->{'filename'}=$filename[0];
202              
203             if (defined ($origin) && $origin=~/\S+/o) {
204             # Strip spaces from begin and end of string.
205             $origin=~s/\s//og;
206             $self->{'_origin'}=$origin;
207             }else{
208             $self->{'_origin'} = basename($filename[0]);
209             }
210              
211              
212             $self->{'IncludeRecursionDetector'}=0;
213             $self->{'DefaultTTLDirectiveFound'}=0;
214             $self->{'_origin'}.="." if $self->{'_origin'}!~ /\.$/o;
215             my $returnval=$self->_read($fh,$filename[0],$self->get_origin,'',0);
216             return $returnval if $returnval =~ /^READ FAILURE:/o;
217              
218             return 0;
219             }
220              
221              
222              
223             sub _read {
224              
225             my $self=shift;
226             my $fh_out=shift; # Filehandle to print parsed output to
227             my $filename=shift; # Filename of file to be read
228             my $lastseenORIGIN=shift; # Relevant to relative domains
229             my $previousdname=shift; # Relevant during $INCLUDE
230             my $lastseenTTL=shift;
231             print ";; _read method called on $filename with origin $lastseenORIGIN\n" if $debug;
232              
233              
234             my $namedcomp_return;
235             $namedcomp_return=$self->_read_namedcomp($fh_out,$filename,$lastseenORIGIN) if ($NAMED_COMPILEZONE);
236             print "namedcomp_return returned: $namedcomp_return\n" if $debug;
237             if (defined($namedcomp_return)){
238             if ($namedcomp_return eq "success"){
239             return "success";
240             }else{
241             # Only if the command failed to execute we'll continue with the "perl code";
242             return("READ FAILURE: from named-compilezone: $namedcomp_return") unless $namedcomp_return=~/Failed to execute/;
243             }
244             }
245              
246              
247             $self->{"IncludeRecursionDetector"}++; # Used for testing INCLUDE LOOPS
248             my $fh_in = new IO::File;
249             $fh_in->open("< $filename" ) || return "READ FAILURE: Could not open $filename\n";
250              
251             $lastseenORIGIN.="." if $lastseenORIGIN !~/\.$/o;
252              
253              
254             my $TTL=0;
255             my $defaultTTL=0;
256              
257              
258             # The following loop parses the zone file. At the end of the
259             # paring logic the $_ contains "name TTL CLASS RTYPE RDATA" whith
260             # all variables set and all names expanded to FQDN.
261             #
262             # During the loop the APEX keyset and it's signatures are removed.
263             # Check RFC1035 section 5 for details on how to handle INCLUDES
264             # and how the lastseenORIGIN propagates
265              
266             my $buffer='';
267             my $openingbracketfound=0;
268            
269              
270             READLINE: while (<$fh_in>){
271             next READLINE if /^\s*$/o ; # All spaces
272             print "LINE: >". $_ if DEBUG>1;
273             print "BUFF: >". $buffer."\n\n" if DEBUG>9;
274             my $i=0; # i is a counter to prevent overruns in multiline RRs
275              
276             # Start parsing the line 'token' by 'token'.
277             # As long as there are non whitespace tokens on the
278             # end of the line then (.*)$ matches those.
279             #
280             # $1 either contains a single whitespace or
281             # the longest nonwhitespace collection of characters
282              
283             # Only go through char-by-char lineparsing if there
284             # are parenthesis, quotes or comments or if we are parsing multilines
285             if ( $openingbracketfound ||
286             /\(/o || # Opening bracket
287             /\;/o || # Comment at end of line
288             /\"/o || # Quote
289             /\)/o # Closing bracket
290             ){
291              
292             LINEPARSE: while ( s/^(.)(.*)$/$2/o && $i<200){ # no more than 200
293             # lines for multi-
294             # line RRs
295             print "LINE: ". $_ if DEBUG>10;
296             print "BUFF: ". $buffer."\n\n" if DEBUG>10;
297              
298             my $char=$1;
299             if ( $char eq ';' ){
300             # rest of line is a comment...
301             if ($openingbracketfound) {
302             next READLINE;
303             }else{
304             next READLINE if $buffer=~s/^\s*$//o ; # buffer only
305             # contains spaces
306             last LINEPARSE;
307             }
308             }elsif ( $char eq '(' ){
309             # Maybe we are to strict here and we should just ignore this
310             return "READ FAILURE: Multiple enclosing opening brackets around ". $_ if $openingbracketfound==1;
311             $openingbracketfound=1;
312             }elsif ( $char eq ')' ){
313             return "READ FAILURE: Multiple enclosing closing brackets around ". $_ if $openingbracketfound==0;
314             $openingbracketfound=0;
315             }elsif ($char eq '"'){
316             # We entered a 'character string'
317             # collect everything upto the closing quote
318             $buffer .= '"';
319             my $k=0;
320             CHRSTR: while (s/(.)(.*)/$2/o){
321             $buffer .= $1;
322             $k++;
323             if ($k>256) {
324             print "Character strings should not be longer than 256 chars\n";
325             print "See RFC1035 section 3.3\n";
326             exit;
327             }
328             # Note that end of line will also terminate character
329             # strings.
330             # This may not be RFC complient so we print a warning.
331             last CHRSTR if $1 eq '"';
332             }
333             print "WARNING: Unmatched quotes at end of line\n" if $buffer !~ /\"$/o;
334             }else{
335             # Single spaces between the tokens.. we depend on this later
336             if ($char=~/^\s+$/o){
337             $buffer.=" " unless $buffer=~/\s$/o;
338             }else{
339             $buffer.=$char;
340             }
341             }
342             $i++;
343             # Next line if we are at end of line and there is still a open bracket
344             # not matched.
345             next READLINE if $openingbracketfound && /^\s*$/o;
346             } # END LINEPARSE
347              
348             # LINE HAS NOW BEEN PARSED.. ALL MULTILINES ARE ON ONE LINE AND
349             #
350             $buffer=~s/\s*$//go; # remove possible trailing spce
351             $_=$buffer;
352             $buffer='';
353             }else{ # when not parsing the line char by char
354             s/\s+/ /go; # Remove extra spaces
355             s/\s*$//go; # Remove extra spaces
356             }
357              
358             print "READLINE:>>".$_."<<\n" if DEBUG >2;
359              
360             if ( /^\s*\$TTL\s+(\d+)/o){ #FOUND a $TTL directive
361             $lastseenTTL=$1;
362             $defaultTTL=$lastseenTTL if (! $defaultTTL );
363             print ";; DEFAULT TTL found : ". $lastseenTTL ."\n" if DEBUG>1;
364             $self->{'default_ttl'}=$defaultTTL;
365             next READLINE;
366             }
367              
368             # replace the @ by the ORIGIN.. as given by the argument.
369             s/@/$lastseenORIGIN/;
370              
371             # Set the current originin. This is the one from the $ORIGIN value from
372             # the zone file. It will be used to complete dnames below.
373             if ( /^\s*\$ORIGIN\s+(\S+)\s*$/o){
374             $lastseenORIGIN=$1;
375             print ";; lastseenORIGIN set to : ". $lastseenORIGIN ."\n" if DEBUG>1;
376             next READLINE;
377             }
378              
379              
380              
381              
382             if ( /^\s*\$INCLUDE\s+(\S+)\s*(\S*)?$/io){
383             my $newfilename=$1;
384             $lastseenORIGIN=$2 if $2;
385             if ($newfilename=~/\//o){
386             # absolute pathname
387             }else{
388             #relative pathname
389             $newfilename=dirname($self->{'filename'})."/".$1; # Relative path...
390             }
391              
392             # Deep recursion is still possible....
393             return "READ FAILURE: Including $filename from itself would cause deep recursion\n" if ($filename eq $newfilename);
394             # Other recursion check
395              
396             return "READ FAILURE: Nested INCLUDE more than 20 levels deep... \n".
397             "check if the files are not including in loops..." if
398             $self->{"IncludeRecursionDetector"} > $MaxIncludeDepth;
399              
400              
401             # RFC 1035 section 5 specifies that the lastseenORIGIN does not traverse
402             # INCLUDES but is unclear on the last seen TTL. We use the lastseen TTL
403             # from the included file
404             $lastseenTTL=$self->_read($fh_out,$newfilename,$lastseenORIGIN,$previousdname,
405             $lastseenTTL);
406              
407             return $lastseenTTL if $lastseenTTL =~ /^READ FAILURE:/o;
408             next READLINE;
409             }
410              
411             # Use the previous dname if no dname was qualified (line starts with blanks)
412             if (/^(\S+)\s+/o){
413             $previousdname=$1;
414             # below is a uggly bug fix.
415             $previousdname=$lastseenORIGIN if ($previousdname eq '$GENERATE');
416             $previousdname=$lastseenORIGIN if ($previousdname eq '$INCLUDE');
417             }else{
418             $_ = $previousdname . $_;
419             }
420              
421              
422             # $_ now either contains a GENERATE statement or a line with not
423             # fully qualified domain names in both owner name as RDATA and
424             # with possibly unqualified TTL and CLASS.
425              
426             if (
427             m{^\s*\$GENERATE #Generate directive
428             \s+((\d+)-(\d+)(/(\d+))?) #Range start-stop or start-stop/step.
429             \s+(\S+) #The LHS
430             \s+(\S+) #The TYPE
431             \s+(\S+) #The RHS
432             }xo){
433             print "Range: $2-$3 " if DEBUG;
434             print "/$5 " if DEBUG && $5;
435             print "LHS: $6 "
436             . "TYPE: $7 "
437             . "RHS: $8 \n" if DEBUG ;
438             my $RANGE=$1;
439             my $LOW=$2;
440             my $HIGH=$3;
441             my $STEP=$5?$5:1;
442             my $LHS=$6;
443             my $TYPE=$7;
444             my $RHS=$8;
445             if ($TYPE !~ /PTR|CNAME|DNAME|A|AAAA|NS/o)
446             {
447             print "Generate only supports PTR, CNAME, DNAME, A, AAAA and NS.\n";
448             next READLINE;
449             }
450             if ($LOW>$HIGH){
451             print "Range should be increasing.\n";
452             print "Skipping the following \$GENERATE directive:\n".$_;
453             next READLINE;
454             }
455             if ($LOW<0||$STEP<0){
456             print "Sorry all vallues in the range need to be positive";
457             print "Skipping the following \$GENERATE directive:\n".$_;
458             next READLINE;
459             }
460             my $i=$LOW;
461             while ($i<=$HIGH){
462              
463             my $ownername =processGENERATEarg($LHS,$i,$lastseenORIGIN);
464              
465             my $my_generated_record= $ownername ." ".$lastseenTTL." IN ".$TYPE." ";
466             if ($TYPE =~/CNAME|PTR|DNAME|NS/o){
467             # These types have expansion of the RDATA to FQDN
468             my $rdatastr= processGENERATEarg($RHS,$i,$lastseenORIGIN);
469             $my_generated_record .= $rdatastr;
470             if (($TYPE =~/CNAME|DNAME/) &&
471             ($ownername eq $rdatastr) ){
472             $i+=$STEP;
473             next;
474             }
475            
476             }else{
477             # A and AAAA are left alone
478             $my_generated_record .= processGENERATEarg($RHS,$i,"");
479             }
480             print ";; GENERATE: ". $my_generated_record ."\n" if DEBUG;
481              
482             print $fh_out $my_generated_record ."\n";
483              
484             if (defined $self->{"create_rr"}){
485             my $rr=Net::DNS::RR->new($my_generated_record);
486            
487             push @{$self->{"create_rr"}},$rr;
488             }
489             $i+=$STEP;
490             }
491              
492              
493             }else{
494             my $returnval= $self->_parseline($_,$lastseenORIGIN,$lastseenTTL);
495             next READLINE if $returnval =~ /^__SKIPPED__$/o;
496             return $returnval if $returnval =~ /^READ FAILURE:/o;
497             $_ = $returnval;
498             print ";; " . $_ . "\n" if DEBUG>2;
499            
500             print $fh_out $_ . "\n";
501             if (defined $self->{"create_rr"}){
502             my $rr=Net::DNS::RR->new($_);
503             push @{$self->{"create_rr"}},$rr;
504             }
505             }
506             }
507             # Done parsing this file.
508             $fh_in->close;
509             $self->{"IncludeRecursionDetector"}--;
510             print ";; returning from _read\n" if DEBUG>2;
511             return $lastseenTTL;
512             }
513              
514              
515              
516              
517             ####################################
518             # Few access methods.
519             # see perldoc for details
520              
521             sub get_io {
522             my $self=shift;
523             return $self->{"fh"};
524             }
525              
526              
527             sub get_array {
528             my $self=shift;
529            
530             return [] unless $self->{create_rr};
531             return $self->{create_rr};
532             }
533              
534              
535             sub get_origin {
536             my $self=shift;
537             return $self->{'_origin'};
538             }
539              
540              
541              
542              
543             #
544             # Internal functions.
545              
546              
547             #####################################
548             # complete_dname will append the origin to the input string if needed.
549             # Does a sanity check on escaped \.
550              
551             sub _complete_dname
552             {
553             my $self=shift;
554             my $dname=shift;
555             my $origin=shift;
556             if ( $dname !~ /\.$/o && $dname !~ /\\\.$/o){ # Hmmmm what if a label ends in an escapped \.
557              
558             $dname .= ".".$origin;
559             # This fixes a bug, If the origin equals the root the above line
560             # caused two trailing dots to be added.
561             chop $dname if $origin eq ".";
562             }
563              
564              
565             return $dname;
566             }
567              
568              
569             ####################################################
570             # processGENERATEarg
571             #
572             # this function is used to expand lhs or rhs variables in
573             # a generate statment.
574             # it takes the lhs or rhs string and and the current vallue of
575             # the itterator as input and returns the beast fully expanded according
576             # to the following rules.
577              
578              
579             #lhs describes the owner name of the resource records to be
580             #created. Any single $ symbols within the lhs side are replaced by the
581             #iterator value. To get a $ in the output you need to escape the $
582             #using a backslash \, e.g. \$. The $ may optionally be followed by
583             #modifiers which change the offset from the interator, field width and
584             #base. Modifiers are introduced by a { immediately following the $ as
585             #${offset[,width[,base]]}. e.g. ${-20,3,d} which subtracts 20 from the
586             #current value, prints the result as a decimal in a zero padded field
587             #of with 3. Available output forms are decimal (d), octal (o) and
588             #hexadecimal (x or X for uppercase). The default modifier is
589             #${0,0,d}. If the lhs is not absolute, the current $ORIGIN is appended
590             #to the name.
591              
592              
593              
594             # See perldoc
595              
596              
597             sub processGENERATEarg {
598             my $lhs=shift;
599             my $i=shift;
600             my $origin=shift;
601              
602             my $expanded="";
603             while ($lhs) {
604             my $remaining="";
605             if ($lhs =~ s/^(\S*?)
606             ((?
607             /$2/x){
608             $expanded.=$1 if $1;
609             $lhs=~ s/^\$(\{(\d+)(,(\d+))?(,(\w+))?\})?(.*)\s?$/$7/;
610             # $lhs=~ s/\$//;
611             my $offset=$2?$2:0;
612             my $width=$4?$4:0;
613             my $format=$6?$6:"d";
614             if ($format !~ /d|o|x|X/o){
615             die "Fatal error in parsing the format in a \$GENERATE statement.\n Should be d,o,x or X\n";
616             }
617             $expanded .= sprintf("%0$width$format",$i+$offset);
618             }else{
619             $expanded.=$lhs;
620             $lhs="";
621              
622             }
623             }
624             $expanded =~s/\\\$/\$/og; #finally substitute '$' for the escaped \$
625            
626             # Only expand to FQDN if the last char is a "." and if the
627             # the $origin argument is not empty.
628              
629             $expanded .= ".". $origin if $expanded !~ /\.$/o && $origin ne "" ;
630              
631             return $expanded;
632             }
633              
634              
635             ###################################
636             #
637             # parseline will complete an inputline of the form []
638             # [] to a line with fully qualified names in
639             # the dname and the RDATA, it will insert the CLASS and TTL if not
640             # specified. The arguments are the lastseenORIGIN and lastseenTTL
641             # that are used to complete the domain names with, and to add to fill
642             # in the unqualified TTLs.
643             #
644              
645             # returns 0 on success
646             # returns string starting with "READ FAILURE:" on error.
647              
648             # returns the string "__SKIPPED__" if a line was skipped (see the
649             # argumens to the read method such as STRIP_SEC &c).
650              
651             sub _parseline {
652             my $self=shift;
653             $_=shift;
654             my $lastseenORIGIN=shift; # vallue of the last seen $ORIGIN directive
655             my $lastseenTTL=shift;
656              
657             my $ttl;
658              
659             my $rtype='';
660             my $rdata='';
661             my $prefix='';
662              
663             ($_ =~ m/$ZONE_RR_REGEX/xso) ||
664             return "READ FAILURE: \"".$_."\" did not match RR pattern.\nPlease clean your zonefile!\n";
665              
666             my $dname=$1;
667              
668             s/^\s*(\S+) / /o; # remove the dname to put it back fully qualified
669             # If there is a match it could still be matching the
670             # string 0, so just testing on $1 will now work....
671             if ( $1 || $1 eq "0" ) {
672             $dname=$1;
673              
674             $dname=$self->_complete_dname($dname,$lastseenORIGIN);
675             $_ = $dname . $_;
676             print ";; read DNAME: " . $dname ."\n" if DEBUG>2 ;
677             }else{
678             return "READ FAILURE: Couldn't match dname in read method while reading\n". $_ . " \nthis Should not happen\n";
679             }
680              
681              
682              
683             # See if there is a TTL value, if not insert one
684             if (/^\S+ (\d+)/o) {
685             print ";; TTL : " . $1. "\n" if $debug>2 ;
686             $ttl=$1;
687             }else {
688            
689             # RFC 1035
690             # 'Omitted class and TTL values are default to the
691             # last explicitly stated values"
692            
693             # I take that to mean last explicitly stated in a $TTL
694             # statement. (Purerely because of bind9 compatibility)
695            
696             # instert last seen TTL
697            
698             s/^(\S+) (.*)$/$1 $lastseenTTL $2/;
699            
700             }
701              
702              
703             # See if there is the CLASS is defined, if not insert one.
704             if(! /^\S+ \d+ ($classes)/){
705             #insert IN
706             s/^(\S+ \d+ )(.*)$/$1IN $2/o;
707             }
708             # We have everything specified.. We now get the RTYPE AND RDATA...
709             /^\S+ \d+ ($classes) (\S+) (.*)$/;
710             if ($1) {
711             print ";; rtype: " . $2 ."\n" if DEBUG>2 ;
712             $rtype=$2;
713             }else{
714             return "READ FAILURE: We expected to match an RTYPE\n". $_ . " \nthis Should not happen\n";
715             }
716             if ($3) {
717             $rdata=$3;
718             print ";; rdata:-->" . $rdata ."<---\n" if DEBUG>2 ;
719            
720             }else{
721             return "READ FAILURE: We expected to find RDATA in the following record\n". $_ . " \ncheck your zonefile\n";
722             }
723              
724              
725             if (defined $ttl) {
726             $prefix=$dname." ".$ttl." IN ".$rtype." ";
727             }else{
728             $prefix=$dname." ".$lastseenTTL." IN ".$rtype." ";
729             }
730              
731             # Expand to FQDN in the RDATA.
732             #
733             # We apply a regular expression to the rdata and expand dnames in there
734             # to fully qualified dnames using the complete_dname function.
735              
736             if ( uc $rtype eq "NS" ){
737             #"NS" RFC 1035, Section 3.3.11
738             # the pattern below is appropriate if the rdata only contains a dname
739             # or the dname is the last item in the RDATA string
740             $_=$prefix.$self->_complete_dname($rdata,$lastseenORIGIN);
741             # skipping
742             # "MD" RFC 1035, Section 3.3.4 (obsolete)
743             # "MF" RFC 1035, Section 3.3.5 (obsolete)
744             } elsif ( uc $rtype eq "CNAME" ){
745             # "CNAME" RFC 1035, Section 3.3.1
746             $_=$prefix.$self->_complete_dname($rdata,$lastseenORIGIN);
747             }elsif ( uc $rtype eq "SOA" ){
748             # "SOA" RFC 1035, Section 3.3.13
749             # first two strings in the SOA rdata are dnames
750             $rdata=~/(\S+)\s+(\S+)\s+(\d+)\s+(.*)$/o;
751             return "READ FAILURE: Soa serial not found in\n $_\n" if ( ! $3 && $3 ne "0" );
752              
753             my $soaserial=$3;
754             $soaserial++ if $self->{"bump_soa"};
755             $_=$prefix.$self->_complete_dname($1,$lastseenORIGIN).
756             " " . $self->_complete_dname($2,$lastseenORIGIN).
757             " " . $soaserial ." ". $4 ; #
758            
759             # Additional sanity check.
760             if ( lc($dname) ne lc($self->{'_origin'})){
761             print "WARNING: ORIGIN as specified or determined from the file name\n";
762             print " does not match the SOA ownername. I'll be using the ownername!\n";
763             print " origin set from ". $self->{'_origin'} ." to: ".$dname ."\n";
764             $self->{'_origin'}=$dname;
765             }
766              
767             }elsif( uc $rtype eq "MB" ){
768             # "MB" RFC 1035, Section 3.3.3
769              
770             $_=$prefix.$self->_complete_dname($rdata,$lastseenORIGIN);
771             }elsif( uc $rtype eq "PTR" ){
772             $_=$prefix.$self->_complete_dname($rdata,$lastseenORIGIN);
773             }elsif( uc $rtype eq "MG"){
774             # "MG" RFC 1035, Section 3.3.6
775             $_=$prefix.$self->_complete_dname($rdata,$lastseenORIGIN);
776             }elsif( uc $rtype eq "MR"){
777             # "MR" RFC 1035, Section 3.3.8
778             $_=$prefix.$self->_complete_dname($rdata,$lastseenORIGIN);
779             # skipping
780             # "NULL" RFC 1035, Section 3.3.10
781             # "WKS" RFC 1035, Section 3.4.2 (deprecated, and no dname)
782             }elsif( uc $rtype eq "PTR"){
783             # "PTR" RFC 1035, Section 3.3.12
784             $_=$prefix.$self->_complete_dname($rdata,$lastseenORIGIN);
785             #skipping
786             # "HINFO" RFC 1035, Section 3.3.2
787             }elsif( uc $rtype eq "MINFO"){
788             # "MINFO" RFC 1035, Section 3.3.7
789             $rdata=~/(\S+) (\S+)$/o;
790             $_=$prefix.$self->_complete_dname($1,$lastseenORIGIN).
791             " " . $self->_complete_dname($2,$lastseenORIGIN);
792             }elsif( uc $rtype eq "MX"){
793             # "MX" RFC 1035, Section 3.3.9
794             $_=$prefix.$self->_complete_dname($rdata,$lastseenORIGIN);
795             # skipping
796             # "TXT" RFC 1035, Section 3.3.14
797             }elsif( uc $rtype eq "RP"){
798             # "RP" RFC 1183, Section 2.2
799             $rdata=~/(\S+) (\S+)$/o;
800             $_=$prefix.$self->_complete_dname($1,$lastseenORIGIN).
801             " " . $self->_complete_dname($2,$lastseenORIGIN);
802             }elsif( uc $rtype eq "AFSDB"){
803             # "AFSDB" RFC 1183, Section 1
804             $_=$prefix.$self->_complete_dname($rdata,$lastseenORIGIN);
805             # skipped
806             # "X25" RFC 1183, Section 3.1
807             # "ISDN" RFC 1183, Section 3.2
808             }elsif( uc $rtype eq "RT"){
809             # "RT" RFC 1183, Section 3.3
810             $_=$prefix.$self->_complete_dname($rdata,$lastseenORIGIN);
811             # skipped
812             # "NSAP" RFC 1706, Section 5
813             }elsif(uc $rtype eq "SIG"){
814             # "SIG" RFC 2555, Section 4.1
815             return "__SKIPPED__" if $self->{'strip_old'};
816             my ($typecovered, $algoritm,
817             $type, $orgttl, $sigexpiration,
818             $siginception, $keytag,$signame,$sig) =
819             $rdata=~/^\s*(\S+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\S+)\s+(.*)/o;
820             $_ = $prefix." $typecovered $algoritm $type $orgttl $sigexpiration $siginception $keytag ".
821             $self->_complete_dname($signame,$lastseenORIGIN)." $sig";
822              
823             }elsif(uc $rtype eq "PX"){
824             # "PX" RFC 2163,
825             my ($preference,$map822,$mapx400)= $rdata=~/(\d+) (\S+) (\S+)$/o;
826             $_=$prefix." ".$preference." ".
827             $self->_complete_dname($map822,$lastseenORIGIN).
828             " ".
829             $self->_complete_dname($mapx400,$lastseenORIGIN);
830             }elsif(uc $rtype eq "KEY"){
831             # NOTHING
832            
833             # skipped
834             # "GPOS" RFC 1712 (obsolete)
835             # "AAAA" RFC 1886, Section 2.1
836             # "LOC" RFC 1876
837            
838             }elsif( uc $rtype eq "NXT"){
839             return "__SKIPPED__" if $self->{'strip_old'};
840             # "NXT" RFC 2535
841             $rdata=~/(\S+) (.*)$/o;
842             $_=$prefix.$self->_complete_dname($1,$lastseenORIGIN).
843             " " . $2;
844            
845            
846             # "EID" draft-ietf-nimrod-dns-xx.txt
847             # "NIMLOC" draft-ietf-nimrod-dns-xx.txt
848             }elsif( uc $rtype eq "SRV"){
849             # "SRV" RFC 2782
850             $_=$prefix.$self->_complete_dname($rdata,$lastseenORIGIN);
851             # skipped
852            
853             # "ATMA" [Dobrowski]
854             # skipped... hmmmmm...
855              
856             }elsif( uc $rtype eq "NAPTR"){
857             # "NAPTR" RFC 2168, 2915
858             $rdata=~/(.*) (\S+)$/o;
859             $_=$prefix.$1." ".$self->_complete_dname($2,$lastseenORIGIN);
860             }elsif( uc $rtype eq "KX"){
861             # "KX" RFC 2230
862             $_=$prefix.$self->_complete_dname($rdata,$lastseenORIGIN);
863             # skipped
864             # "CERT" RFC 2358
865             # "A6" RFC 2874
866             }elsif( uc $rtype eq "DNAME"){
867             # "DNAME" RFC 2672
868             $_=$prefix.$self->_complete_dname($rdata,$lastseenORIGIN);
869            
870             #skipped
871             # "SINK" [Eastlake] # I don't know about this RR
872             # "OPT" RFC 2671
873              
874             # "APL" RFC 3123 NO dname in RDATA
875             # "DS" NO dname in RDATA
876             # "SSHFP NO dname in RDATA
877            
878             }elsif( uc $rtype eq "NSEC"){
879             # "NSEC"
880             return "__SKIPPED__" if $self->{'strip_nsec'};
881             $rdata=~/(\S+) (.*)$/o;
882             $_=$prefix.$self->_complete_dname($1,$lastseenORIGIN).
883             " " . $2;
884             }elsif( uc $rtype eq "DNSKEY"){
885             # "DNSKEY"
886             return "__SKIPPED__" if $self->{'strip_dnskey'};
887            
888             }elsif( uc $rtype eq "RRSIG"){
889             # "RRSIG"
890             return "__SKIPPED__" if $self->{'strip_rrsig'};
891             my ($typecovered, $algoritm,
892             $type, $orgttl, $sigexpiration,
893             $siginception, $keytag,$signame,$sig) =
894             $rdata=~/^\s*(\S+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\S+)\s+(.*)/o;
895             return "__SKIPPED__" if $self->{'strip_dnskey'} && uc($typecovered) eq "DNSKEY";
896             return "__SKIPPED__" if $self->{'strip_nsec'} && uc($typecovered) eq "NSEC";
897             return "__SKIPPED__" if $self->{'bump_soa'} && uc($typecovered) eq "SOA";
898             $_ = $prefix." $typecovered $algoritm $type $orgttl $sigexpiration $siginception $keytag ".
899             $self->_complete_dname($signame,$lastseenORIGIN)." $sig";
900              
901              
902            
903             }elsif( uc $rtype=~/TYPE\d+/o){
904             # Unknown RR.
905             }
906              
907             return $_;
908              
909             }
910              
911              
912              
913              
914              
915             # Use named-compilezone -D to do the processin;
916              
917             sub _read_namedcomp{
918             my $self=shift;
919             my $fh_out=shift;
920             my $filename=shift;
921             my $origin=shift;
922             my $tmpfh = File::Temp->new();
923             my $tmpfname = $tmpfh->filename;
924            
925             print ";; Tempfilename: $tmpfname\n" if $debug;
926              
927             print ";; _read_namedcomp: $filename $origin\n" if $debug;
928             my $cmd="named-compilezone -i none -o $tmpfname $origin $filename";
929             print ";; Running: ".join(" ", $cmd)."\n" if $debug;
930              
931             my @result=`$cmd`;
932             if ($debug){
933             foreach my $i (@result){
934             print ";;; $i\n";
935             }
936             }
937             my $lastresult=pop( @result);
938            
939            
940             if ( $? == -1 ) {
941             return "command failed: $!\n"
942             }elsif($lastresult =~/failed/){
943             return $lastresult;
944             }
945              
946            
947             open(DUMP,$tmpfname)|| return ("Could not execute ". join(" ", $cmd)) ; # This should cause classic parsing
948             my $loadzone_result="";
949              
950              
951             $origin=~ s/\.$// unless ($origin eq ".") ;
952            
953              
954              
955             my $ProcessedApex;
956            
957             CONTENT: while () {
958             if (/^\S+\s+\d+\s+IN\s+(SOA|RRSIG\s+\w+|DNSKEY|NSEC|SOA|NXT|SIG)\s+/o){
959             my $type=$1;
960             $self->{strip_dnskey}&& ($type eq "DNSKEY")&& next CONTENT;
961             # Also strip the sig over DNSKEY if we are stripping DNSKEYS
962             if ($type =~ /RRSIG\s+(\w+)/){
963             $self->{strip_rrsig} && next CONTENT;
964             $self->{strip_dnskey} && ($1 eq "DNSKEY") && next CONTENT;
965             $self->{strip_nsec} && ($1 eq "NSEC") && next CONTENT;
966             $self->{bump_soa} && ($1 eq "SOA") && next CONTENT;
967             }
968              
969             $self->{strip_rrsig}&& ($type eq "RRSIG")&& next CONTENT;
970             $self->{strip_nsec}&& ($type eq "NSEC")&& next CONTENT;
971             $self->{strip_old} &&
972             (($type eq "NXT") || ($type eq "SIG"))&&
973             next CONTENT;
974             if ($self->{bump_soa} && ($type eq "SOA")){
975             my $soa=Net::DNS::RR->new($_);
976             $soa->serial($soa->serial()+1);
977             $_=$soa->string ."\n"; # The newline is FITAL, the next record
978             # would otherwise disapear behind a
979             # comment.
980             }
981             }
982            
983             print $fh_out $_;
984             if (defined $self->{"create_rr"}){
985             my $rr=Net::DNS::RR->new($_);
986             push @{$self->{"create_rr"}},$rr;
987             }
988            
989            
990             }
991            
992             print $fh_out "\n"; # Make sure file ends with newline
993             close(DUMP);
994             return ("success");
995              
996              
997             }
998              
999             sub DESTROY {
1000              
1001             close(DUMP);
1002             }
1003              
1004              
1005              
1006             1;
1007              
1008              
1009              
1010              
1011              
1012             #
1013              
1014              
1015              
1016             =head1 NAME
1017              
1018             Net::DNS::Zone::Parser - A Zone Pre-Parser
1019              
1020             =head1 SYNOPSIS
1021              
1022             C
1023              
1024             =head1 DESCRIPTION
1025              
1026             The Net::DNS::Zone::Parser should be considered a preprocessor that
1027             "normalizes" a zonefile.
1028              
1029             It will read a zonefile in a format conforming to the relevant RFCs
1030             with the addition of BIND's GENERATE directive from disk and will
1031             write fully specified resource records (RRs) to a filehandle. Whereby:
1032              
1033             =over
1034              
1035             =item - all comments are stripped;
1036              
1037             =item - there is one RR per line;
1038              
1039             =item - each RR is fully expanded i.e. all domain names are fully qualified
1040             (canonicalised) and the CLASS and TTLs are specified.
1041              
1042             =item - Some RRs may be 'stripped' from the source or otherwise
1043             processed. For details see the 'read' method.
1044              
1045             =back
1046              
1047             Note that this module does not have a notion of what constitutes a
1048             valid zone; it only parses. For example, the parser will happilly
1049             parse RRs with ownernames that are below in another zone because a NS
1050             RR elsewhere in the zone.
1051              
1052              
1053              
1054             =head1 METHODS
1055              
1056             =head2 new
1057              
1058             my $parser=Net::DNS::Zone::Parser->new($io);
1059              
1060             Creates the a parser instance.
1061              
1062             The optional argument should be a IO::File or IO::Handle type of
1063             object. If not specified a temporary IO::File type object will be
1064             created to which the lines will be printed. This object can be
1065             obtained using the get_io method
1066              
1067             =head2 get_io
1068              
1069             my $io=$parser->get_io;
1070             $io->seek(0,0);
1071             print while (< $io >);
1072              
1073              
1074             Returns the filehandle to which the zone file has been written. This
1075             is either the filehandle specified as argument to the new() method or
1076             one that points to a temporary file.
1077              
1078             =head2 read
1079              
1080              
1081             my $parser=Net::DNS::Zone::Parser->new;
1082             $parser->read("/tmp/example.foo");
1083             $parser->read("/tmp/foo.db",
1084             { ORIGIN => "example.db",
1085             };
1086              
1087              
1088             # alternatively
1089              
1090             $returnval=$parser->read("/tmp/foo.db",
1091             { ORIGIN => "example.db",
1092             CREATE_RR => 1,
1093             STRIP_SEC => 1,
1094             };
1095             if ($returnval) {
1096             die $returnval;
1097             }else{
1098             $RRarrayref=$parser->get_array();
1099             }
1100              
1101             'read' reads a zonefile from disk to 'pre-processes' it. The first
1102             argument is a path to the zonefile. The second parameter is a hash
1103             with optional arguments to tweak the reading.
1104              
1105             The read method returns 0 on success and a string starting with "READ
1106             FAILURE:" and a description on why the error occurred, on error.
1107              
1108             The zone file is written (streamed) to a filehandle, also see the
1109             get_io method.
1110              
1111              
1112             The HASH may contain 1 or more of the following arguments.
1113              
1114             =over
1115              
1116              
1117             =item ORIGIN
1118              
1119             the origin of the zone being parsed. if ommited the origin is taken to
1120             be the same as the name of the file.
1121              
1122             =item CREATE_RR
1123              
1124             if the value evaluates to TRUE an array of Net::DNS::RR objects is
1125             build that can be returned using the get_array method. When CREATE_RR
1126             is true the read module will fail if Net::DNS::RR->new() cannot parse
1127             the input i.e. when the RDATA of a RR is not correctly specified.
1128             Since the instance maintains the RR array in core setting this
1129             variable may be problematic for large zones.
1130              
1131             =item STRIP_RRSIG
1132              
1133             if the value evaluates to TRUE all RRSIG RRs in the zone are ignored
1134             i.e. stripped from the output
1135              
1136             =item STRIP_NSEC
1137              
1138             if the value evaluates to TRUE all NSEC RRs in the zone are ignored
1139             i.e. stripped from the output
1140              
1141             =item STRIP_DNSKEY
1142              
1143             if the value evaluates to TRUE all DNSKEY RRs and their related RRSIGs
1144             in the zone are ignored i.e. stripped from the output
1145              
1146             =item STRIP_SEC
1147              
1148             if the value evaluates to TRUE all DNSKEY, RRSIG and NSEC RRs in the
1149             zone are ignored i.e. stripped from the output
1150              
1151             =item STRIP_OLD
1152              
1153             if this value evaluates to TRUE all NXT and SIG RRs are ignored (the
1154             KEY RRs are _not_ ignored).
1155              
1156              
1157             =item BUMP_SOA
1158              
1159             if this value evaluates to TRUE the SOA serial will be increased by 1
1160             when written to the filehandle.
1161              
1162              
1163             =back
1164              
1165              
1166             =head2 get_array
1167              
1168             Returns a reference to the array that is created if CREATE_RR is set
1169             to true during the read method.
1170              
1171             =head2 get_origin
1172              
1173             my $origin=$parser->get_origin;
1174              
1175             Returns the origin of the zone that was parsed.
1176              
1177              
1178             =head1 Functions
1179              
1180             =head2 processGENERATEarg
1181              
1182             use Net::DNS::Zone::Parser (processGENERATEarg)
1183             $generated=processGENERATEarg(0.0.${1,3},5,"inaddr.arpa."
1184              
1185             This exported function parses the "LHS" and "RHS" from a BIND generate
1186             directive. The first argument contains the "LHS" or "RHS", the second
1187             argument the iterator vallue and the last argument contains the value
1188             of the "origin" that is to be added if the result of the generate is
1189             not a FQDN (it is the vallue that is stupidly appended if the synthesized
1190             name does not end with a ".").
1191              
1192              
1193             From the BIND documentation:
1194              
1195             lhs describes the owner name of the resource records to be
1196             created. Any single $ symbols within the lhs side are replaced by the
1197             iterator value. To get a $ in the output you need to escape the $
1198             using a backslash \, e.g. \$. The $ may optionally be followed by
1199             modifiers which change the offset from the iterator, field width and
1200             base. Modifiers are introduced by a { immediately following the $ as
1201             ${offset[,width[,base]]}. e.g. ${-20,3,d} which subtracts 20 from the
1202             current value, prints the result as a decimal in a zero padded field
1203             of with 3. Available output forms are decimal (d), octal (o) and
1204             hexadecimal (x or X for uppercase). The default modifier is
1205             ${0,0,d}. If the lhs is not absolute, the current $ORIGIN is appended
1206             to the name.
1207              
1208             =cut
1209              
1210              
1211              
1212              
1213             =head1 Supported DIRECTIVEs
1214              
1215             =head2 INCLUDE
1216              
1217             $INCLUDE []
1218              
1219             will read the file as specified by 'path'. If 'path' is absolute it
1220             will be interpreted as such. If it is relative it will be taken
1221             relative to the path of the zonefile that includes it.
1222              
1223             Optionally $INCLUDE will take a 2nd argument that sets the current
1224             origin for relative domains.
1225              
1226             The parser only accept IN class zone files.
1227              
1228             =head2 TTL
1229              
1230             Specifying the default TTL
1231              
1232             =head2 ORIGIN
1233              
1234             Specifying the origin used to complete non fully qualified domain
1235             names.
1236              
1237             =head2 GENERATE
1238              
1239             See the BIND documentation.
1240              
1241              
1242             =head1 Related packages.
1243              
1244             There are other packages with likewise functionality; they where not
1245             suitable for my purposes. But maybe they are suitable for you. So
1246             before you start using this module you may want to look at these.
1247              
1248             DNS::Zone::File will parse a zonefile but will not expand domain names
1249             that are not fully qualified since it has no logic to interpret the
1250             RDATA of each individual RR. You can use this module to pre-process
1251             the file and then feed it to DNS::Zone::File (Default) to create a
1252             DNS::Zone instance.
1253              
1254             DNS::ZoneFile has almost the same functionality as this code it the
1255             canonicalises RR records it is aware off. It also has an INCLUDE
1256             function. Being an abstraction of a zonefile it has an interface to
1257             add and delete RRs from the zonefile and print it. The code does not
1258             support a GENERATE feature.
1259              
1260             Net::DNS::ZoneFile also almost has the same functionality, it supports
1261             the GENERATE, INCLUDE and ORIGIN primitives. It also supports more
1262             classes than just the IN class. However, this module first loads the
1263             complete zone in memory; which may be problematic for very large
1264             zones. It only seems to support a subset of the available RR types.
1265              
1266             All of these classes are abstractions of zonefiles, not of zones
1267             i.e. there is no notion of where the zonecuts are and what data is out
1268             of zone.
1269              
1270              
1271              
1272             =head1 TODO, BUGS and FEATURES.
1273              
1274             =over indentlevel
1275              
1276             =item FEATURE
1277              
1278             This code only supports zones in the Zone files in the IN class.
1279              
1280             =item FEATURE
1281              
1282             More sanity checking on the RDATA for each RR.
1283              
1284             The pre-processor it will only look for 'dnames' in the RDATA that
1285             need expansion and not check or validate other entries in the RDATA.
1286              
1287              
1288             =item FEATURE
1289              
1290             The zonefile formating rules allow the CLASS to be specified
1291             before the TTL. This code does not parse such lines.
1292              
1293             =item FEATURE
1294              
1295             The KX RR (RFC 2230) will have its RDATA expanded but since
1296             there is no implementation of it in Net::DNS it will fail to read if
1297             CREATE_RR => 1 in the read method.
1298              
1299              
1300             =item TODO
1301              
1302             This code needs to know of RR types that have RDATA with dnames.
1303              
1304             For completeness these are the RRtypes that have domain names in
1305             their rdata and that have been implemented.
1306              
1307             NS, CNAME, SOA, MB, PTR, MG, MR, PTR, MINFO, MX, RP, AFSDB, RT,
1308             SIG, NXT, SRV, DNAME, NSEC, and RRSIG
1309              
1310             RRtypes that do not have domain names in their RDATA will be parsed
1311             transparently.
1312              
1313             New types will need to be implemented if they become available.
1314             Please inform the developer of new RRtypes with a domain name in them
1315             that has not been implemented.
1316              
1317              
1318              
1319             =back =head1 COPYRIGHT
1320              
1321             Copyright (c) 2003, 2004 RIPE NCC. Author Olaf M. Kolkman
1322            
1323              
1324             All Rights Reserved
1325              
1326             Permission to use, copy, modify, and distribute this software and its
1327             documentation for any purpose and without fee is hereby granted,
1328             provided that the above copyright notice appear in all copies and that
1329             both that copyright notice and this permission notice appear in
1330             supporting documentation, and that the name of the author not be
1331             used in advertising or publicity pertaining to distribution of the
1332             software without specific, written prior permission.
1333              
1334              
1335             THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING
1336             ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS; IN NO EVENT SHALL
1337             AUTHOR BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY
1338             DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
1339             AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
1340             OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
1341              
1342             The $GENERATE primitive parser is based on code in Net::DNS::ZoneFile
1343              
1344              
1345             =head1 SEE ALSO
1346              
1347             L, L, L, L,
1348             L
1349              
1350              
1351             =cut
1352