#! /usr/bin/perl -wT
#
# Check_dns - Check DNS data - replaces `C' code version
#
# Copyright (c) 2004 Howard Wilkinson, <howard@cohtech.com> Coherent Technology Limited
#
# Extended form of check_dns to provide response matching without need for
# external utility
#

require 5.004;
use POSIX;
use strict;
use lib utils.pm ;
use Plugin;
use Plugin::Parameter qw($helpparameterlist $versionparameterlist $hostnameparameter
			 $timeoutparameter $verboseparameter);

use Net::DNS;

use vars qw($opt_V $opt_h $opt_s $opt_r $opt_t $opt_T
	    $opt_H $opt_a $opt_S $opt_D $opt_m $PROGNAME);

use utils qw(%ERRORS &usage);

sub check_known_querytype ($$$);
sub check_known_matchtype ($$$);
sub check_result ($$$);

$ENV{'PATH'}='';
$ENV{'BASH_ENV'}='';
$ENV{'ENV'}='';
$ENV{'CDPATH'}='';

$hostnameparameter->default("www.yahoo.com");

my $serverparameter = new Plugin::Parameter(-name => "server", -flags => [ 's', 'server' ],
					    -optional => "yes", -valueoptional => "no", -type => "HOSTNAME",
					    -checker => \&Plugin::Parameter::_check_hostname,
					    -description => "DNS server to query for the lookup. If not specified the nameserver from resolv.conf will be used");
my $reverseparameter =  new Plugin::Parameter(-name => "reverse-server", -flags => [ 'r', 'reverse-server' ],
					      -optional => "yes", -valueoptional => "no", -type => "HOSTNAME",
					      -checker => \&Plugin::Parameter::_check_hostname,
					      -description => "DNS server to query for the reverse lookup. If not specified the forward server will be used");
my $querytypeparameter = new Plugin::Parameter(-name => "querytype", -flags => [ 'T', 'querytype' ],
					       -optional => "yes", -valueoptional => "no", -type => "QUERYTYPE", -default => "ANY",
					       -checker => \&check_known_querytype,
					       -description => "Type of query to issue. Available types are - [A,CNAME,HINFO,MINFO,MX,NS,PTR,SOA,TXT,UINFO,WKS,ANY]");
my $usesearchlistparameter = new Plugin::Parameter(-name => "matchtype", -flags => [ "S", "use-searchlist" ],
						   -optional => "yes", -valueoptional => "yes", -type => "NONE",
						   -description => "Use the resolv.conf searchlist to look up the name");
my $appenddomainparameter = new Plugin::Parameter(-name => "appenddomain", -flags => [ 'D', 'append-domain' ],
						  -optional => "yes", -valueoptional => "yes", -type => "NONE",
						  -description => "Append the domain name during lookup");
my $matchtypeparameter = new Plugin::Parameter(-name => "matchtype", -flags => [ 'm', 'match-type' ],
					       -optional => "yes", -valueoptional => "no", -type => "MATCHTYPE", -default => "oneofsuffix",
					       -checker => \&check_known_matchtype,
					       -description => "Type of match to use for answer(s). exact, prefix, suffix, substr, oneof, oneofprefix, onesuffix, oneofsubstr, bindobj(NOT IMPLEMENTED), perlobj(NOT IMPLEMENTED)");
my $answerparameter = new Plugin::Parameter(-name => "answer", -flags => [ 'a', 'answer' ],
					    -optional => "yes", -valueoptional => "no", -type => "STRING",
					    -description => "A string to match with the response from the server");

my @commandparameterlist = ( $hostnameparameter,
			     $serverparameter,
			     $reverseparameter,
			     $timeoutparameter,
			     $querytypeparameter,
			     $usesearchlistparameter,
			     $appenddomainparameter,
			     $matchtypeparameter,
			     $answerparameter,
			     $verboseparameter);

my $plugin = new Plugin(-revision => '$Revision: 1.0 $',
			-copyright => "2004 Howard Wilkinson <howard\@cohtech.com>",
			-shortcomment => "Check functioning of a DNS server",
			-longcomment => "This plugin use the resolver library to obtain Resource Record(s) for the given 'hostname' query. An optional DNS server may be specified. If no DNS server is given the default server(s) specified in resolv.conf will be used. The server response(s) can be check against a given string using exact or fuzzy matching.",
			-parameterlists => [ \@commandparameterlist, $helpparameterlist, $versionparameterlist ]);

my $exitval = $ERRORS{'OK'};
my @querytypes = ( 'A',
		   'AAAA',
		   'AFSDB',
		   'CERT',
		   'CNAME',
		   'DNAME',
		   'EID',
		   'HINFO',
		   'ISDN',
		   'LOC',
		   'MB',
		   'MG',
		   'MR',
		   'MX',
		   'NAPTR',
		   'NIMLOC',
		   'NSAP',
		   'NS',
		   'NULL',
		   'OPT',
		   'PTR',
		   'RP',
		   'RT',
		   'SOA',
		   'SRV',
		   'TKEY',
		   'TXT',
		   #'UINFO',
		   #'WKS',
		   'X25',
		   'ANY'
		 );
