#!/usr/bin/perl
#
# Copyright 2009-2010 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.7"],

#		['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(1);

@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());

# Ensure the requested level of detail is within our acceptable range.
if (($opts{'d'} < 1) || ($opts{'d'} > 9)) {
    print STDERR "invalid -d option:  $opts{'d'}\n";
    exit(1);
}

# 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 expiry 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/.*\+//;

    return if($keytag eq '');

    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)) {
		my @keyset = ();

		# 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));

		# Divide the keyset into its component keys.
		@keyset = split / /, $set->{'keys'};

		# Print the info for each key in the keyset.
		foreach my $keyname (sort(@keyset)) {
		    print_key($keytype, $krflookup{$keyname});
		}
	    }
	}

	# 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 DIRECTORIES...]

=head1 DESCRIPTION

The B<lsdnssec> program summarizes information about DNSSEC-related files.
These files may be specified on the command line or found in directories
that were given on the command line.  The B<-d> flag controls the amount
of detail in the B<lsdnssec> output.

B<lsdnssec> displays the following information about each zone for which it
collects information:

=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 B<.krf> files B<lsdnssec> finds.

=item rolling status

If any zone keys are being rolled via B<rollerd>, then the status of the
rolling state is shown.  The time needed to reach the next state is also
displayed.

This information is collected from any B<.rollrec> files found by
B<lsdnssec>.

=back

=head1 OPTIONS

=over

=item B<-r>

Show only rolling information from the rollrec files.  By default both
roll-state and key information is shown.

=item B<-k>

Show only keying information from the krf files.  By default both
roll-state and key information is shown.

=item B<-d 1-9>

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

=item B<--debug>

Turns on extra debugging information.

=back

=head1 COPYRIGHT

Copyright 2009-2010 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

