Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add $INCLUDE parsing and RFC 2308 $TTL compliance #2

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
Open
88 changes: 81 additions & 7 deletions lib/DNS/ZoneParse.pm
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ my (
%dns_ptr, %dns_a4, %dns_srv, %dns_hinfo, %dns_rp, %dns_loc,
%dns_generate,
%dns_last_name, %dns_last_origin, %dns_last_class, %dns_last_ttl,
%dns_found_origins, %unparseable_line_callback, %last_parse_error_count,
%dns_dollar_ttl, %dns_found_origins, %unparseable_line_callback, %last_parse_error_count,
%alt_ttl_mode
);

my %possibly_quoted = map { $_ => undef } qw/ os cpu text mbox /;
Expand All @@ -33,14 +34,15 @@ sub new {
my $file = shift;
my $origin = shift;
my $unparseable_callback = shift;
my $alt_ttl_mode = shift;
my $self = bless [], $class;

if ( ref $unparseable_callback eq 'CODE' ) {
$unparseable_line_callback{$self} = $unparseable_callback;
}

$self->_initialize();
$self->_load_file( $file, $origin ) if $file;
$self->_load_file( $file, $origin, $alt_ttl_mode ) if $file;
return $self;
}

Expand Down Expand Up @@ -82,6 +84,7 @@ sub DESTROY {
delete $dns_last_name{$self};
delete $dns_last_origin{$self};
delete $dns_last_ttl{$self};
delete $dns_dollar_ttl{$self};
delete $dns_last_class{$self};
delete $dns_found_origins{$self};
delete $unparseable_line_callback{$self};
Expand Down Expand Up @@ -371,14 +374,16 @@ sub _initialize {
$dns_last_name{$self} = undef;
$dns_last_origin{$self} = undef;
$dns_last_ttl{$self} = undef;
$dns_dollar_ttl{$self} = undef;
$alt_ttl_mode{$self} = 0;
$dns_last_class{$self} = 'IN'; # Class defaults to IN.
$dns_found_origins{$self} = {};
$last_parse_error_count{$self} = 0;
return 1;
}

sub _load_file {
my ( $self, $zonefile, $origin ) = @_;
my ( $self, $zonefile, $origin, $alt_ttl_mode ) = @_;
my $zone_contents;
if ( ref( $zonefile ) eq 'SCALAR' ) {
$zone_contents = $$zonefile;
Expand All @@ -392,15 +397,19 @@ sub _load_file {
croak qq[DNS::ZoneParse Could not open input file: "$zonefile":$!];
}
}
if ( $self->_parse( $zonefile, $zone_contents, $origin ) ) { return 1; }
if ( $self->_parse( $zonefile, $zone_contents, $origin, $alt_ttl_mode ) ) { return 1; }
}

sub _parse {
# Support IsAlnum for unicode names.
use utf8;
my ( $self, $zonefile, $contents, $origin ) = @_;
my ( $self, $zonefile, $contents, $origin, $alt_ttl_mode ) = @_;
$self->_initialize();

if ( defined $alt_ttl_mode ) {
$alt_ttl_mode{$self} = $alt_ttl_mode;
}

# Here's how we auto-detect the zonefile and origin. Note, the zonefile is
# only used to print out a comment in the file, so its okay if we're
# inaccurate. First, prefer what the user configures. Next, try to read a
Expand Down Expand Up @@ -457,6 +466,66 @@ sub _parse {
my $generate_range = qr{\d+\-\d+(?:/\d+)?};
my $last_good_line;

# Process $INCLUDEs into zone records by reading RRs from file, cleaning
# them and letting them be parsed like all other records.
if (grep { $_ =~ /\$INCLUDE/ } @$records) {
my $inc_data;
my @comb;

local $/;

my @inc_directives = grep { $_ =~ /\$INCLUDE/ } @$records;

foreach my $inc_direct ( @inc_directives ) {
# We have to split $INCLUDE direcives by space unless inside a quoted
# string as described in RFC 1035 'character string' so we split
# quoted strings and then split unquoted strings by recognition of the
# quotes remaining from the first split.
#
# Empty strings are removed to keep consistent results when
# rougue quotes are left in $INCLUDE params.
$inc_direct =~ s/\$INCLUDE //;

my ($inc_f, $inc_domain, $inc_comment) =
grep { $_ ne '' } map {
if ($_ !~ /\"/) { split /\s/, $_ } else { $_ =~ s/\"//g; $_; }
} split /\" | \"/, $inc_direct;

if ( -e $inc_f ) {
open RINCLUDE, "<$inc_f"
or croak qq[DNS::ZoneParse Could not open file specified in \$INCLUDE: "$inc_f"];
} else {
croak qq[DNS::ZoneParse \$INCLUDE references missing file: "$inc_f"];
}

$inc_data = <RINCLUDE>;
close RINCLUDE;

# Set $ORIGINs around included zones if an
# include domain is specified setting the adding back
# the last origin specified as to not modify the relative
# origin of the including zone.
if (defined $inc_domain) {
my $last_origin = (grep { $_ =~ /\$ORIGIN/ } @$records)[-1];

if (! defined $last_origin) {
# Use identified origin if $ORIGIN not present
$last_origin = "\$ORIGIN $origin";
}

$inc_data = sprintf( "\$ORIGIN %s\n%s\n%s",
$inc_domain, $inc_data, $last_origin );
}

@$records = (
@$records,
@{ $self->_clean_records( $inc_data ) }
);
}

@$records = grep { $_ !~ /\$INCLUDE/ } @$records;
}

foreach ( @$records ) {
#TRACE( "parsing line <$_>" );

Expand Down Expand Up @@ -622,6 +691,7 @@ sub _parse {
$dns_soa{$self}->{ttl} = $1;
}
$dns_last_ttl{$self} = $1;
$dns_dollar_ttl{$self} = $1 if $alt_ttl_mode{$self};
} elsif (
/^($valid_name)? \s+
$ttl_cls
Expand Down Expand Up @@ -945,10 +1015,14 @@ sub _massage {
if ( $record->{'ttl'} ) {
$record->{'ttl'} = $dns_last_ttl{$self} = uc( $record->{'ttl'} );
} else {
if ( !defined $dns_last_ttl{$self} ) {
# Set TTL to $TTL, last TTL or SOA TTL respectively as per RFC 2308
if ( $dns_dollar_ttl{$self} && $alt_ttl_mode{$self} ) {
$record->{'ttl'} = $dns_dollar_ttl{$self};
} elsif ( $dns_last_ttl{$self} ) {
$record->{'ttl'} = $dns_last_ttl{$self};
} else {
die "No ttl defined!\n";
}
$record->{'ttl'} = $dns_last_ttl{$self};
}
}

Expand Down
132 changes: 132 additions & 0 deletions t/dns-zoneparse-dollar-ttl.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
use strict;
BEGIN { $^W++ }
use Test::More tests => 13;
use lib '../lib/';

# See if the module compiles - it should...
require_ok( 'DNS::ZoneParse' );

my $zone_data = do { local $/; <DATA> };
close DATA;

sub on_parse_fail {
my ( $dns, $line, $reason ) = @_;
if ( $line !~ /this should fail/ ) {
ok( 0, "Parse failure ($reason) on line: $line\n" );
}
}

# Specify alternate TTL parsing (using RFC2308 perscribed method of parsing
# $TTL directives).
my $alt_ttl_mode = 1;

my $str_zonefile = DNS::ZoneParse->new( \$zone_data, undef, \&on_parse_fail, $alt_ttl_mode );
ok( $str_zonefile, 'new obj from string' );
ok( $str_zonefile->last_parse_error_count() == 0, "caught all errors (none!)" );
test_zone( $str_zonefile );

my $serialized = $str_zonefile->output();
$str_zonefile = DNS::ZoneParse->new( \$serialized, undef, \&on_parse_fail, $alt_ttl_mode );
ok( $str_zonefile, 'new obj from output' );
ok( $str_zonefile->last_parse_error_count() == 0, "caught all errors (none!)" );
test_zone( $str_zonefile );

sub test_zone {
my $zf = shift;

# Ensure $TTL and absence of $TTL is working as per RFC 2308
is_deeply(
$zf->soa,
{
'ORIGIN' => 'dns-zoneparse-test.net.',
'minimumTTL' => '86400',
'serial' => '2000100502',
'refresh' => '10801',
'retry' => '3600',
'expire' => '691200',
'ttl' => '3600',
'primary' => 'ns0.dns-zoneparse-test.net.',
'origin' => '@',
'email' => 'support\\.contact.dns-zoneparse-test.net.',
'class' => 'IN',
},
'SOA parsed ok'
);

is_deeply(
$zf->ns,
[
{
'ORIGIN' => 'dns-zoneparse-test.net.',
'class' => 'IN',
'ttl' => '10',
'host' => 'ns1.dns-zoneparse-test.net.',
'name' => 'ns1',
},
],
'NS parsed ok'
);

is_deeply(
$zf->a,
[
{
'ORIGIN' => 'dns-zoneparse-test.net.',
'class' => 'IN',
'ttl' => '3600',
'name' => '@',
'host' => '127.0.0.1'
},
{
'ORIGIN' => 'dns-zoneparse-test.net.',
'class' => 'IN',
'ttl' => '3600',
'name' => 'ns1',
'host' => '127.0.0.2'
},
{
'ORIGIN' => 'dns-zoneparse-test.net.',
'class' => 'IN',
'ttl' => '3600',
'name' => 'www',
'host' => '127.0.0.3'
},
],
'A parsed ok'
);

is_deeply(
$zf->cname,
[
{
'ORIGIN' => 'dns-zoneparse-test.net.',
'class' => 'IN',
'ttl' => '3600',
'name' => 'ftp',
'host' => 'www'
},
],
'CNAME parsed ok'
);

}

__DATA__
$ORIGIN dns-zoneparse-test.net.
$TTL 3600
@ IN SOA ns0.dns-zoneparse-test.net. support\.contact.dns-zoneparse-test.net. (
2000100502 ; serial number
10801 ; refresh
3600 ; retry
691200 ; expire
86400 ) ; minimum TTL

; This zone demonstrates that without the $TTL directive
; TTLs are inherited from the last TTL specified.

IN A 127.0.0.1
ns1 IN A 127.0.0.2
IN 10 NS ns1.dns-zoneparse-test.net.

www IN A 127.0.0.3
ftp IN CNAME www
Loading