my @matchtypes = ( 'exact',               # All answer match exactly
		   'oneof',               # One answer matches exactly
		   'prefix',              # All answers match a prefix
		   'oneofprefix',         # One answer matches a prefix
		   'suffix',              # All answers match a suffix
		   'oneofsuffix',         # One answer matches a suffix
		   'substr',              # All answers match a substring
		   'oneofsubstr',         # One answer matches a substring
		   'bindobj',             # All answers match as a bind object
		   'oneofbindobj',        # One answer matches a bind object
		   'perlobj',             # All answers match a perl object check
		   'oneofperlobj'         # One answer matches a perl object check
		 );

$plugin->init();

# Defaults
my $res = new Net::DNS::Resolver;

if ($opt_s) {
  $res->nameservers($opt_s);
}

my $starttime = time();

$plugin->start_timeout($opt_t, "no response from server - timeout expired");

my $query = ($opt_S)
  ?$res->search($opt_H, $opt_T, "IN")
  :(($opt_D)
    ?$res->query($opt_H, $opt_T, "IN")
    :$res->send($opt_H, $opt_T, "IN"));

$plugin->stop_timeout();

my $responsetime = time() - $starttime;

# Allow fuzzy matching on records - i.e. one of the response can match
# the answer

my @results = ();

if (defined $query) {
  foreach my $record ($query->answer) {
    push @results, $record;
    next;
  }
}

if ((!defined $query || @results == 0) && $responsetime >= $opt_t) {
  print "$PROGNAME CRITICAL: Plugin failed after $responsetime seconds\n";
  exit $ERRORS{'CRITICAL'};
}

my $one_ok = $ERRORS{'CRITICAL'};

foreach my $result (@results) {
  my $r = check_result($result, $opt_m, $opt_a);

  next if ($r == $ERRORS{'UNKNOWN'});
  last if ($r != $ERRORS{'OK'});
  $one_ok = $ERRORS{'OK'};;
}

if ($opt_m =~ /^oneof/ && $one_ok == $ERRORS{'OK'}) { $exitval = $ERRORS{"OK"}; }

if (@results == 0) { $exitval = $ERRORS{'CRITICAL'}; }

print "$PROGNAME ";
foreach my $key (keys %ERRORS) {
  if ($ERRORS{$key} == $exitval) { print $key; last; }
}
print " - $responsetime seconds response time, ";

if ($exitval == $ERRORS{'OK'} || !defined $opt_a) {
  if (@results != 0) {
    print "Response(s) is/are ";
  } else {
    print "No responses";
  }
} else {
  print "Expected $opt_a but got ";
  if (@results == 0) {
    print "no response";
  }
}

my $prefix = "";
foreach my $result (@results) {
  print $prefix . $result->string;
  $prefix = ", ";
}
print "\n";

exit $exitval;

sub check_known_querytype ($$$) {
  my ($opt, $parameter, $plugin) = @_;

  foreach my $querytype (@querytypes) {
    if ($$opt eq $querytype) { return; }
  }

  $plugin->usage();
  usage("$PROGNAME UNKNOWN: Query type $$opt not valid\n");
}

sub check_known_matchtype ($$$) {
  my ($opt, $parameter, $plugin) = @_;

  foreach my $matchtype (@matchtypes) {
    if ($$opt eq $matchtype) { return; }
  }

  $plugin->usage();
  usage("$PROGNAME UNKNOWN: Matchtype $$opt not valid\n");
}

sub check_result ($$$) {
# Check to see if the result matches
# If it does then if the condition is a one only return 'OK'
# If it does and the condition is an exact then return 'UNKNOWN'

# Set exitval to worst match and return current match
  my ($result, $matchtype, $answer) = @_;


  if (!defined $answer) { return $ERRORS{'UNKNOWN'} };

  my $res = $ERRORS{'OK'};
 RESULT: {
    if ($matchtype =~ /bindobj/) {
      # Do an object type check
      $res = $ERRORS{'UNKNOWN'};
      last RESULT;
    }

    if ($matchtype =~ /perlobj/) {
      # Run the answer as a perl subroutine
      $res = $ERRORS{'UNKNOWN'};
      last RESULT;
    }

    my $string = $result->string;

    if ($matchtype eq 'exact'
	|| $matchtype eq 'oneof') {
      if ($string ne $answer) { $res = $ERRORS{'CRITICAL'}; }
      last RESULT;
    }

    if ($matchtype =~ /prefix/) {
      if ($string !~ /^$answer/) { $res = $ERRORS{'CRITICAL'}; }
      last RESULT;
    }

    if ($matchtype =~ /suffix/) {
      if ($string !~ /$answer$/) { $res = $ERRORS{'CRITICAL'}; }
      last RESULT;
    }

    if ($matchtype =~ /substr/) {
      if ($string !~ /$answer/) { $res = $ERRORS{'CRITICAL'}; }
      last RESULT;
    }
  }

  if ($res > $exitval) { $exitval = $res; }

  if ($matchtype =~ /^oneof/) { return $ERRORS{'OK'}; }

  return $res;
}


