#!/usr/bin/perl
#
# Copyright 2009 SPARTA, Inc.  All rights reserved.  See the COPYING
# file distributed with this software for details.
#

use strict;

use Getopt::Long qw(:config no_ignore_case_always);

use Net::DNS::SEC::Tools::BootStrap;
use Net::DNS::SEC::Tools::conf;
use Net::DNS::SEC::Tools::defaults;
use Net::DNS::SEC::Tools::keyrec;
use Net::DNS::SEC::Tools::rollrec;
use Net::DNS::SEC::Tools::rollmgr;
use Net::DNS::SEC::Tools::tooloptions;
use Net::DNS::SEC::Tools::timetrans;
use Net::DNS::SEC::Tools::QWPrimitives;
use Net::DNS::ZoneFile::Fast;
use POSIX qw(getcwd);
use IO::Dir;
use Data::Dumper;

#
# Detect required Perl modules.
#
dnssec_tools_load_mods('Date::Parse'	=> "",
		       'Date::Format'	=> "",);

our $VERSION = "0.1";

my %opts = (d => '5');

DTGetOptions(config => [qw(allow_zero)],
	     \%opts,
		['GUI:VERSION',"0.1\nDNSSEC-Tools Version: 1.5"],

#		['o|obsolete', 'Show obsolete keys/etc as well as current'],

		['z|zone=s',   'Show information only about zone1,zone2,...'],

	        ['k|key-data',    'Show keying data (default = everything)'],
	        ['r|roll-status', 'Show rolling status (default = everything)'],

		['d|detail=i', 'Details level (1-9, 5 = default)',
		 values => [1..9], type => 'menu', default => 5],

		["debug",
		 "Debugging output (show extra processing information)."],

		['GUI:otherargs_text',"[FILES OR DIRECTORIES...]"],
	       ) || exit;

