File Coverage

blib/lib/DNS/ZoneParse.pm
Criterion Covered Total %
statement 399 436 91.5
branch 202 254 79.5
condition 21 35 60.0
subroutine 23 27 85.1
pod 8 10 80.0
total 653 762 85.7


line stmt bran cond sub pod time code
1             # DNS::ZoneParse
2             # Parse and Manipulate DNS Zonefiles
3             package DNS::ZoneParse;
4              
5 6     6   5806 use 5.006;
  6         18  
  6         220  
6 6     6   5693 use Storable 'dclone';
  6         25582  
  6         487  
7 6     6   4790 use POSIX 'strftime';
  6         41463  
  6         40  
8 6     6   6467 use File::Basename;
  6         14  
  6         453  
9 6     6   32 use vars qw($VERSION);
  6         10  
  6         264  
10 6     6   29 use strict;
  6         8  
  6         170  
11 6     6   25 use Carp;
  6         18  
  6         23655  
12              
13             # It makes everyone's life easier if you double-escape the backslash, and only
14             # the backslash, here.
15             my @ESCAPABLE_CHARACTERS = qw/ ; " \\\\ /;
16              
17             my $rr_class = qr/(?:IN|HS|CH)/i;
18             my $rr_ttl = qr/(?:\d+[wdhms]?)+/i;
19              
20             $VERSION = '1.10';
21             my (
22             %dns_id, %dns_soa, %dns_ns, %dns_a, %dns_cname, %dns_mx, %dns_txt,
23             %dns_ptr, %dns_a4, %dns_srv, %dns_hinfo, %dns_rp, %dns_loc,
24             %dns_generate,
25             %dns_last_name, %dns_last_origin, %dns_last_class, %dns_last_ttl,
26             %dns_found_origins, %unparseable_line_callback, %last_parse_error_count,
27             );
28              
29             my %possibly_quoted = map { $_ => undef } qw/ os cpu text mbox /;
30              
31             sub new {
32 12     12 1 4225 my $class = shift;
33 12         24 my $file = shift;
34 12         24 my $origin = shift;
35 12         25 my $unparseable_callback = shift;
36 12         39 my $self = bless [], $class;
37              
38 12 50       70 if ( ref $unparseable_callback eq 'CODE' ) {
39 12         79 $unparseable_line_callback{$self} = $unparseable_callback;
40             }
41              
42 12         42 $self->_initialize();
43 12 50       69 $self->_load_file( $file, $origin ) if $file;
44 12         73 return $self;
45             }
46              
47             sub on_unparseable_line {
48 0     0 1 0 my $self = shift;
49 0         0 my $arg = shift;
50 0 0       0 if ( !defined $arg ) {
    0          
51 0         0 return $unparseable_line_callback{$self};
52             } elsif ( ref $arg eq 'CODE' ) {
53 0         0 my $old = $unparseable_line_callback{$self};
54 0         0 $unparseable_line_callback{$self} = $arg;
55 0         0 return $old;
56             } else {
57 0         0 return undef;
58             }
59             }
60              
61             sub last_parse_error_count {
62 12     12 1 3890 my $self = shift;
63 12         71 return $last_parse_error_count{$self};
64             }
65              
66             sub DESTROY {
67 12     12   5146 my $self = shift;
68 12         98 delete $dns_soa{$self};
69 12         65 delete $dns_ns{$self};
70 12         83 delete $dns_a{$self};
71 12         55 delete $dns_cname{$self};
72 12         47 delete $dns_mx{$self};
73 12         74 delete $dns_txt{$self};
74 12         31 delete $dns_ptr{$self};
75 12         38 delete $dns_a4{$self};
76 12         46 delete $dns_srv{$self};
77 12         229 delete $dns_hinfo{$self};
78 12         40 delete $dns_rp{$self};
79 12         120 delete $dns_loc{$self};
80 12         45 delete $dns_id{$self};
81 12         34 delete $dns_generate{$self};
82 12         30 delete $dns_last_name{$self};
83 12         64 delete $dns_last_origin{$self};
84 12         29 delete $dns_last_ttl{$self};
85 12         30 delete $dns_last_class{$self};
86 12         35 delete $dns_found_origins{$self};
87 12         37 delete $unparseable_line_callback{$self};
88 12         687 delete $last_parse_error_count{$self};
89             }
90              
91             sub AUTOLOAD {
92 78     78   4571 my $self = shift;
93 78         436 ( my $method = $DNS::ZoneParse::AUTOLOAD ) =~ s/.*:://;
94              
95 78 0       561 my $rv =
    0          
    50          
    100          
    100          
    100          
    100          
    100          
    50          
    100          
    100          
    100          
    100          
    100          
    100          
96             $method eq 'soa' ? $dns_soa{$self}
97             : $method eq 'ns' ? $dns_ns{$self}
98             : $method eq 'a' ? $dns_a{$self}
99             : $method eq 'cname' ? $dns_cname{$self}
100             : $method eq 'mx' ? $dns_mx{$self}
101             : $method eq 'txt' ? $dns_txt{$self}
102             : $method eq 'ptr' ? $dns_ptr{$self}
103             : $method eq 'aaaa' ? $dns_a4{$self}
104             : $method eq 'srv' ? $dns_srv{$self}
105             : $method eq 'hinfo' ? $dns_hinfo{$self}
106             : $method eq 'rp' ? $dns_rp{$self}
107             : $method eq 'loc' ? $dns_loc{$self}
108             : $method eq 'generate' ? $dns_generate{$self}
109             : $method eq 'zonefile' ? $dns_id{$self}->{ZoneFile}
110             : $method eq 'origin' ? $dns_id{$self}->{Origin}
111             : undef;
112              
113 78 50       202 croak "Invalid method called: $method" unless defined $rv;
114 78         1251 return $rv;
115             }
116              
117             # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
118             # Public OO Methods
119             # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
120              
121             sub dump {
122             # returns a HOH for use with XML modules, etc
123 0     0 1 0 my $self = shift;
124 0         0 return dclone( {
125             SOA => $dns_soa{$self},
126             AAAA => $dns_a4{$self},
127             A => $dns_a{$self},
128             NS => $dns_ns{$self},
129             CNAME => $dns_cname{$self},
130             MX => $dns_mx{$self},
131             PTR => $dns_ptr{$self},
132             TXT => $dns_txt{$self},
133             SRV => $dns_srv{$self},
134             HINFO => $dns_hinfo{$self},
135             RP => $dns_rp{$self},
136             LOC => $dns_loc{$self},
137             } );
138             }
139              
140             sub new_serial {
141 6     6 1 2495 my $self = shift;
142 6   100     27 my $incriment = shift || 0;
143 6         15 my $soa = $dns_soa{$self};
144 6 100       17 if ( $incriment > 0 ) {
145 3         9 $soa->{serial} += $incriment;
146             } else {
147 3         411 my $newserial = strftime( "%Y%m%d%H", localtime( time ) );
148 3 100       22 $soa->{serial} =
149             ( $newserial > $soa->{serial} )
150             ? $newserial
151             : $soa->{serial} + 1;
152             }
153 6         19 return $soa->{serial};
154             }
155              
156             sub output {
157 5     5 1 17 my $self = shift;
158 5 50       48 my $zone_ttl = $dns_soa{$self}{ttl} ? "\$TTL $dns_soa{$self}{ttl}" : '';
159 5         14 my $output = '';
160 5         49 $output .= <
161             ;
162             ; Database file $dns_id{$self}->{ZoneFile} for $dns_id{$self}->{Origin} zone.
163             ; Zone version: $dns_soa{$self}->{serial}
164             ;
165             ZONEHEADER1
166              
167 5 100       35 if ( $dns_soa{$self}->{'ORIGIN'} ne $dns_soa{$self}->{'origin'} ) {
168 3         15 $output .= "\n\$ORIGIN $dns_soa{$self}->{'ORIGIN'}\n\n";
169             }
170              
171 5         83 $output .= <
172              
173             $zone_ttl
174             $dns_soa{$self}->{origin} $dns_soa{$self}->{ttl} IN SOA $dns_soa{$self}->{primary} $dns_soa{$self}->{email} (
175             $dns_soa{$self}->{serial} ; serial number
176             $dns_soa{$self}->{refresh} ; refresh
177             $dns_soa{$self}->{retry} ; retry
178             $dns_soa{$self}->{expire} ; expire
179             $dns_soa{$self}->{minimumTTL} ; minimum TTL
180             )
181             ;
182             ; Zone NS Records
183             ;
184              
185             ZONEHEADER2
186              
187             my @origins_to_process = grep {
188 7 100       35 if ( $_ eq $dns_soa{$self}->{'ORIGIN'} ) {
  5         30  
189 5         22 0;
190             } else {
191 2         5 1;
192             }
193 5         13 } keys %{ $dns_found_origins{$self} };
194 5         24 unshift @origins_to_process, $dns_soa{$self}->{'ORIGIN'};
195              
196 5         17 foreach my $process_this_origin ( @origins_to_process ) {
197 7 100       40 if ( $process_this_origin ne $dns_soa{$self}->{'ORIGIN'} ) {
198 2         7 $output .= "\n\;\n\; $process_this_origin records\n\;\n\n";
199 2         5 $output .= "\$ORIGIN $process_this_origin\n\n";
200             }
201              
202 7         15 foreach my $o ( @{ $dns_ns{$self} } ) {
  7         29  
203 14 50       42 next unless defined $o;
204 14 100       50 next unless $o->{'ORIGIN'} eq $process_this_origin;
205 10         68 $self->_escape_chars( $o );
206 10         59 $output .= "$o->{name} $o->{ttl} $o->{class} NS $o->{host}\n";
207             }
208              
209 7         19 foreach my $o ( @{ $dns_mx{$self} } ) {
  7         31  
210 6 50       14 next unless defined $o;
211 6 100       19 next unless $o->{'ORIGIN'} eq $process_this_origin;
212 2         7 $self->_escape_chars( $o );
213 2         13 $output .= "$o->{name} $o->{ttl} $o->{class} MX $o->{priority} $o->{host}\n";
214             }
215              
216 7         22 foreach my $o ( @{ $dns_a{$self} } ) {
  7         27  
217 27 50       46 next unless defined $o;
218 27 100       63 next unless $o->{'ORIGIN'} eq $process_this_origin;
219 9         18 $self->_escape_chars( $o );
220 9         38 $output .= "$o->{name} $o->{ttl} $o->{class} A $o->{host}\n";
221             }
222 7         14 foreach my $o ( @{ $dns_cname{$self} } ) {
  7         28  
223 6 50       92 next unless defined $o;
224 6 100       20 next unless $o->{'ORIGIN'} eq $process_this_origin;
225 2         7 $self->_escape_chars( $o );
226 2         14 $output .= "$o->{name} $o->{ttl} $o->{class} CNAME $o->{host}\n";
227             }
228 7         29 foreach my $o ( @{ $dns_a4{$self} } ) {
  7         30  
229 3 50       9 next unless defined $o;
230 3 100       12 next unless $o->{'ORIGIN'} eq $process_this_origin;
231 1         5 $self->_escape_chars( $o );
232 1         6 $output .= "$o->{name} $o->{ttl} $o->{class} AAAA $o->{host}\n";
233             }
234 7         16 foreach my $o ( @{ $dns_txt{$self} } ) {
  7         23  
235 21 50       39 next unless defined $o;
236 21 100       49 next unless $o->{'ORIGIN'} eq $process_this_origin;
237 7         16 $self->_escape_chars( $o );
238 7         34 $output .= qq[$o->{name} $o->{ttl} $o->{class} TXT "$o->{text}"\n];
239             }
240 7         15 foreach my $o ( @{ $dns_ptr{$self} } ) {
  7         26  
241 0 0       0 next unless defined $o;
242 0 0       0 next unless $o->{'ORIGIN'} eq $process_this_origin;
243 0         0 $self->_escape_chars( $o );
244 0         0 $output .= "$o->{name} $o->{ttl} $o->{class} PTR $o->{host}\n";
245             }
246 7         12 foreach my $o ( @{ $dns_srv{$self} } ) {
  7         24  
247 6 50       13 next unless defined $o;
248 6 100       21 next unless $o->{'ORIGIN'} eq $process_this_origin;
249 2         8 $self->_escape_chars( $o );
250 2         15 $output .= "$o->{name} $o->{ttl} $o->{class} SRV $o->{priority} $o->{weight} $o->{port} $o->{host}\n";
251             }
252 7         33 foreach my $o ( @{ $dns_hinfo{$self} } ) {
  7         31  
253 114 50       185 next unless defined $o;
254 114 100       237 next unless $o->{'ORIGIN'} eq $process_this_origin;
255 38         67 $self->_escape_chars( $o );
256 38         164 $output .= "$o->{name} $o->{ttl} $o->{class} HINFO $o->{cpu} $o->{os}\n";
257             }
258 7         23 foreach my $o ( @{ $dns_rp{$self} } ) {
  7         26  
259 3 50       9 next unless defined $o;
260 3 100       11 next unless $o->{'ORIGIN'} eq $process_this_origin;
261 1         4 $self->_escape_chars( $o );
262 1         7 $output .= "$o->{name} $o->{ttl} $o->{class} RP $o->{mbox} $o->{text}\n";
263             }
264 7         17 foreach my $o ( @{ $dns_loc{$self} } ) {
  7         26  
265 18 50       36 next unless defined $o;
266 18 100       42 next unless $o->{'ORIGIN'} eq $process_this_origin;
267 6         14 $self->_escape_chars( $o );
268 6         30 $output .= "$o->{name} $o->{ttl} $o->{class} LOC $o->{d1} $o->{m1} $o->{s1} $o->{NorS} ";
269 6         15 $output .= "$o->{d2} $o->{m2} $o->{s2} $o->{EorW} ";
270 6         19 $output .= "$o->{alt} $o->{siz} $o->{hp} $o->{vp}\n";
271             }
272 7         14 foreach my $o ( @{ $dns_generate{$self} } ) {
  7         28  
273 3 50       9 next unless defined $o;
274 3 100       12 next unless $o->{'ORIGIN'} eq $process_this_origin;
275 1         3 $self->_escape_chars( $o );
276 1         12 $output .= "\$GENERATE $o->{range} $o->{lhs} $o->{ttl} $o->{class} $o->{type} $o->{rhs}\n";
277             }
278              
279             }
280              
281 5         38 return $output;
282             }
283              
284             sub fqname {
285 9     9 1 18 my ( $self, $record_ref ) = @_;
286              
287             # Is this an SOA record?
288 9 100       27 if ( $record_ref->{'origin'} ) {
289 3 50 33     18 if ( ( $record_ref->{'origin'} eq '@' ) || ( $record_ref->{'origin'} =~ /\.$/ ) ) {
290 3         19 return $record_ref->{'ORIGIN'};
291             } else {
292 0 0       0 if ( $record_ref->{'ORIGIN'} =~ /^\./ ) {
293 0         0 return $record_ref->{'origin'} . $record_ref->{'ORIGIN'};
294             }
295 0         0 return $record_ref->{'origin'} . '.' . $record_ref->{'ORIGIN'};
296             }
297             } else {
298 6 100       20 if ( $record_ref->{'name'} eq '@' ) {
299 3         15 return $record_ref->{'ORIGIN'};
300             } else {
301 3 50       15 if ( $record_ref->{'ORIGIN'} =~ /^\./ ) {
302 0         0 return $record_ref->{'name'} . $record_ref->{'ORIGIN'};
303             }
304 3         18 return $record_ref->{'name'} . '.' . $record_ref->{'ORIGIN'};
305             }
306             }
307             }
308              
309             sub ttl_to_int {
310 16     16 1 30 my ( $self, $t ) = @_;
311              
312             # Passed in nothing? Huh?
313 16 100       44 if ( !$t ) {
314 1         4 return 0;
315             }
316              
317             # If it's all digits already, just pass it right back.
318 15 100       62 if ( $t =~ /^\d+$/ ) {
319 1         8 return $t;
320             }
321              
322             # If it doesn't look like a valid TTL string, error. We know, because of
323             # the above test, that it's not just a number, if we got this far.
324 14 50       58 if ( $t !~ /^(?:\d+[WDHMS])+$/i ) {
325 0         0 die "Unknown TTL string format!\n";
326             }
327 14         27 $t = uc( $t );
328              
329 14         16 my $r;
330             my %ttl;
331 14         55 while ( $t =~ /(\d+)([WDHMS])/g ) {
332             # Did we already see this modifier?
333 32 50       75 if ( defined $ttl{ $2 } ) { die "Invalid TTL!\n"; }
  0         0  
334 32         119 $ttl{ $2 } = $1;
335             }
336              
337 14         22 foreach my $m ( qw/ W D H M S / ) {
338 70 100       138 if ( !exists $ttl{ $m } ) { $ttl{ $m } = 0; }
  38         62  
339             }
340            
341 14         30 $r = $ttl{'W'} * 7;
342 14         20 $r = ( $r + $ttl{'D'} ) * 24;
343 14         18 $r = ( $r + $ttl{'H'} ) * 60;
344 14         17 $r = ( $r + $ttl{'M'} ) * 60;
345 14         16 $r = ( $r + $ttl{'S'} );
346              
347 14 50       45 die unless $r == $ttl{'S'} + 60 * ( $ttl{'M'} + 60 * ( $ttl{'H'} + 24 * ( $ttl{'D'} + 7 * $ttl{'W'} ) ) );
348 14         81 return $r;
349             }
350              
351             # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
352             # Private Methods
353             # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
354              
355             sub _initialize {
356 24     24   37 my $self = shift;
357 24         84 $dns_id{$self} = {};
358 24         58 $dns_soa{$self} = {};
359 24         93 $dns_ns{$self} = [];
360 24         54 $dns_a{$self} = [];
361 24         57 $dns_cname{$self} = [];
362 24         50 $dns_mx{$self} = [];
363 24         45 $dns_txt{$self} = [];
364 24         54 $dns_ptr{$self} = [];
365 24         45 $dns_a4{$self} = [];
366 24         48 $dns_srv{$self} = [];
367 24         46 $dns_hinfo{$self} = [];
368 24         48 $dns_rp{$self} = [];
369 24         45 $dns_loc{$self} = [];
370 24         51 $dns_generate{$self} = [];
371 24         49 $dns_last_name{$self} = undef;
372 24         75 $dns_last_origin{$self} = undef;
373 24         43 $dns_last_ttl{$self} = undef;
374 24         126 $dns_last_class{$self} = 'IN'; # Class defaults to IN.
375 24         58 $dns_found_origins{$self} = {};
376 24         49 $last_parse_error_count{$self} = 0;
377 24         65 return 1;
378             }
379              
380             sub _load_file {
381 12     12   26 my ( $self, $zonefile, $origin ) = @_;
382 12         18 my $zone_contents;
383 12 100       43 if ( ref( $zonefile ) eq 'SCALAR' ) {
384 11         20 $zone_contents = $$zonefile;
385             } else {
386 1         2 my $inZONE;
387 1 50       74 if ( open( $inZONE, '<', $zonefile ) ) {
388 1         4 local $/;
389 1         39 $zone_contents = <$inZONE>;
390 1         14 close( $inZONE );
391             } else {
392 0         0 croak qq[DNS::ZoneParse Could not open input file: "$zonefile":$!];
393             }
394             }
395 12 50       48 if ( $self->_parse( $zonefile, $zone_contents, $origin ) ) { return 1; }
  12         75  
396             }
397              
398             sub _parse {
399             # Support IsAlnum for unicode names.
400 6     6   6237 use utf8;
  6         63  
  6         32  
401 12     12   21 my ( $self, $zonefile, $contents, $origin ) = @_;
402 12         32 $self->_initialize();
403              
404             # Here's how we auto-detect the zonefile and origin. Note, the zonefile is
405             # only used to print out a comment in the file, so its okay if we're
406             # inaccurate. First, prefer what the user configures. Next, try to read a
407             # comment we would have written if we wrote the file out in the past.
408             # Finally, pick up any SOA or $ORIGIN statements present in the file.
409 12 100       75 if ( ref( $zonefile ) eq 'SCALAR' ) { $zonefile = ''; }
  11         21  
410              
411 12 50 66     49 if ( !$origin || !$zonefile ) {
412             # I don't know why the ( dns)? capture is there, perhaps at one point
413             # this module wrote a different header comment? I'll leave it as to
414             # preserve whatever backwards compatability this affords us...
415 12         109 $contents =~ /^\s*;\s*Database file (\S+)( dns)? for (\S+) zone/im;
416 12 100 66     76 if ( !$origin && $3 ) { $origin = $3; }
  6         18  
417 12 100 66     64 if ( !$zonefile && $1 ) { $zonefile = $1; }
  6         14  
418             }
419              
420 12 100       35 if ( $zonefile ) {
421 7         427 $zonefile = basename( $zonefile );
422             } else {
423 5         7 $zonefile = 'unknown';
424             }
425              
426 12 100       34 if ( $origin ) {
427             # A trite way of insuring there is a trailing dot on the origin. It's
428             # really important you supply a trailing . in an origin when you mean
429             # it.
430 8         60 $origin =~ s/([^.])$/$1./;
431             } else {
432 4         7 $origin = '';
433             }
434              
435 12         53 $dns_id{$self} = {
436             ZoneFile => $zonefile,
437             Origin => $origin,
438             };
439              
440 12         41 my $records = $self->_clean_records( $contents );
441              
442             # Everything valid in the name, except the '.' character.
443 36         126 my $valid_name_start_char = q/(?:[\p{IsAlnum}\@_\-*:+=!#$%^&`~,\[\]{}|?'\/]|/
444 12         66 . join( '|', map { "\\\\$_" } @ESCAPABLE_CHARACTERS ) . ')';
445              
446             # The above, but adds the literal '.' character.
447 6     6   54 my $valid_name_char = qr/(?:$valid_name_start_char|[\.\\])/o;
  6         10  
  6         84  
  12         413  
448 12         141589 my $valid_txt_char = qr/\S+/o;
449 12         72 my $valid_quoted_txt_char = qr/.+/o;
450             # Like the above, but adds whitespace (space and tabs) too.
451 12         190 my $valid_quoted_name_char = qr/(?:$valid_name_start_char|[. ;\t()\\])/o;
452 12         11616 my $valid_name = qr/$valid_name_start_char$valid_name_char*/o;
453 12         12753 my $valid_ip6 = qr/[\@a-zA-Z_\-\.0-9\*:]+/;
454 12         47 my $rr_type = qr/\b(?:NS|A|CNAME)\b/i;
455             #my $ttl_cls = qr/(?:($rr_ttl)\s)?(?:($rr_class)\s)?/o;
456 12         674 my $ttl_cls = qr/(?:\b((?:$rr_ttl)|(?:$rr_class))\s)?(?:\b((?:$rr_class)|(?:$rr_ttl))\s)?/o;
457 12         65 my $generate_range = qr{\d+\-\d+(?:/\d+)?};
458 12         25 my $last_good_line;
459              
460 12         195 foreach ( @$records ) {
461             #TRACE( "parsing line <$_>" );
462              
463             # It's faster to skip blank lines here than to remove them inside
464             # _clean_records.
465 379 100       1191 next if /^\s*$/;
466              
467             # The below is inside of an eval block to catch possible errors
468             # found inside _massage and propagate them up properly.
469 269         317 eval {
470 269         1130 local $SIG{__DIE__} = 'DEFAULT';
471              
472 269 100       53492 if (
    100          
    100          
    100          
    100          
    50          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
473             /^($valid_name)? \s+ # host
474             $ttl_cls # ttl & class
475             ($rr_type) \s # record type
476             ($valid_name) # record data
477             /ixo
478             )
479             {
480 57         207 my ( $name, $ttl, $class, $type, $host ) = ( $1, $2, $3, $4, $5 );
481 57 100       208 my $dns_thing =
    100          
482             uc $type eq 'NS' ? $dns_ns{$self}
483             : uc $type eq 'A' ? $dns_a{$self}
484             : $dns_cname{$self};
485 57         349 push @$dns_thing,
486             $self->_massage( {
487             name => $name,
488             class => $class,
489             host => $host,
490             ttl => $ttl,
491             } );
492             } elsif (
493             /^($valid_name)? \s+
494             $ttl_cls
495             AAAA \s
496             ($valid_ip6)
497             /ixo
498             )
499             {
500 3         12 my ( $name, $ttl, $class, $host ) = ( $1, $2, $3, $4 );
501 3         4 push @{ $dns_a4{$self} },
  3         24  
502             $self->_massage( {
503             name => $name,
504             class => $class,
505             host => $host,
506             ttl => $ttl,
507             } );
508             } elsif (
509             /^($valid_name)? \s+
510             $ttl_cls
511             MX \s+
512             (\d+) \s+
513             ($valid_name_char+)
514             /ixo
515             )
516             {
517             # host ttl class mx pri dest
518 6         29 my ( $name, $ttl, $class, $pri, $host ) = ( $1, $2, $3, $4, $5 );
519 6         8 push @{ $dns_mx{$self} },
  6         49  
520             $self->_massage( {
521             name => $name,
522             priority => $pri,
523             host => $host,
524             ttl => $ttl,
525             class => $class,
526             } );
527             } elsif (
528             /^($valid_name)? \s+
529             $ttl_cls
530             SRV \s+
531             (\d+) \s+
532             (\d+) \s+
533             (\d+) \s+
534             ($valid_name)
535             /ixo
536             )
537             {
538             # host ttl class mx priority weight port dest
539 6         33 my ( $name, $ttl, $class, $pri, $weight, $port, $host ) = ( $1, $2, $3, $4, $5, $6, $7 );
540 6         9 push @{ $dns_srv{$self} },
  6         53  
541             $self->_massage( {
542             name => $name,
543             priority => $pri,
544             weight => $weight,
545             port => $port,
546             host => $host,
547             ttl => $ttl,
548             class => $class,
549             } );
550             } elsif (
551             /^($valid_name) \s+
552             $ttl_cls
553             SOA \s+
554             ($valid_name) \s+
555             ($valid_name) \s+
556             ($rr_ttl) \s+
557             ($rr_ttl) \s+
558             ($rr_ttl) \s+
559             ($rr_ttl) \s+
560             ($rr_ttl)
561             /ixo
562             )
563             {
564             # SOA record
565 12         22525 $dns_soa{$self} = $self->_massage( {
566             origin => $1,
567             ttl => $2,
568             class => $3,
569             primary => $4,
570             email => $5,
571             serial => $6,
572             refresh => $7,
573             retry => $8,
574             expire => $9,
575             minimumTTL => $10,
576             } );
577              
578 12 100       74 if ( !$origin ) {
579 4         13 $origin = $1;
580 4         46 $dns_id{$self} = {
581             ZoneFile => $zonefile,
582             Origin => $origin,
583             };
584             }
585              
586             } elsif (
587             /^($valid_name)? \s+
588             $ttl_cls
589             PTR \s+
590             ($valid_name)
591             /ixo
592             )
593             {
594             # PTR
595 0         0 push @{ $dns_ptr{$self} },
  0         0  
596             $self->_massage( {
597             name => $1,
598             class => $3,
599             ttl => $2,
600             host => $4,
601             } );
602             } elsif (
603             /($valid_name)? \s+
604             $ttl_cls
605             TXT \s+
606             ("$valid_quoted_txt_char*(?
607             /ixo
608             ) {
609 21         31 push @{ $dns_txt{$self} },
  21         171  
610             $self->_massage( {
611             name => $1,
612             ttl => $2,
613             class => $3,
614             text => $4,
615             } );
616             } elsif (
617             /^\s*\$TTL \s+
618             ($rr_ttl)
619             /ixo
620             ) {
621 10 50       73112 if ( !defined $dns_soa{$self} ) {
622 0         0 $dns_soa{$self}->{ttl} = $1;
623             }
624 10         118 $dns_last_ttl{$self} = $1;
625             } elsif (
626             /^($valid_name)? \s+
627             $ttl_cls
628             HINFO \s+
629             ("$valid_quoted_txt_char*(?
630             ("$valid_quoted_txt_char*(?
631             /ixo
632             )
633             {
634 114         970 push @{ $dns_hinfo{$self} },
  114         1037  
635             $self->_massage( {
636             name => $1,
637             ttl => $2,
638             class => $3,
639             cpu => $4,
640             os => $5,
641             } );
642             } elsif (
643             /^($valid_name)? \s+
644             $ttl_cls
645             RP \s+
646             ($valid_name_char+) \s+
647             ($valid_name_char+)
648             /ixo
649             )
650             {
651 3         6 push @{ $dns_rp{$self} },
  3         34  
652             $self->_massage( {
653             name => $1,
654             ttl => $2,
655             class => $3,
656             mbox => $4,
657             text => $5,
658             } );
659             } elsif (
660             /^($valid_name)? \s+
661             $ttl_cls
662             LOC \s+
663             (-?[\d\.]+) \s*
664             ([\d\.]*) \s*
665             ([\d\.]*) \s+
666             ([NS]) \s+
667             (-?[\d\.]+) \s*
668             ([\d\.]*) \s*
669             ([\d\.]*) \s+
670             ([EW]) \s*
671             (-?[\d\.]*m?) \s*
672             ([\d\.]*m?) \s*
673             ([\d\.]*m?) \s*
674             ([\d\.]*m?)
675             /ixo
676             )
677             {
678 18         26 push @{ $dns_loc{$self} },
  18         377  
679             $self->_massage( {
680             name => $1,
681             ttl => $2,
682             class => $3,
683             d1 => $4,
684             m1 => $5,
685             s1 => $6,
686             NorS => $7,
687             d2 => $8,
688             m2 => $9,
689             s2 => $10,
690             EorW => $11,
691             alt => $12,
692             siz => $13,
693             hp => $14,
694             vp => $15,
695             } );
696              
697             } elsif ( /^\s*\$ORIGIN\s+($valid_name_char+)/io ) {
698 12         6126 my $new_origin = $1;
699             # We could track each origins origin, all the way down, but what
700             # would that get us? Madness, surely.
701 12 100       61 if ( $new_origin !~ /\.$/ ) {
702 4 50       11 if ( $dns_last_origin{$self} =~ /^\./ ) {
703 0         0 $new_origin .= $dns_last_origin{$self};
704             } else {
705 4         10 $new_origin .= '.' . $dns_last_origin{$self};
706             }
707             }
708 12         35 $dns_last_origin{$self} = $new_origin;
709 12         72 $dns_found_origins{$self}->{ $new_origin } = 1;
710              
711             } elsif ( /^ \s* \$GENERATE \s+
712             ($generate_range) \s+ # range
713             ($valid_name) \s+ # lhs
714             (?:($rr_ttl) \s+)? # ttl
715             (?:($rr_class) \s+)? # class
716             ([a-z]+) \s+ # type
717             ($valid_name) # rhs
718             /ixo
719             )
720             {
721 3         2434 push @{ $dns_generate{$self} },
  3         53  
722             $self->_massage( {
723             range => $1,
724             lhs => $2,
725             ttl => $3,
726             class => $4,
727             type => $5,
728             rhs => $6,
729             } );
730              
731             } else {
732 4         20 die "Unknown record type\n";
733             }
734              
735             }; # End of eval block.
736 269 100       7327 if ( $@ ) {
737 4         8 chomp $@;
738 4         6 $last_parse_error_count{$self}++;
739 4 50       11 if ( $unparseable_line_callback{$self} ) {
740 4         17 $unparseable_line_callback{$self}->( $self, $_, $@, $last_good_line );
741             } else {
742 0         0 carp "Unparseable line ($@)\n $_\n";
743             }
744             } else {
745 265         459 $last_good_line = $_;
746             }
747             }
748 12         210 return 1;
749             }
750              
751             sub _clean_records {
752 12     12   20 my $self = shift;
753 12         23 my $zone = shift;
754 12         22 my $x = 0;
755 12         20 my $in_comment = 0;
756 12         17 my $in_quote = 0;
757 12         17 my $in_concat = 0;
758 12         18 my $last_char = '';
759 12         13 my $next_is_escaped = 0;
760 12         18 my @lines;
761              
762 12         45 $zone =~ s/\r\n/\n/sg;
763 12         831 $zone =~ s{[ \t]+}{ }g; # Collapse whitespace, turn TABs to spaces.
764              
765             # Trim comments, handle parentheses and some escape sequences.
766 12         20 while (1) {
767 13302         15145 my $c = substr( $zone, $x, 1 );
768              
769             # If we're not in a comment then process parentheses, braces, comment
770             # tags, and quotes. If not, just look for the newline.
771 13302 100       19316 if ( !$in_comment ) {
    100          
772 10859 100       13722 if ( !$next_is_escaped ) {
773 10798 100       27566 if ( $c eq '"' ) {
    100          
    100          
774 230         291 $in_quote = !$in_quote;
775             } elsif ( $c eq '\\' ) {
776 61         86 $next_is_escaped = 1;
777             } elsif ( !$in_quote ) {
778 9660 100 100     34237 if ( $c eq ';' ) {
    100          
    100          
779 171         162 $in_comment = 1;
780 171         245 substr( $zone, $x, 1 ) = '';
781 171         217 $x--;
782             } elsif ( $c eq '(' ) {
783 14         29 substr( $zone, $x, 1 ) = ' ';
784 14         18 $in_concat++;
785             } elsif ( ( $in_concat ) && ( $c eq ')' ) ) {
786 14         25 substr( $zone, $x, 1 ) = ' ';
787 14         21 $in_concat--;
788             }
789             }
790             } else {
791 61         78 $next_is_escaped = 0;
792             }
793             } elsif ( $c ne "\n" ) {
794 2272         2463 substr( $zone, $x, 1 ) = '';
795 2272         2065 $x--;
796             }
797 13302 100       20933 if ( $c eq "\n" ) {
798 446         456 $in_comment = 0;
799 446 100       755 if ( $in_concat ) {
800 67         90 substr( $zone, $x, 1 ) = '';
801 67         73 $x--;
802             }
803             }
804 13302         11233 $x++;
805 13302 100       22256 if ( $x >= length( $zone ) ) { last; }
  12         34  
806 13290         13437 $last_char = $c;
807             }
808              
809 12         301 return [ split( /\n/, $zone ) ];
810             }
811              
812             sub _massage {
813 243     243   339 my ( $self, $record ) = @_;
814              
815 243         838 foreach my $r ( keys %$record ) {
816 1389 100       2518 if ( !defined $record->{$r} ) {
817 272         320 $record->{$r} = '';
818 272         364 next;
819             }
820 1117 100       1964 if ( exists $possibly_quoted{$r} ) {
821 255 100       940 ( $record->{$r} =~ s/^"// ) && ( $record->{$r} =~ s/"$// );
822             }
823              
824             # We return email addresses just as they are in the file... for better
825             # or worse (mostly for backwards compatability reasons).
826 1117 100 100     3957 if ( $r ne 'email' && $r ne 'mbox' ) {
827 1102         3128 while ( $record->{$r} =~ m/\\/g ) {
828 49         98 my $pos = pos( $record->{$r} );
829 49         76 my $escape_char = substr( $record->{$r}, $pos, 1 );
830 49 50       97 if ( $escape_char =~ /\d/ ) {
831 0         0 $escape_char = substr( $record->{$r}, $pos, 3 );
832             # Max oct value that converts to 255 in dec.
833 0 0 0     0 if ( ( $escape_char =~ /^\d{3}$/ ) && ( $escape_char <= 377 ) ) {
834 0         0 substr( $record->{$r}, $pos - 1, 4 ) = chr( oct( $escape_char ) );
835             } else {
836 0         0 die "Invalid escape sequence\n";
837             }
838             } else {
839             # Not followed by a digit, so just remove the backslash.
840             # Like BIND does...
841 49         105 substr( $record->{$r}, $pos - 1, 2 ) = $escape_char;
842             }
843 49         201 pos( $record->{$r} ) = $pos;
844             }
845             }
846             }
847              
848 243 50 66     2705 if (
      33        
      33        
849             ( ( $record->{'class'} =~ $rr_class ) && ( $record->{'ttl'} =~ $rr_class ) )
850             ||
851             ( ( $record->{'class'} =~ $rr_ttl ) && ( $record->{'ttl'} =~ $rr_ttl ) )
852             ) {
853 0         0 die "Invalid ttl/class values!\n";
854             };
855              
856 243 100 66     1706 if ( ( $record->{'class'} =~ $rr_ttl ) || ( $record->{'ttl'} =~ $rr_class ) ) {
857 38         59 my $x = $record->{'class'};
858 38         91 $record->{'class'} = $record->{'ttl'};
859 38         65 $record->{'ttl'} = $x;
860             }
861              
862 243 100       430 if ( $record->{'class'} ) {
863 133         267 $record->{'class'} = uc $record->{'class'};
864 133         371 $dns_last_class{$self} = $record->{'class'};
865             } else {
866             # This case should never happen, because we supply a default.
867             #if ( !defined $dns_last_class{$self} ) {
868             # die "No class defined!\n";
869             #}
870 110         321 $record->{'class'} = $dns_last_class{$self};
871             }
872              
873             # This is silly, but we don't know what type of record we are massaging at
874             # this point. We can detect an SOA record because it's the only type that
875             # supplies this value, which is what we need to do here to properly set
876             # the owner.
877 243 100       510 if ( exists $record->{'minimumTTL'} ) {
878 12         45 $dns_last_name{$self} = $record->{'origin'};
879              
880             # In the case of an SOA record, we fall back to the minimumTTL value
881             # when a TTL isn't otherwise specified. This is what BIND does.
882 12 100       40 if ( $record->{'ttl'} ) {
883 7         34 $record->{'ttl'} = $dns_last_ttl{$self} = uc( $record->{'ttl'} );
884             } else {
885 5 100       19 if ( $dns_last_ttl{$self} ) {
886 3         12 $record->{'ttl'} = $dns_last_ttl{$self};
887             } else {
888 2         9 $record->{'ttl'} = $dns_last_ttl{$self} = uc( $record->{'minimumTTL'} );
889             }
890             }
891              
892 12 100       49 if ( $record->{'origin'} eq '@' ) {
893             # We encountered a @ SOA line without an origin directive above
894             # it, so we will try and guess the origin.
895 5 100       18 if ( !$dns_last_origin{$self} ) {
896 3 50       15 if ( !$dns_id{$self}->{'Origin'} ) {
897 0         0 die "Unknown origin\n";
898             }
899 3         9 $dns_last_origin{$self} = $dns_id{$self}->{'Origin'};
900             }
901 5         21 $record->{'ORIGIN'} = $dns_last_origin{$self};
902             } else {
903 7         15 my $new_origin = $record->{'origin'};
904              
905             # Similar to above, it's origins all the way down. Don't bother
906             # tracking each separately, just collapse them all into the
907             # current origin.
908 7 100       38 if ( $new_origin =~ /\.$/ ) {
909             # If no one has set an $ORIGIN before, we need to use the SOA
910             # line to do it.
911 5 50       20 if ( !$dns_last_origin{$self} ) {
912 5         15 $dns_last_origin{$self} = $new_origin;
913             }
914             }
915             # Now we have a valid ORIGIN for this SOA, so assign it.
916 7         25 $record->{'ORIGIN'} = $dns_last_origin{$self};
917             }
918             # Alright, make sure we know we found this origin.
919 12         50 $dns_found_origins{$self}->{ $record->{'ORIGIN'} } = 1;
920              
921             # Not an SOA record.
922             } else {
923              
924             # The silliness continues: only $GENERATE directives have a lhs, and
925             # don't need a 'name'.
926 231 100       452 if ( !exists $record->{'lhs'} ) {
927 228 100       1895 if ( $record->{'name'} ) {
928 212         451 $dns_last_name{$self} = $record->{'name'};
929             } else {
930             #TRACE( "Record has no name, using last name" );
931 16 50       53 if ( !$dns_last_name{$self} ) {
932 0         0 die "No current owner name\n";
933             }
934 16         34 $record->{'name'} = $dns_last_name{$self};
935             }
936             }
937              
938 231 50       459 if ( !$dns_last_origin{$self} ) {
939 0         0 die "Unknown origin\n";
940             } else {
941 231         566 $record->{'ORIGIN'} = $dns_last_origin{$self};
942             }
943              
944             # Nothing special about TTL parsing for non-SOA records.
945 231 100       399 if ( $record->{'ttl'} ) {
946 90         296 $record->{'ttl'} = $dns_last_ttl{$self} = uc( $record->{'ttl'} );
947             } else {
948 141 50       323 if ( !defined $dns_last_ttl{$self} ) {
949 0         0 die "No ttl defined!\n";
950             }
951 141         518 $record->{'ttl'} = $dns_last_ttl{$self};
952             }
953             }
954              
955             #DUMP( "Record parsed", $record );
956 243         903 return $record;
957             }
958              
959             sub _escape_chars {
960 79     79   102 my $self = shift;
961 79         89 my $clean_me = shift;
962 79         100 local $" = '|';
963              
964 79         82 foreach my $k ( keys( %{$clean_me} ) ) {
  79         216  
965 510         2498 $clean_me->{$k} =~ s/(@ESCAPABLE_CHARACTERS)/\\$1/g;
966             }
967             }
968              
969 0     0 0   sub TRACE { 0 && print @_, $/ }
970 0     0 0   sub DUMP { 0 && require Data::Dumper && TRACE( shift, Data::Dumper::Dumper( @_ ) ) }
971              
972             1;
973             __END__