#! /usr/bin/perl

use v5.10;
use warnings;
use strict;
use File::Temp;
use Getopt::Long;
use Pod::Usage;
use File::Basename;
use Net::LibIDN qw(:all);
use if $ENV{DEBUG} => "Smart::Comments";
use DNStools::Config qw(get_config);

my $ME = basename $0;

sub rm_keys($@);
sub check_zone($@);
sub create_ksk($@);
sub create_zsk(@);
sub post_create($@);

my $CHARSET = "UTF-8";
my %cf;

MAIN: {

    %cf = get_config();
    my $cmd;

    system("command -v dnssec-keygen &>/dev/null");
    die "$ME: command 'dnssec-keygen' not found in $ENV{PATH}\n" if $?;

    GetOptions(
        "zsk" => sub { push @$cmd => "zsk" },
        "ksk" => sub { push @$cmd => "ksk" },
        "rm"  => sub { push @$cmd => "rm" },
        "check" => sub { $cmd = "check" },
        "h|help" => sub { pod2usage(-exit => 0, -verbose => 1) },
        "m|man"  => sub {
            pod2usage(
                -exit      => 0,
                # "system('perldoc -V &>/dev/null')" appears shorter, but may not
                # do what you expect ( it still returns 0 on debian squeeze with
                # dash as system shell even if cannot find the command in $PATH)
                -noperldoc => system('perldoc -V >/dev/null 2>&1'),
                -verbose   => 2
            );
        },
      )
      and @ARGV
      and @$cmd == 1
      and $cmd = $cmd->[0]
      or pod2usage;

    # checks the zones in argv if they're managed ones
    my @zones;
    foreach my $utf8zone (@ARGV) {
        my $zone = idn_to_ascii($utf8zone, $CHARSET);

        die "zone $zone is not managed\n"
          if not -f "$cf{master_dir}/$zone/$zone";

        push @zones, $zone;
    }

    given ($cmd) {
        when ("zsk")   { exit create_zsk(@zones) };
        #when ("ksk")   { return create_ksk(@zones) };
        #when ("check") { return check_zone(@zones) };
        #when ("rm")    { return rm_keys(@zones) };
	default		{ die "not implemented\n" };
    };
}

sub rm_keys ($@) {

    # deletes all the keys were handed over -rm in argv
    my ($master_dir, @zone) = @_;

    for (@zone) {
        my $zone = $_;

        my $zpf = "$master_dir/$zone";
        my $ep  = 0;

        if (-e "$zpf/$zone.signed") {
            unlink "$zpf/$zone.signed" and $ep = 1;
        }
        if (-e "$zpf/.keycounter") {
            unlink "$zpf/.keycounter" and $ep = 1;
        }
        if (-e "$zpf/.index.ksk") {
            unlink "$zpf/.index.ksk" and $ep = 1;
        }
        if (-e "$zpf/.index.zsk") {
            unlink "$zpf/.index.zsk" and $ep = 1;
        }
        if (-e "$zpf/dsset-$zone.") {
            unlink "$zpf/dsset-$zone." and $ep = 1;
        }
        if (-e "$zpf/keyset-$zone.") {
            unlink "$zpf/keyset-$zone." and $ep = 1;
        }

        for (glob("$zpf/K$zone*")) {
            chomp($_);
            unlink("$_");
        }

        if ($ep == 1) {
            print " * $zone: removed key-set\n";
        }

        open(my $old, "$zpf/$zone") or die "$zpf/$zone: $!\n";
        my $fh = File::Temp->new(DIR => $zpf)
          or die "Can't create tmpfile: $!\n";
        print $fh grep { not /^\s*\$INCLUDE.*"K$zone.*\.key"/i } <$old>;
        rename($fh->filename => "$zpf/$zone")
          or die "Can't rename " . $fh->filename . " to $zpf/$zone: $!\n";
    }
}

sub create_ksk ($@) {
    my ($master_dir, @zone) = @_;
    my @index;
    my $keyname;

    for (@zone) {
        my $zone = $_;
        my $zpf  = "$master_dir/$zone";

        $keyname =
          `cd $zpf && dnssec-keygen -a RSASHA1 -b 2048 -f KSK -n ZONE $zone`;

        unless (-f "$zpf/.index.ksk") { @index = (); }
        else {
            open(INDEX, "$zpf/.index.ksk") or die "$zpf/.index.ksk: $!\n";
            @index = <INDEX>;
            close(INDEX);
        }

        push @index, $keyname;
        if (@index > 2) { shift(@index); }

        {
            my $fh = File::Temp->new(DIR => "$zpf")
              or die "Can't create tmpdir: $!\n";
            print $fh join "" => @index, "";
            rename($fh->filename => "$zpf/.index.ksk")
              or die "Can't rename "
              . $fh->filename
              . " to $zpf/.index.ksk: $!\n";
        }

        chomp($keyname);
        print " * $zone: new KSK $keyname\n";
        print "!! THE KSK must be published !! \n";

    }
}