@ARGV = (getcwd()) if ($#ARGV == -1);
# XXX: deal with '.' passed in on the command line

my @files = @ARGV;
my @filestodo;
my %zoneinfo;

my $todaystime = time();
my $gmtime = str2time(scalar gmtime());

# generate the list of files to read in.
expand_files(@files);

# load the contents of everything into memory.
load_files(@filestodo);

# print summarized results
print_zone_information();


#
# load the various types of files we understand
#

my %krfdata;
my %krflookup;

sub remember_keyrecs {
    my @krnames = keyrec_names();
    foreach my $krn (@krnames) {
	debug("looking up: $krn\n");
	my $krf = keyrec_fullrec($krn);
	if ($krf->{'keyrec_type'}) {
	    push @{$krfdata{$krf->{'zonename'}}{$krf->{'keyrec_type'}}}, $krf;
	    $krflookup{$krf->{'keyrec_name'}} = $krf;
	}
    }
}

my %rrdata;
my %rrlookup;

sub remember_rollrecs {
    my @rrnames = rollrec_names();
    foreach my $rrn (@rrnames) {
	debug("looking up rr: $rrn\n");
	my $rr = rollrec_fullrec($rrn);
	if ($rr->{'rollrec_type'}) {
	    push @{$rrdata{$rr->{'rollrec_name'}}{$rr->{'rollrec_type'}}}, $rr;
	    $rrlookup{$rr->{'rollrec_name'}} = $rr;
	}
    }
}

sub load_files {
    my @todolist = @_;
    foreach my $file (@todolist) {
	if ($file =~ /\.krf$/) {
	    # load it as a krf
	    keyrec_read($file);
	    remember_keyrecs();
	} elsif ($file =~ /\.(rollrec|rrf)$/) {
	    # load it as a rollrec
	    rollrec_read($file);
	    remember_rollrecs();
	} else {
	    # XXX: maybe look at the first line to determine what it is?
	}
	# XXX: load .signed files and look for sig expirary times?
    }
}

# expand the list of files/directories to just all the files
# i.e. remove the directories and replace with their file contents
sub expand_files {
    my @files = @_;
    foreach my $file (@files) {
	if (-f $file) {
	    push @filestodo, $file;
	} elsif (-d $file) {
	    my $dirh = IO::Dir->new($file);
	    my $direntry;
	    $file .= "/" if ($file !~ /\/$/);
	    while (defined($direntry = $dirh->read)) {
		my $fullfile = $file . $direntry;
		push @filestodo, $fullfile if (-f $fullfile);
		# XXX: add recursive option.
	    }
	}
    }
}

sub get_ksk_phase3_length {
    my ($rollrec) = @_;

    my $length = $rollrec->{'maxttl'}*2;

    if ($rollrec->{'istrustanchor'}) {
	# we should do a proper RFC5011 waiting period
	# use either their defined value or a default of 60 days
	# The 60 days comes from the rollerd 60 day default
	my $addtime = 
	  Net::DNS::ZoneFile::Fast::ttl_fromtext($rollrec->{'holddowntime'});
	
	$addtime ||= (2*30*24*60*60);

	$length += $addtime;
    }

    return $length;
}


sub print_key {
    my ($keytype, $key) = @_;

    my $keytag = $key->{'keyrec_name'};
    $keytag =~ s/.*\+//;

    my $smalltype   = substr($keytype, 0, 3);
    my $smallstatus = substr($keytype, 3, 3);

    my $life = $key->{$smalltype . 'life'};
    my $created = $key->{'keyrec_gensecs'};
    my $age = $todaystime - $created;
    my $percent;

    if ($age > $life) {
	$percent = 100;
    } else {
	$percent = int(100*($age/$life));
    }

    printf("  key:  %05.5d %3.3s %3.3s %4.4d %-12.12s %3.3d%% %30s|\n",
	   $keytag, uc($keytype), $smallstatus,
	   $key->{$smalltype . 'length'}, $key->{'algorithm'},
	   $percent, fuzzytimetrans($life));

    if ($opts{'d'} > 8) {
	printf("  file: %s\n", $key->{'keypath'});
    }

    if ($opts{'d'} > 4) {
	print "  life: |";
	print "=" x int(65*($percent/100));
	print "O";
	print "-" x (65-int(65*($percent/100)));
	print ($percent < 100 ? "|" : "X");
	print "\n";
    }

    if ($percent == 100 && $opts{'d'} > 1) {
	print "  WARN:       *** key has passed its expected lifetime ***\n";
    } elsif ($opts{'d'} > 6) {
	print "        (" . fuzzytimetrans($life-$age) . " remaining)\n";
    }

    print "\n" if ($opts{'d'} > 2);
}

sub print_roll {
    my ($rolltype, $rollrec) = @_;

    my $type;

    if ($rollrec->{'zskphase'} > 0) {
	$type = 'zsk';
    } elsif ($rollrec->{'kskphase'} > 0) {
	$type = 'ksk';
    } else {
	print("  roll: not current rolling any keys\n");
	return;
    }

    my $phase = $rollrec->{$type . 'phase'};
    printf("  roll: %3.3s phase:  %d - %s\n", uc($type),
	   $phase, rollmgr_get_phase(uc($type), $phase));

    printf("        started:    $rollrec->{phasestart}\n")
      if ($opts{'d'} > 6);

    my $started = str2time($rollrec->{'phasestart'});

    #
    # bar graph
    #
    my $rolltimelength = 0;
    my $currentperiod = 0;

    my $barlength = 66;

    my @phasespots;

    if ($type eq 'zsk') {
	# phase 1
	$rolltimelength += $rollrec->{'maxttl'}*2;
	$phasespots[1] = 0;

	if ($phase == 1) {
	    $currentperiod += ($gmtime - $started);
	} else {
	    $currentperiod = $rolltimelength;
	}

	# phase 2
	$phasespots[2] = $rolltimelength;
	# $rolltimelength += 0;
	# $currentperiod += 0;

	# phase 3
	$phasespots[3] = $rolltimelength;
	if ($phase == 3) {
	    $currentperiod = $rolltimelength + ($gmtime - $started);
	} else {
	    $currentperiod = $rolltimelength + $rollrec->{'maxttl'}*2;
	}
	$rolltimelength += $rollrec->{'maxttl'}*2;

	# phase 4
	$phasespots[4] = $rolltimelength;
	# $rolltimelength += 0;
	# $currentperiod += 0;

	my $percentdone = $currentperiod / $rolltimelength;

	if ($opts{'d'} > 4) {
	    #
	    # now construct the timeline based on the gathered information
	    #

	    my $outstring = "=" x int($barlength * $percentdone);
	    $outstring .= "-" x int($barlength - ($barlength * $percentdone));

	    my $lastpt = -1;
	    foreach my $phasepoint (1..4) {
		my $pt =
		  int($barlength * $phasespots[$phasepoint]/$rolltimelength);
		$pt++ if ($lastpt == $pt);

		$pt -= 1 if ($pt == $barlength);

		substr($outstring, $pt, 1, $phasepoint);
		$lastpt = $pt;
	    }

	    # and finally print it out
	    printf("  time: |%s|\n", $outstring);
	}

    } elsif ($type eq 'ksk') {
	# phase 1
	$rolltimelength += $rollrec->{'maxttl'}*2;
	$phasespots[1] = 0;

	if ($phase == 1) {
	    $currentperiod += ($gmtime - $started);
	} else {
	    $currentperiod = $rolltimelength;
	}

	# phase 2
	$phasespots[2] = $rolltimelength;
	# $rolltimelength += 0;
	# $currentperiod += 0;

	# phase 3
	$phasespots[3] = $rolltimelength;
	$rolltimelength += get_ksk_phase3_length($rollrec);

	if ($phase == 3) {
	    $currentperiod = $phasespots[3] + ($gmtime - $started);
	} else {
	    $currentperiod = $rolltimelength;
	}

	# phase 4
	$phasespots[4] = $rolltimelength;
	# $rolltimelength += 0;
	# $currentperiod += 0;

	# phase 5
	$phasespots[5] = $rolltimelength;
	# $rolltimelength += 0;
	# $currentperiod += 0;

	# phase 6
	$phasespots[6] = $rolltimelength;
	# $rolltimelength += 0;
	# $currentperiod += 0;

	# phase 7
	# XXX: rollerd needs to be fixed to wait for 2*parent_TTL
	$phasespots[7] = $rolltimelength;
	# $rolltimelength += 0;
	# $currentperiod += 0;

	my $percentdone = $currentperiod / $rolltimelength;

	#
	# now construct the timeline based on the gathered information
	#

	if ($opts{'d'} > 4) {
	    my $outstring = "=" x int($barlength * $percentdone);
	    $outstring .= "-" x int($barlength - ($barlength * $percentdone));

	    my $lastpt = -1;
	    foreach my $phasepoint (1..7) {
		my $pt =
		  int($barlength * $phasespots[$phasepoint]/$rolltimelength);
		$pt++ if ($lastpt == $pt);

		$pt = $barlength-2 if ($pt > $barlength-2);

		substr($outstring, $pt, 1, $phasepoint);
		$lastpt = $pt;
	    }

	    # ugly hack to force fit 4, 5 and 6 phases in to the left
	    $outstring =~ s/.7/67/ if ($outstring !~ /6/);
	    $outstring =~ s/.6/56/ if ($outstring !~ /5/);
	    $outstring =~ s/.5/45/ if ($outstring !~ /4/);

	    # and finally print it out
	    printf("  time: |%s|\n", $outstring);
	}
    }

    #
    # remaining time
    #
    if ($opts{'d'} > 4 &&
	(($type eq 'zsk' &&
	  ($phase == 1 || $phase == 3)) ||
	 ($type eq 'ksk' &&
	  ($phase == 1 || $phase == 3)))
       ) {
	my $timeremaining = $rollrec->{'maxttl'}*2;
	$timeremaining = get_ksk_phase3_length($rollrec)
	  if ($type eq 'ksk' && $phase == 3);

	$timeremaining -= ($gmtime - $started);

	my $phaseremaining = "";
	if ($timeremaining > 0) {
	    $phaseremaining = fuzzytimetrans($timeremaining);
	} else {
	    $phaseremaining = "none -- ready for next phase";
	}

	printf("  time: remaining phase $phase: %-30.30s %17.17s\n", $phaseremaining, "total: " . fuzzytimetrans($rolltimelength) . "|");
    }

    if ($opts{'d'} > 2) {
	printf("\n");
    }
}

sub print_zone_information {
    my $zonelineformat = "%-30s\n";

    my @zones;
    if ($opts{'z'}) {
	@zones = split(/\s*,\s*/, $opts{'z'});
    } else {
	@zones = keys(%krfdata);
    }
    foreach my $zone (@zones) {
	next if ($zone eq '');
	
	print "Zone: $zone\n";

	# print each active key
	if ($opts{'k'} || !($opts{'k'} || $opts{'r'})) {
	    foreach my $keytype (qw(kskpub kskcur zskpub zskcur)) {

		# for each keytype, look up the set containing the key(s)
		# print the resulting key.
		my $set = $krflookup{$krflookup{$zone}{$keytype}};
		next if (!defined($set));
		print_key($keytype, $krflookup{$set->{'keys'}});
	    }
	}

	# print each roller roll
	if ($opts{'r'} || !($opts{'k'} || $opts{'r'})) {
	    foreach my $rolltype (qw(roll)) {
		foreach my $roll (@{$rrdata{$zone}{$rolltype}}) {
		    print_roll($rolltype, $rrlookup{$roll->{'rollrec_name'}});
		}
	    }
	}

	print "\n";
    }
}


#######################################################################
# debugging output
#
sub debug {
    print STDERR @_ if ($opts{'v'});
}

=pod

=head1 NAME

lsdnssec - List DNSSEC components of zones from files or directories

=head1 SYNOPSIS

  lsdnssec [-d 1-9] [FILES OR DIRECTIORIES...]

=head1 DESCRIPTION

The lsdnssec program summarizes together information about DNSSEC
related files given to it, or in files within directories that were
given to it.  Various levels of detail can be output based on the -d flag.

Each zone that the tool collects information about has the following
information shown about it:

=over

=item keys

Key information is shown about the keys currently in use.  A bar graph
is included that shows the age of the key with respect to the
configured expected key-life time.

This information is collected from any .krf files the tool finds.

=item rolling status

If any zone keys are being rolled via rollerd, then the status of the
rolling state is shown (along with the time needed to reach the next
state).

This information is collected from any .rollrec files the tool finds.

=back

=head1 OPTIONS

=over

=item -r

Show only rolling information from the rollrec files.  By default both
rolling and keying information is shown.

=item -k

Show only keying information from the krf files.  By default both
rolling and keying information is shown.

=item -d 1-9

Controls the amount of information shown in the output.  A level of 9
shows everything, and a level of 1 shows a minimal amount.  The
default level is 5.

=item --debug

Turns on extra debugging information.

=back

=head1 COPYRIGHT

Copyright 2009 SPARTA, Inc.  All rights reserved.
See the COPYING file included with the DNSSEC-Tools package for details.

=head1 AUTHOR

Wes Hardaker <hardaker AT AT AT users.sourceforge.net>

=head1 SEE ALSO

B<lskrf(1)>

B<zonesigner(8)>,
B<rollerd(8)>

=cut

