#!/usr/bin/perl

# Copyright (c) 2010,2012 Peter Palfrader <peter@palfrader.org>
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

use strict;
use warnings;
use threads;

use English;
use Getopt::Long;
use FindBin qw($Bin);
use YAML;
use File::Basename;

my $CHECK = $Bin.'/dsa-check-zone-rrsig-expiration';

$SIG{__DIE__} = sub {
	print @_;
	exit 3;
};

sub convert_time {
	my $ticks = shift;
	my $unit = shift;

	unless (defined $unit) {
		my $newticks;
		($newticks, $unit) = $ticks =~ m/^(\d*)([smhdw]?)$/;
		if (!defined $newticks) {
			print STDERR "Warning: invalid timestring to convert '$ticks'\n";
			return $ticks;
		}
		$ticks = $newticks;
	}

	if ($unit eq 's' || $unit eq '') { }
	elsif ($unit eq 'm') { $ticks *= 60; }
	elsif ($unit eq 'h') { $ticks *= 60*60; }
	elsif ($unit eq 'd') { $ticks *= 60*60*24; }
	elsif ($unit eq 'w') { $ticks *= 60*60*24*7; }
	else { print STDERR "Warning: invalid unit '$unit'\n" }
	return $ticks;
}

sub check_one {
	$SIG{'KILL'} = sub { threads->exit(); };

	my $zone = shift;
	my $check = shift;
	my $extra = shift;
	my $params = shift;

	my @cmd = ($check, '-w', $params->{'warn'}, '-c', $params->{'critical'});
	push(@cmd, '-r', $extra->{'initial_refs'}) if exists $extra->{'initial_refs'};
	push(@cmd, '-d') if $params->{'debug'};
	push(@cmd, $zone);
	open(P, '-|', @cmd) or die ("Cannot run $CHECK for $zone\n");
	my @p = <P>;
	close P;
	$p[0] = $zone.': '. $p[0] if (scalar @p > 0);

	my $res = $CHILD_ERROR >> 8;

	return ($res, \@p);
}

my $USAGE = "Usage: $PROGRAM_NAME [--help] | [--debug] [--timeout=<nn>] [--warn=<nn>] [--critical=<nn>] [--geozonedir=<geodir>] <indir>\n";
my $params = { 'timeout' => 45, 'warn' => '14d', 'critical' => '7d' };
Getopt::Long::config('bundling');
GetOptions (
	'--help' => \$params->{'help'},
	'--timeout=i' => \$params->{'timeout'},
	'--warn=s' => \$params->{'warn'},
	'--debug' => \$params->{'debug'},
	'--critical=s' => \$params->{'critical'},
	'--geozonedir=s' => \$params->{'geozonedir'},
) or die ($USAGE);
if ($params->{'help'}) {
	print $USAGE;
	exit(0);
};
die ($USAGE) unless (scalar @ARGV == 1);
my $INDIR = shift;


my $states = [qw{critical warn unknown ok unsigned}];
my $count =   { map { $_ => [] } @$states };
my $details = { map { $_ => [] } @$states };


my %dnsseczones;
# load list of classic zones that will do DNSSEC
chdir $INDIR or die "chdir $INDIR failed? $!\n";
opendir INDIR, '.' or die ("Cannot opendir $INDIR\n");
for my $file (sort {$a cmp $b} (readdir INDIR)) {
	next if ( -l "$file" );
	next unless ( -f "$file" );

	my $do_dnssec = 1;
	my $delegated = 1;
	my $initial_refs = undef;
	open(F, '<', $file) or die ("Cannot open $file: $!\n");
	for (<F>) {
		if (/^; wzf:\s*dnssec\s*=\s*0\s*$/) { $do_dnssec = 0; }
		if (/^; delegated\s*=\s*no\s*$/) { $delegated = 0; }
		if (/^; check-initial-refs\s*=\s*(.*?)\s*$/) { $initial_refs = $1; }
	};
	close F;

	if ($do_dnssec && $delegated) {
		die "Duplicate zone $file?\n" if exists $dnsseczones{$file};
		$dnsseczones{$file} = {};
		$dnsseczones{$file}->{'initial_refs'} = $initial_refs if defined $initial_refs;
	} else {
		push @{$count  ->{'unsigned'}}, $file;
		push @{$details->{'unsigned'}}, "$file: marked unsigned or undelegated.\n";
	};
}
closedir(INDIR);

# load list of geodns zones that will do DNSSEC
if (defined $params->{'geozonedir'}) {
	chdir $params->{'geozonedir'} or die "chdir $params->{'geozonedir'} failed? $!\n";
	opendir INDIR, '.' or die ("Cannot opendir $params->{'geozonedir'}\n");
	for my $file (sort {$a cmp $b} (readdir INDIR)) {
		next unless $file =~ /\.zone$/;

		my $zone = basename($file, '.zone');
		die "Duplicate zone $zone?\n" if exists $dnsseczones{$zone};
		$dnsseczones{$zone} = {};
	}
	closedir(INDIR);
}

my %threads;
for my $zone (sort {$a cmp $b} keys %dnsseczones) {
	die "Duplicate zone $zone?\n"  if defined $threads{$zone};
	my $thr = threads->create({'context' => 'list'},
	                          \&check_one, $zone, $CHECK, $dnsseczones{$zone}, $params);
	$threads{$zone} = $thr;
}

my $begin = time;
while (time - $begin <= $params->{timeout}) {
	for my $zone (sort {$a cmp $b} keys %threads) {
		next unless $threads{$zone}->is_joinable();

		my ($res, $det) = $threads{$zone}->join();

		my $type = ($res == 0) ? 'ok' :
		           ($res == 1) ? 'warn' :
		           ($res == 2) ? 'critical' :
		                         'unknown';

		push @{$details->{$type}}, @$det;
		push @{$count  ->{$type}}, $zone;
		delete $threads{$zone};
	}
	last if scalar keys %threads == 0;
	print STDERR (scalar keys %threads), " threads left: ", join(" ", keys %threads), "\n" if $params->{'debug'};
	sleep 1;
}
for my $zone (sort {$a cmp $b} keys %threads) {
	push @{$count  ->{'warn'}}, $zone;
	push @{$details->{'warn'}}, "$zone: timeout during check\n";
	$threads{$zone}->kill('KILL')->detach();
}

my $exit = 0;
my %state_mapping = (
	'unknown' => 255,
	'critical' => 2,
	'warn' => 1,
	'ok' => 0 );

for my $state (@$states) {
	@{$count->{$state}}   = sort {$a cmp $b} @{$count->{$state}};
	@{$details->{$state}} = sort {$a cmp $b} @{$details->{$state}};

	if (scalar @{$count->{$state}}) {
		printf "%s: %d", uc($state), scalar @{$count->{$state}};
		if ($state_mapping{$state} > 0) {
			print ": ", join(', ', @{$count->{$state}});
		};
		print "; ";
		$exit = $state_mapping{$state} if ($state_mapping{$state} > $exit);
	};
};
printf "unsigned: %d", scalar @{$count->{'unsigned'}};
print "\n";
for my $state (@$states) {
	for (@{$details->{$state}}) {
		s/\|/;/g;
		print $_;
	}
}
exit $exit;