sub create_zsk (@) {
    my @zones = @_;

    my $keyname;

    foreach my $zone (@zones) {
        my $dir  = "$cf{master_dir}/$zone";

        chomp($keyname = `cd $dir && dnssec-keygen -a RSASHA1 -b 512 -n ZONE $zone`);

	my @index;
	open(my $idx, "+>>", "$dir/.index.zsk") or die "Can't open $dir/.index.zsk: $!\n";
	seek($idx, 0, 0);
	chomp(@index = <$idx>);

        push @index, $keyname;
	shift @index if @index > 2;

	truncate($idx, 0);
	print $idx join "\n" => @index, "";
	close($idx);

        say "$zone: new ZSK $keyname";

        open(my $kc, ">", "$dir/.keycounter") or die "$dir/.keycounter: $!\n";
        print $kc "0\n";
        close($kc);
    }
}

sub check_zone ($@) {
    my ($master_dir, @zone) = @_;

    for (@zone) {
        my $zone = $_;
        my $zpf  = "$master_dir/$zone";
        my $keyfile;
        my @content;
        my @keylist;

        for (<$zpf/*>) {
            if (m#(K$zone.*\.key)#) {
                $keyfile = $1;
                open(KEYFILE, "<", "$zpf/$keyfile")
                  or die "$zpf/$keyfile: $!\n";
                @content = <KEYFILE>;
                close(KEYFILE);
                for (@content) {
                    if (m#DNSKEY.257#) {
                        push @keylist, $keyfile;
                    }
                }
            }
        }

        open(INDEX, ">$zpf/.index.ksk") or die "$zpf/.index.ksk: $!\n";
        for (@keylist) {
            s#\.key##;
            print INDEX "$_\n";
        }
        close(INDEX);

        print " * $zone: new .index.ksk created\n";
        if (-f "$zpf/.index.zsk") {
            unlink("$zpf/.index.zsk") or die "$zpf/.index.zsk: $!\n";
        }
    }
}

sub post_create ($@) {
    my ($master_dir, @zone) = @_;
    for (@zone) {
        my $zone = $_;
        `touch $master_dir/$zone/$zone`;
        &kill_useless_keys($zone, $master_dir);
        &key_to_zonefile($zone, $master_dir);
    }
}

sub kill_useless_keys ($@) {

    # the function deletes all keys that are not available in the zone

    my $zone       = $_[0];
    my $master_dir = $_[1];
    my @keylist    = ();
    my $zpf        = "$master_dir/$zone";

    open(INDEX, "<$zpf/.index.zsk") or die "$zpf/.index.zsk: $!\n";
    @keylist = <INDEX>;
    close(INDEX);
    open(INDEX, "<$zpf/.index.ksk") or die "$zpf/.index.ksk: $!\n";
    push @keylist, <INDEX>;

    # shortened the key name from the index file on the id in order to
    # be able to compare
    for (@keylist) {
        chomp;
        s#K.*\+.*\+(.*)#$1#;
    }

    # reviewed every key file (KSK, ZSK), whether they are described in
    # the respective index file. if not they will be deleted.
    for (glob("$master_dir/$zone/K*")) {
        chomp;
        my $file     = $_;
        my $rm_count = 1;
        my $keyname;
        for (@keylist) {
            if ($file =~ /$_/) { $rm_count = 0; }
        }
        if ($rm_count == 1) {
            unlink "$file";
            if ($file =~ /$zpf\/(.*\.key)/) {
                print " * $zone: Key $1 removed \n";
            }
        }
    }
}

sub key_to_zonefile ($@) {

    # the function added all keys to the indexfile
    my $zone       = $_[0];
    my $master_dir = $_[1];
    my $zpf        = "$master_dir/$zone";
    my @old_content;
    my @new_content = ();

    open(ZONEFILE, "<$zpf/$zone");
    @old_content = <ZONEFILE>;
    close(ZONEFILE);

    for (@old_content) {
        unless (m#INCLUDE.*key#) { push @new_content, $_; }
    }

    for (<$zpf/*>) {
        if (m#(.*\/)(K.*\.key)#) {
            push @new_content, "\$INCLUDE \"$2\"\n";
        }
    }
    open(ZONEFILE, ">$zpf/$zone") or die "$zpf/$zone: $!\n";
    print ZONEFILE @new_content;
    close(ZONEFILE);
}

__END__

=pod

=head1 NAME

    dnssec-keytool - key management

=head1 SYNOPSIS

    dnssec-keytool {--zsk|--ksk|--rm|--check} zone...

=head1 DESCRIPTION

Blabla.

=head1 OPTIONS

=over

=item B<--zsk> 

Create a new ZSK for the zones.

=item B<--ksk>

Create a new KSK for the zones.

=item B<--rm>

Remote all key material from the zones.

=item B<--check>

???

=back

=cut

# vim:sts=4 sw=4 aw ai sm:
