vidns
changeset 6 271dfe27e1d3
parent 5 70ecc1882968
child 7 286a373ab86b
equal deleted inserted replaced
5:70ecc1882968 6:271dfe27e1d3
     1 #! /usr/bin/perl
       
     2 use 5.010;
       
     3 use strict;
       
     4 use warnings;
       
     5 use File::Temp;
       
     6 use Smart::Comments;
       
     7 use Digest::SHA qw(sha512_hex);
       
     8 use Getopt::Long;
       
     9 use Pod::Usage;
       
    10 
       
    11 sub parse {
       
    12     my $data = join '', @_;
       
    13     my @lines = split /\n/, $data;
       
    14 
       
    15     my @zone;
       
    16     my ($origin, $ttl, $last_label, $soa_seen);
       
    17 
       
    18     foreach (@lines) {
       
    19         s{;.*$}{};
       
    20         given ($_) {
       
    21             when (m{^\s*$})                { next }
       
    22             when (m{^\s*\$ORIGIN\s+(\S+)}) { $origin = $1 }
       
    23             when (m{^\s*\$TTL\s+(\S+)})    { $ttl = $1 }
       
    24             when (
       
    25                 m{^(?<label>\S+)?
       
    26 		    \s+(?<ttl>\d[\dwdmhs]*(?=\s+))?
       
    27 		    \s+(?:(?:IN|ANY)\s+)?(?<rrtype>[a-z]\S*(?=\s+))
       
    28 		    \s+(?<data>.*)
       
    29 		  }ix
       
    30               )
       
    31             {
       
    32                 my %rrset = (
       
    33                     label => $last_label =
       
    34                       defined $+{label}
       
    35                     ? $+{label} eq '@'
       
    36                           ? $origin
       
    37                           : $+{label}
       
    38                     : $last_label,
       
    39                       ttl => h2ttl($+{ttl} // $ttl),
       
    40                       rrtype => uc $+{rrtype},
       
    41                       data   => $+{data},
       
    42                 );
       
    43 
       
    44                 if ($rrset{rrtype} eq 'SOA') {
       
    45                     next if $soa_seen;
       
    46                     $soa_seen = 1;
       
    47                 }
       
    48 
       
    49                 # label ergänzen, wenn nicht FQDN
       
    50                 $rrset{label} .= ".$origin"
       
    51                   unless substr($rrset{label}, -1) eq '.';
       
    52 
       
    53                 given ($rrset{rrtype}) {
       
    54 
       
    55                     # origin steht im SOA
       
    56                     when ('SOA') {
       
    57                         $origin = $rrset{label};
       
    58 
       
    59                         # fix the nameserver name
       
    60                         $rrset{data} =~ s{^(\S+[^.])(?=\s)}{$1.$origin};
       
    61 
       
    62                         # fix the hostmaster address
       
    63                         $rrset{data} =~ s{^\S+\s+\K(\S+[^.])(?=\s)}{$1.$origin};
       
    64                     }
       
    65 
       
    66                     # bei einigen RRs müssen wir die Daten korrigieren
       
    67                     when ([qw/MX NS PTR/]) {
       
    68                         $rrset{data} .= ".$origin"
       
    69                           unless substr($rrset{data}, -1) eq '.';
       
    70                     }
       
    71                 }
       
    72                 my $id = sha512_hex(sort %rrset);
       
    73                 push @zone, { id => $id, rrset => \%rrset };
       
    74             }
       
    75         }
       
    76     }
       
    77 
       
    78     # list of {
       
    79     #	id => $id,
       
    80     #	rrset => { label => …, ttl => …, rrtype => …, data => … }
       
    81     # }
       
    82     return @zone;
       
    83 }
       
    84 
       
    85 sub ttl2h {
       
    86     my $seconds = shift;
       
    87     my @out;
       
    88     my @units = ([w => 604800], [d => 86400], [h => 3600], [m => 60], [s => 1]);
       
    89 
       
    90     foreach (@units) {
       
    91         my $x = int($seconds / $_->[1]);
       
    92         push @out, "$x$_->[0]" if $x;
       
    93         $seconds %= $_->[1] or last;
       
    94     }
       
    95 
       
    96     return join '', @out;
       
    97 }
       
    98 
       
    99 sub h2ttl {
       
   100     my $ttl = shift;
       
   101     my $out;
       
   102     my %factor = (
       
   103         w => 604800,
       
   104         d => 86400,
       
   105         h => 3600,
       
   106         m => 60,
       
   107         s => 1,
       
   108     );
       
   109 
       
   110     while ($ttl =~ m{(\d+)([wdhms])}g) {
       
   111         $out += $1 * $factor{$2};
       
   112     }
       
   113 
       
   114     return $out // $ttl;
       
   115 }
       
   116 
       
   117 sub nice {
       
   118 
       
   119     # get a list of { id => $id, rrset => \%rrset }
       
   120     my @zone =
       
   121       sort {
       
   122         length $a->{label} <=> length $b->{label}
       
   123           or $a->{label}
       
   124           cmp $b->{label}
       
   125       } map { $_->{rrset} } @_;
       
   126 
       
   127     my @out;
       
   128     my $origin = (grep { $_->{rrtype} eq 'SOA' } @zone)[0]->{label};
       
   129     my $ttl    = (grep { $_->{rrtype} eq 'SOA' } @zone)[0]->{ttl};
       
   130     my $l1 = (sort map { index $_->{label}, '.' } @zone)[-1];
       
   131     my $l2 = (sort map { length $_->{rrtype} } @zone)[-1];
       
   132     push @out, "\$ORIGIN $origin", "\$TTL " . ttl2h($ttl);
       
   133 
       
   134     my $print = sub {
       
   135         my %r = %{ +shift };
       
   136         state $last_label;
       
   137 
       
   138         $r{label} = '@' if $r{label} eq $origin;
       
   139         $r{label} =~ s{\.\Q$origin\E$}{};
       
   140         $r{data} =~ s{\.\Q$origin\E$}{} if $r{rrtype} ~~ [qw(MX SOA PTR)];
       
   141         $r{ttl} = $r{ttl} == $ttl ? '' : ttl2h($r{ttl});
       
   142         $r{label} = do {
       
   143             if (defined $last_label and $r{label} eq $last_label) { '' }
       
   144             else { $last_label = $r{label} }
       
   145         };
       
   146 
       
   147         return sprintf '%-*s %6s %-*s    %s',
       
   148           $l1 => $r{label},
       
   149           $r{ttl},
       
   150           $l2 => $r{rrtype},
       
   151           $r{data};
       
   152     };
       
   153     push @out, $print->($_) foreach @zone;
       
   154     return join "\n", @out;
       
   155 }
       
   156 
       
   157 sub delta {
       
   158     my ($zone1, $zone2) = @_;
       
   159     my %zone1 = map { $_->{id}, $_->{rrset} } @$zone1;
       
   160     my %zone2 = map { $_->{id}, $_->{rrset} } @$zone2;
       
   161     my @keys1 = keys %zone1;
       
   162     my @keys2 = keys %zone2;
       
   163     delete @zone1{@keys2};
       
   164     delete @zone2{@keys1};
       
   165     say 'update add ', join ' ' => @{$_}{qw/label ttl rrtype data/}
       
   166       foreach values %zone2;
       
   167     say 'update delete ', join ' ' => @{$_}{qw/label ttl rrtype data/}
       
   168       foreach values %zone1;
       
   169     exit;
       
   170 }
       
   171 
       
   172 sub main {
       
   173     my %o = (
       
   174         key    => undef,
       
   175         server => undef,
       
   176     );
       
   177 
       
   178     GetOptions(
       
   179         'k|key=s'    => \$o{key},
       
   180         's|server=s' => \$o{server},
       
   181       )
       
   182       && @ARGV == 1
       
   183       or pod2usage();
       
   184 
       
   185     my @dig = (
       
   186         dig => 'AXFR',
       
   187         defined $o{key} ? (-k => $o{key}) : (),
       
   188         defined $o{server} ? ("\@$o{server}") : (),
       
   189         $ARGV[0]
       
   190     );
       
   191 
       
   192     my @zone1 = grep {
       
   193         not $_->{rrset}{rrtype} ~~
       
   194           [qw(RRSIG NSEC3 NSEC3PARAM NSEC DNSKEY TSIG)]
       
   195     } parse(`@dig`);
       
   196 
       
   197     my $tmp = File::Temp->new();
       
   198     $tmp->print(nice @zone1);
       
   199     $tmp->flush();
       
   200     system $ENV{EDITOR} // 'vi' => $tmp->filename;
       
   201     $tmp->seek(0, 0);
       
   202     my @zone2 = parse(<$tmp>);
       
   203     delta(\@zone1, \@zone2);
       
   204     exit;
       
   205 }
       
   206 
       
   207 exit main(@ARGV) if not caller;
       
   208 
       
   209 __END__
       
   210 
       
   211 =head1 NAME
       
   212 
       
   213  vidns -- editor for dynamically maintained zones
       
   214 
       
   215 =head1 SYNOPSIS
       
   216 
       
   217  vidns [-k key] [-s server] <zone>
       
   218 
       
   219 =head1 DESCRIPTION
       
   220 
       
   221 =head1 PREREQUISITES
       
   222 
       
   223 We need some tools to be installed:
       
   224 
       
   225 =over
       
   226 
       
   227 =item B<dig>
       
   228 
       
   229 The domain information grabber is used for the zone transfer currently.
       
   230 
       
   231 =item B<nsupdate>
       
   232 
       
   233 The nsupdate tool is used to send the updates back to the server.
       
   234 
       
   235 =back
       
   236 
       
   237 =cut