#!/usr/bin/perl
# Copyright 2004-2008 SPARTA, Inc.  All rights reserved.
# See the COPYING file included with the DNSSEC-Tools package for details.

######################################################################
# Options are described in the POD at the end of this file
######################################################################

use IO::Socket::INET;
use IO::File;
use Getopt::Long;
use Net::DNS::SEC::Tools::conf;
use Net::DNS::SEC::Tools::BootStrap;

our $VERSION = "0.9";

######################################################################
# detect needed GraphViz requirement
#
dnssec_tools_load_mods('GraphViz' => "");

########################################################
# Globals

my $gv;
my $name;
my $class; 
my $type;
my $status;
my $dest;
my $i;
my $socket;
my $count;
my $edge_str;
my $giffile;
my $htmlfile;
my $htmlfh;
my $giftmp;
my $logfile;
my $logfh;
my %opts;
my %loglevels;
my $image_has_changed;

########################################################
# Constants

my $SUCCESS = 1;
my $BOGUS = 2;
my $DATA_MISSING = 3;
my $ERROR = 4;
my $IGNORED = 5;

########################################################
# Defaults

%opts = (
	s => 0,
	a => "127.0.0.1",
	p => "1053",
	g => "val_log_map.gif",
	r => 5,
    i => "",
    m => "",
);

%loglevels = (
    $SUCCESS => 1,
    $BOGUS => 1,
    $DATA_MISSING => 1,
    $ERROR => 1,
    $IGNORED => 0
);

$image_has_changed = 0;

########################################################
# main

# Parse command-line options
GetOptions(\%opts, "s", "a=s", "p=s", "h=s", "r=i", "g=s", "l=s", "i=s", "m=s"
	   "version");

if ($opts{'version'}) {
    print "drawvalmap Version:   $VERSION\n";
    print "DNSSEC-Tools Version: 1.3\n";
    exit;
}

# Parse the dnssec-tools.conf file
my %dtconf = parseconfig();

my $libvalnode = "libval";
my $curnode = $libvalnode; 

$count = 0;
$giffile = $opts{'g'};
$htmlfile = $opts{'h'};
$refresh = $opts{'r'};
$logfile = shift;

# add log levels into our map
my $loglevelstr = $opts{'l'};
if (defined $loglevelstr) {
    my @tmplevels;
    %loglevels = ();
    push @tmplevels, split(/,/, $loglevelstr);
    if ($#tmplevels == 0) {
        $loglevels{$loglevelstr} = 1;
    } else {
        for (my $i = 0; $i <=$#tmplevels; $i++) {
            $loglevels{$tmplevels[$i]} = 1; 
        }
    }
}

# create an HTML file with the image and 
# with auto refresh set to desired value 
if (defined $htmlfile) {
    $htmlfh = new IO::File(">$htmlfile");
    print $htmlfh "<html>\n<head>\n".
	    "<title>Validator Results</title>\n".
	    "<meta http-equiv=\"refresh\" content=\"$refresh\">\n".
	    "</head>\n".
	    "<body> <img src=\"$giffile\" alt=\"Validator Status\"> </body>\n".
	    "</html>";
    $htmlfh->close;
}

$gv = GraphViz->new(rankdir => 1, edge => { fontsize => '9'});
$gv->add_node($libvalnode);

# check if socket operation is desired
if($opts{'s'} == 1) {
	$local_host = $opts{'a'};
	$local_port = $opts{'p'};
	$socket = IO::Socket::INET->new(LocalAddr => $local_host,
                                LocalPort => $local_port,
                                Proto    => "udp",
                                Type     => SOCK_DGRAM)
    	or die "Couldn't bind to $local_host:$local_port\n";

	while ($_=<$socket>) {
        if (!matches_logreq($_)) {
            $curnode = $libvalnode;
            next;
        }
		$curnode = update_graph($curnode);
        if ($image_has_changed) {
            update_gif();
        }
	}

	# Never reached
	close($socket);
} 

if (defined $logfile) {
    $logfh = new IO::File("<$logfile");
} else {
    $logfh = STDIN;
}

# Read from file handle
while ($_=<$logfh>) {
    if (!matches_logreq($_)) { 
        $curnode = $libvalnode;
        next;
    }
	$curnode = update_graph($curnode);
}

update_gif();

#
# End Main
########################################################


########################################################
# 
# Check if line matches log requirements 
#
sub matches_logreq {
    my $line = shift;
    my $ignorestr = $opts{'i'};
    my $requirestr = $opts{'m'};
    if ($line =~ /\s*name=(\S+)\s*class=(\S+)\s*type=(\S+)\s*from-server=(\S+)\s*status=(\S+?):/) {
        ($name, $class, $type, $dest, $status) = ($1, $2, $3, $4, $5); 
        if (($ignorestr && ($line =~ /$ignorestr/)) || 
            ($requirestr && !($line =~ /$requirestr/))) {
            return 0;
        }
        return 1;
    }
    return 0;
}


########################################################
# 
# store graph as gif 
#
sub update_gif {
    # update the image only when something has changed
    $giftmp = $giffile . "tmp"; 
    # generate the gif file
    $gv->as_gif($giftmp);
    rename($giftmp, $giffile);
    $image_has_changed = 0;
}

#############################################################
#
# update the graph
#
sub update_graph {

    my $level;
    my %prop;
    my $newnode;
    my $prevnode = shift; 

    $log[$count] =  "$name $class $type $prevnode $dest $status";
    $edge_str = $dest ;
    $newnode = "$name, $class, $type";

	# remove duplicates from the array so that 
	# all edges on the graph are different
	my %hash = map { $_, 1 } @log;
	@log = keys %hash;

	# Check if something new was added 
	if($count == ($#log + 1)) {
		return $newnode;
	}

	$count = $#log + 1;

	# add the node and an edge from the Validator
    ($level, %prop) = get_ac_lineattr($status);
    if ($loglevels{$level} == 1) {
        $gv->add_node($newnode);
        if ($prevnode ne $newnode) {
            $gv->add_edge($prevnode, $newnode, label => $edge_str, decorateP => '1', %prop); 
        }
        $image_has_changed = 1;
    }

    return $newnode;
}

#############################################################
# get the edge properties based on the error status passed as 
# the parameter
#
sub get_ac_lineattr {

	my %prop;
	my $ac_status;
	$ac_status = shift;
    my $level;

	$prop{'dir'} = 'back';

    
    # success
    if (($ac_status eq "VAL_AC_VERIFIED") ||
        ($ac_status eq "VAL_AC_TRUST")) { 

		$prop{'color'} =  "green";
		$prop{'fillcolor'} =  "green";
        $level = $SUCCESS;

    }
    # trusted bug not validated 
    elsif (($ac_status eq "VAL_AC_IGNORE_VALIDATION") ||
        ($ac_status eq "VAL_AC_TRUSTED_ZONE") ||
        ($ac_status eq "VAL_AC_PROVABLY_UNSECURE") ||
        ($ac_status eq "VAL_AC_BARE_RRSIG")) { 

		$prop{'color'} =  "yellow";
		$prop{'fillcolor'} =  "yellow";
        $level = $IGNORED;

    }
    # not trusted 
    elsif (($ac_status eq "VAL_AC_NOT_VERIFIED") ||
            ($ac_status eq "VAL_AC_UNTRUSTED_ZONE") ||
            ($ac_status eq "VAL_AC_NO_TRUST_ANCHOR")) {

		$prop{'color'} =  "red";
		$prop{'fillcolor'} =  "red";
        $level = $BOGUS;

    }
    # error conditions 
    elsif  (($ac_status eq "VAL_AC_RRSIG_MISSING") ||
        ($ac_status eq "VAL_AC_DNSKEY_MISSING") ||
        ($ac_status eq "VAL_AC_DS_MISSING") ||
        ($ac_status eq "VAL_AC_DATA_MISSING") ||
        ($ac_status eq "VAL_AC_DNS_ERROR")) { 

		$prop{'color'} =  "red";
		$prop{'fillcolor'} =  "red";
		$prop{'style'} = 'dashed';
        $level = $DATA_MISSING;
    } 
    # unexpected errors
    else { 
		$prop{'color'} =  "black";
		$prop{'fillcolor'} =  "black";
        $level = $ERROR;
    } 

	return $level, %prop;
}

#############################################################
# get the edge properties based on the error status passed as 
# the parameter
#
sub get_valstatus_lineattr {

	my %prop;
	my $val_status;
	$val_status = shift;

	$prop{'dir'} = 'back';

	if (($val_status eq "VAL_SUCCESS") ||
		($val_status eq "VAL_NONEXISTENT_NAME")||
		($val_status eq "VAL_NONEXISTENT_TYPE") ||
		($val_status eq "VAL_VALIDATED_ANSWER")) {
		$prop{'color'} =  "green";
		$prop{'fillcolor'} =  "green";
		$prop{'style'} = 'bold';
	} 
    elsif (
		   ($val_status eq "VAL_NONEXISTENT_NAME_NOCHAIN")||
		   ($val_status eq "VAL_NONEXISTENT_TYPE_NOCHAIN")||
           ($val_status eq "VAL_PROVABLY_UNSECURE") ||
           ($val_status eq "VAL_BARE_RRSIG") ||
		   ($val_status eq "VAL_IGNORE_VALIDATION") ||
		   ($val_status eq "VAL_TRUSTED_ZONE") ||
		   ($val_status eq "VAL_TRUSTED_ANSWER") ||
		   ($val_status eq "VAL_LOCAL_ANSWER")) {
		$prop{'color'} =  "yellow";
		$prop{'fillcolor'} =  "yellow";
		$prop{'style'} = 'bold';
    }
	elsif (($val_status eq "VAL_BOGUS") ||
		   ($val_status eq "VAL_UNTRUSTED_ANSWER") ||
		   ($val_status eq "VAL_UNTRUSTED_ZONE") ||
		   ($val_status eq "VAL_BAD_PROVABLY_UNSECURE")||
		   ($val_status eq "VAL_NOTRUST")){
		$prop{'color'} = "red";
		$prop{'fillcolor'} =  "red";
		$prop{'style'} = 'bold';
	} 
	else {
		# errors 
		$prop{'color'} =  "black";
		$prop{'fillcolor'} =  "black";
		$prop{'style'} = 'dashed';
	}

	return %prop;
}


=head1 NAME

drawvalmap - Generate a graphical output of validation status values
             encountered by the validator library.

=head1 SYNOPSIS

drawvalmap <logfile>

drawvalmap

=head1 DESCRIPTION

drawvalmap is a simple utility that can be used to display the validator status values 
in a graphical format. The input to this script is a set of log messages that 
can be read either from file or from a socket. The output is an GIF file containing an image of 
the various validator authentication chain status values. 

The script reads data from STDIN if the logfile and the socket option are both unspecified.
If the -f option is given, the output GIF file is embedded in an HTML file with the given
name. The HTML file auto-refreshes according to the refresh time supplied by the -r option, 
allowing changes to the validator graph to be constantly tracked. 

The typical usage of this script is in the following way:

bash# drawvalmap <logfile>

It would not be uncommon to use this script for
troubleshooting purposes in which case output generated by a driver 
program would be "piped" to this script in the manner shown below.

bash# validate -o6:stdout secure.example.com. | drawvalmap -f val_log_map.html

In each case the script generates the results in a "val_log_map.gif" file. 
In the second case, an HTML file with the name val_log_map.html is also generated. 

=head1 OPTIONS

=over

=item -s

This changes the mode of operation to read input from a socket.
The default address and port to which drawvalmap binds is
127.0.0.1:1053 

=item -a <IP address>

This changes the address to which drawvalmap binds itself to
the specified value. This option takes effect only if the
-s option is also specified.

=item -p <port>

This changes the port to which drawvalmap binds itself to
the specified value. This option takes effect only if the
-s option is also specified.

=item -h <file.html>

This creates an html file with the given name, which contains the gif image of the validation map.

=item -r <refresh period>

This changes the refresh period in the HTML file to the given value. The default is 5 seconds.

=item -g <file.gif>

This changes the name of .gif file to the given value.

=item -i <ignore_pattern_string>

This causes drawvalmap to ignore log records that match the given ignore pattern. 

=item -m <match_pattern_string>

This causes drawvalmap to include only log records that match the given pattern. 
If a given log record matches a pattern given by the -m option and also matches the
pattern given by the -i option the effective result is that of ignoring the record. 

=back
  

=head1 PRE-REQUISITES

GraphViz

=head1 COPYRIGHT

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

=head1 AUTHOR

Suresh Krishnaswamy, hserus@users.sourceforge.net

=cut

