#! /usr/bin/perl
# nagios: -epn
# anders@fupp.net
# Alternative HTTP checker for Nagios (tired of check_http)
#
# Aims:
#
# - show full URLs and any relevant error message, and text strings checked for
# - log request and response, error output etc. to retrievable web page and
# send the url along with the # alert, avoiding the need to simulate and
# recreate errors
#
# Warning:
#
# - this code may not be the most advanced, but does the trick for me
# - be careful with multiline searches, script runs slow when combined
# with text searches of variable length (.* etc.). I use a fixed length
# (for example I do exact match for number of spaces), which runs fast
# enough
#
# 2012-11-23: First version.
# 2013-02-04: Add support for SSL, authentication and sending a header.
# Add command arguments to error page.
# 2013-02-05: Change arguments to be somewhat check_http compatible
# 2013-09-12: Actually time out, to give a better timeout message than what
# Nagios gives.
# 2013-10-29: Add SSL SNI support.
# 2014-01-08: Add support for chunked encoding.
# 2015-03-10: Add pre-check support, to check if server is supposed to be
# healthy.

use IO::Socket;
use IO::Socket::SSL;
use Net::SSLeay;
use Getopt::Std;
use Time::HiRes qw/ time /;
use Text::Glob qw( match_glob );
use Digest::MD5 qw(md5);
use MIME::Base64;
use List::Util qw[min max];
use LWP::UserAgent;
use HTTP::Status qw(is_server_error);

# configuration:
# where to put error reports:
my $datadir = "/usr/local/www/h";
# url to serve them on:
my $errorurl = "http://h.xy.no/";
# end, configuration

my $ua = LWP::UserAgent->new;
$ua->timeout(3);

@args = @ARGV;
getopts('I:k:u:c:w:r:s:mdt:p:H:a:SxA:P:');
my @CHARS = ('a'..'z', 'A'..'Z', '0'..'9');
$can_client_sni = Net::SSLeay::OPENSSL_VERSION_NUMBER() >= 0x01000000;
if ($opt_x) {
	$ssl_verify_mode = SSL_VERIFY_PEER;
} else {
	$ssl_verify_mode = SSL_VERIFY_NONE;
}

sub shorten_url {
	my $url= shift;

	my @output;

	foreach my $int (unpack('N4', md5($url))) {

		my $out;
		for (my $j = 0; $j < 6; $j++) {
			my $val = 0x0000003D & $int;
			$out   .= $CHARS[$val];
			$int    = $int >> 5;
		}

		push @output, $out;
	}

	return \@output;
}

sub usage {
	print "Usage: check_http_alt [-I <IP/host>] [-H <virtualhost>] -u <url (path)>\n";
	print "[-c <critical time>] [-w <warning time>] [-r <regexp search>]\n";
	print "[-s <status code>] [-m (multi line)] [-t <timeout>] [-p <port numer>]\n";
	print "[-k <send http header] [-a <user:password>]\n";
	print "[-S (ssl)] [-x (check peer SSL certificate)] [-A (alternative user agent)]\n";
    print "[-P <pre-check URL> (return OK if this does not return 200 OK)]\n";
	exit(1);
}

sub timeout {
    my $ret;
    my $rettxt;
    my $timeout = shift;
    if ($opt_c && ($timeout > $opt_c)) {
            $ret = 2;
            $rettxt = ", beyond critical timeout $opt_c";
    } elsif ($opt_w && ($timeout > $opt_w)) {
            $ret = 1;
            $rettxt = ", beyond warning timeout $opt_w";
    } else {
            $ret = 1;
    }
	doexit($ret,"Could not check $urltxt, general timeout after $timeout seconds$rettxt");
}

if ($opt_u =~ /^https:\/\//) {
	$opt_S = 1;
	$proto = "https";
} elsif ($opt_S) {
	$proto = "https";
} else {
	$proto = "http";
}

if ($opt_u =~ /^(http|https):\/\//) {
	# Remove protocol, assume http/https
	$opt_u =~ s@^${1}:\/\/@@;

	# Find hostname
	$hosttmp = $opt_u;
	$hosttmp =~ s@\/.*@@;
	if ($hosttmp ne "") {
		$opt_H = $hosttmp;
	} else {
		usage;
	}
	$opt_u =~ s@^$opt_H@@;
	if ($opt_u eq "") {
		$opt_u = "/";
	}
}
if ((!$opt_I) && $opt_H) {
	# Specify IP/host either in -I or -H
	$opt_I = $opt_H;
}

usage unless ($opt_u);

if (!$opt_s) {
	$opt_s = "200";
}
if ($opt_t && $opt_t =~ /^\d+$/) {
	$timeout = $opt_t;
} else {
    if ($opt_w =~ /^\d+$/ && $opt_c =~ /^\d+$/) {
        $timeout = max($opt_w, $opt_c)+1;
    } elsif ($opt_w =~ /^\d+$/) {
        $timeout = $opt_w+1;
    } elsif ($opt_c =~ /^\d+$/) {
        $timeout = $opt_c+1;
    } else {
        $timeout = 10;
    }
}

