#! /usr/bin/perl
#line 3
# Copyright: (C) 2014-2015 Heiko Schlittermann <hs@schlittermann>
# This program is released unter the Terms of the GPL.
use 5.10.1;
use strict;
use warnings;
use if $ENV{DEBUG} => 'Smart::Comments';
use Getopt::Long;
use Pod::Usage;
use File::Copy;
use DNS::Vi;
use if $] >= 5.020, experimental => 'smartmatch';

sub slurp {
    local $/    = undef;
    local @ARGV = @_;
    <>;
}

sub main {
    my %o = (
        local => undef,
        key   => (grep { -f } glob "$ENV{HOME}/.dnsvi/default/K*key")[0]
          // (undef, local => 1),
        server => undef,
        debug  => undef,
        editor => $ENV{EDITOR} // 'vi',
        skip   => [qw/RRSIG NSEC3 NSEC3PARAM NSEC DNSKEY TSIG/],
    );

    GetOptions(
        'k|key=s'    => \$o{key},
        's|server=s' => \$o{server},
        'd|debug!'   => \$o{debug},
        'l|local!'   => \$o{local},
        'editor=s'   => \$o{editor},
      )
      && @ARGV >= 1
      or pod2usage();

    my %auth = get_auth_info shift @ARGV;
    my $zone = $auth{name};

    $o{server} = $o{local} ? 'localhost' : $auth{mname}
      if not defined $o{server};

    my @dig = (
        dig => 'AXFR',
        defined $o{key} ? (-k => $o{key}) : (),
        defined $o{server} ? ("\@$o{server}") : (),
        $zone
    );

    my @zone1 = parse($_ = `@dig`, { -skip => $o{skip} })
      or die "Empty zone\n";

  UNDO:
    my @zone2 = @zone1;
    my $backup;    # it's a tmp file containing the original

  EDIT:
    @zone2 = do {
        if (my $file = shift @ARGV) {
            parse(slurp($file), { -skip => $o{skip} });
        }
        else {
            edit(
                @zone2,
                {
                    -skip   => $o{skip},
                    -editor => $o{editor},
                    -backup => \$backup
                }
            );
        }
    };
    ### @zone2

    my %delta = delta(\@zone1, \@zone2);
    if (!%delta) {
        say 'nothing changed';
        return 0;
    }

  VIEW:
    if ($_ eq 'v' or (map { @{$_} } values %delta) < 10) {
        say 'The following changes need your confirmation.';
        say join "\n", show(@delta{qw/add del/});
    }
    else {
        say 'added: ', 0 + @{ $delta{add} }, ', removed: ',
          0 + @{ $delta{del} };
    }
  CONFIRM:
    print 'action [yqQvVeu?] ?';
    $_ = get_key;

    given ($_) {
        when ('y') { }
        when ('q') { }
        when ('V') { }
        when ('Q') { return 1 }
        when ('e') { goto EDIT }
        when ('v') { goto VIEW }
        when ('u') { goto UNDO }
        when ('?') {
            print <<_;
  y -- yes: submit changes and exit
  q -- quit: save changes as ",dnsvi-$$"
  Q -- quit: discard changes and exit
  v -- view changes
  V -- view changes as nsupdate commands
  e -- edit again
  u -- undo and edit again
  ? -- what?
_
            goto CONFIRM;
        }
        default { goto CONFIRM }
    }

    /^[yV]$/ and update(
        \@zone1,
        @delta{qw/add del/},
        {
            $_ eq 'V' ? (-dry => 1) : (),
            -server => $o{server},
            -local  => $o{local},
            -debug  => $o{debug},
            -key    => $o{key}
        }
      )
      or do {
        if ($backup) {
            copy($backup->filename, ",dnsvi-$$")
              and say "Saved as ',dnsvi-$$'";
        }
      };

    goto CONFIRM if $_ eq 'V';

    return 0;
}

exit main(@ARGV) if not caller;

__END__

=head1 NAME

 dnsvi -- editor for dynamically maintained zones

=head1 SYNOPSIS

 dnsvi [[-l] | [[-k key] [-s server]]] [-d] <zone> [<file>]

=head1 DESCRIPTION

This tools supports you in maintaining a dynamic zone. Normally you'll
use it with the name of zone. For batch mode you may use it with an
additional parameter, die edited zone file.

=head2 OPTIONS

=over

=item B<-l>|B<--local> 

Local mode, when running on the server where the updates need to go to.
But still zone transfers need to be enabled! (default: on, if not
default key file is found, otherwise off)

=item B<-s>|B<--server> B<server-name>

The name of the server to contact for the AXFR and the update.
(default: main nameserver from the SOA record)

=item B<-k>|B<--key> B<key-file>

The name of the key file we need for TSIG (the AXFR will use it,
as well as the update). (default: F<~/.dnsvi/default/K*key)

To create such a key you may use 

    dnssec-keygen -a HMAC-MD5 -b 512 -n USER heiko

Then copy the resulting files somewhere (you'll need both files).
On the server side include the key into to configuration:

    key "<name>" {
	algorithm   HMAC-MD5;
	secret	    "<the secret from the created key file>"
    };

Per zone you should use 

    zone "<zone>" {
	...
	update-policy {
	    grant local-ddns zonesub any;   // support for -l
	    grant <key-name> zonesub;	    // support for -k
	};
	...
    };

=item B<-d>

This option enables debugging of C<nsupdate>. (default: off)

=back

=head1 PREREQUISITES

We need some tools to be installed:

=over

=item B<dig>

The domain information groper is used for the zone transfer currently.

=item B<nsupdate>

The nsupdate tool is used to send the updates back to the server.

=back

=cut