if ($opt_P) {
    my $response = $ua->get($opt_P);
    my $rc = $response->code;
    if ($rc ne "200" && (!is_server_error($rc))) {
        print "Ignoring check of $proto://$opt_H$opt_u on $opt_I, pre-check of $opt_P indicates we should as it does not return 200 OK and is not a server error.\n";
        exit(0);
    }
}

$gentimeout = $timeout + 1;
$SIG{ALRM} = sub { timeout($gentimeout); };
alarm($gentimeout);

if ($opt_H) {
	$hoststr = "Host: $opt_H\r\n";
	$sslhost = $opt_H;
} else {
	$hoststr = "";
	$sslhost = $opt_I;
}
if (!($can_client_sni)) {
	$sslhost = "";
}
if (!$opt_p) {
	if ($opt_S) {
		$opt_p = 443;
	} else {
		$opt_p = 80;
	}
}
if ($opt_d) {
	print "hoststr=$hoststr";
}

if ($opt_A) {
    $useragent = $opt_A;
} else {
    $useragent = "check_http/v1.4.15 (nagios-plugins 1.4.15)";
}

sub doexit {
	my $ret = shift;
	my $txt = shift;

	my $shortref;
	my $short;
	my $shorturl;
	my $end;
	my $timespent;
	$end = time;
	$timespent = $end-$start;
	$txt .= sprintf(". %.3fs, %d bytes.", $timespent, length($data));
	if ($ret != 0) {
		$shortref = shorten_url($opt_I . $opt_H . $opt_u . $start);
		$short = $$shortref[0];
		open(REPORT, ">$datadir/$short");
		print REPORT "Request sent at " . localtime($start) . ":\n";
		print REPORT "I (check_http_alt) was run with arguments: @args\n\n";
		print REPORT $getstr;
		print REPORT "Response headers:\n$headers\n\n";
		print REPORT "Response body:\n$data\n\n";
		print REPORT "Plugin response: $txt";
		close(REPORT);
		$txt .= " $errorurl$short";
	}
	print "$txt\n";
	exit($ret);
}

$urltxt = "URL $proto://$opt_H$opt_u on $opt_I";
$start = time;
if ($opt_S) {
	$sock = new IO::Socket::SSL (
			PeerAddr => $opt_I,
			SSL_hostname => $sslhost,
			SSL_verify_mode => $ssl_verify_mode,
			Proto => 'tcp',
			PeerPort => $opt_p,
			Timeout => $timeout
			);
	doexit(2,"Could not check $urltxt, connection error: $SSL_ERROR $@") unless ($sock);
} else {
	$sock = new IO::Socket::INET (
			PeerAddr => $opt_I,
			Proto => 'tcp',
			PeerPort => $opt_p,
			Timeout => $timeout
			);
	doexit(2,"Could not check $urltxt, connection error: $@") unless ($sock);
}

$getstr = "GET $opt_u HTTP/1.1\r\n" . $hoststr . "User-Agent: $useragent\r\n";
if ($opt_k) {
	$getstr .= "$opt_k\r\n";
}
if ($opt_a) {
	$getstr .= "Authorization: Basic " . encode_base64($opt_a, "") . "\r\n";
}
$getstr .= "Connection: close\r\n\r\n";
print $sock $getstr;
$headers = "";
$data = "";
while (<$sock>) {
	last if (/^\s*$/);
	$headers .= $_;
}

if ($headers=~ /^transfer-encoding:[ \t]*chunked\b/im) {
	while ($chunksize= hex(<$sock>)) {
		$left = $chunksize;
		while ($left && ($got= read($sock, $buf, $left))) {
			$data .= $buf;
			$left -= $got;
		}
		doexit(2,"Could not read chunked response body.") unless defined ($got);
		$_ = <$sock>;	# Clear \r\n
	}
} else {
	while (<$sock>) {
		$data .= $_;
	}
}
close($sock);

$httpcode = (split /\n/, $headers)[0];
$httpcode =~ s@^HTTP/[\d\.]+ (\d+).*@$1@g;

if ($opt_c && $timespent > $opt_c) {
	doexit(2,"Slow $urltxt");
}
if ($opt_w && $timespent > $opt_w) {
	doexit(1,"Slow $urltxt");
}
doexit(3, "Unknown status code $urltxt") if ($httpcode eq "");
doexit(2,"Wrong status code $urltxt, got $httpcode expected $opt_s") if ($httpcode !~ /^$opt_s$/);

if ($opt_r) {
	if ($opt_m) {
		if ($data =~ /$opt_r/s) {
			doexit(0,"Looked for multi $opt_r in $urltxt, fine");
		} else {
			doexit(2,"Looked for $opt_r in $urltxt as multiline text did not find it");
		}
	} else {
		if ($data =~ /$opt_r/) {
			doexit(0,"Looked for $opt_r in $urltxt, fine");
		} else {
			doexit(2,"Looked for $opt_r in $urltxt did not find it");
		}
	}
}

doexit(0,"Checked $urltxt, fine (response code $httpcode)");