#!/usr/bin/perl # # $Id$ # use strict; use warnings; use Getopt::Long; use LWP; use XML::Parser; use Crypt::OpenSSL::X509; use Date::Parse; use POSIX qw(mktime); use constant { OK => 0, WARNING => 1, CRITICAL => 2, UNKNOWN => 3, DEPENDENT => 4, }; use constant { SHIBBOLETH => 'shibboleth', XERCES => 'xerces', OPENSAML => 'opensaml', XML_TOOLING => 'xml-tooling', XML_SECURITY => 'xml-security', }; my $CODE2STRING = { 0 => 'OK', 1 => 'WARNING', 2 => 'CRITICAL', 3 => 'UNKNOWN', 4 => 'DEPENDENT', }; sub get_recursive { my $ctx_ref = shift; my $path_ref = shift; my $results_ref = shift; my $no_fail_bail = shift || 0; # fixup root array if (ref($ctx_ref) eq 'ARRAY') { if (scalar(@{$ctx_ref}) != 1) { die('malformed data, root array must contain one element'); } $ctx_ref = { 'Kids' => $ctx_ref, }; } my $current = shift(@{$path_ref}); if ($current =~ m/^@/) { my $target = substr($current, 1); my $value = $ctx_ref->{$target}; if (defined($value)) { push(@{$results_ref}, $value); return 1; } else { return 0; } } else { my $target = "main::" . $current; my $results = 0; foreach my $i (@{$ctx_ref->{'Kids'}}) { if (ref($i) eq $target) { if (scalar(@{$path_ref}) == 0) { push(@{$results_ref}, $i); $results += 1; } else { $results += get_recursive($i, $path_ref, $results_ref, $no_fail_bail); } } } return $results; } } sub get { my $ctx_ref = shift; my $path = shift; my $no_fail_bail = shift || 0; my @p = split('/', $path); my @results = (); if (get_recursive($ctx_ref, \@p, \@results, $no_fail_bail) > 0) { return $results[0]; } else { if ($no_fail_bail) { return undef; } else { die("$path: not found"); } } } sub get_all { my $ctx_ref = shift; my $path = shift; my $no_fail_bail = shift || 0; my @p = split('/', $path); my @results = (); if (get_recursive($ctx_ref, \@p, \@results, $no_fail_bail) <= 0) { if (!$no_fail_bail) { die("$path: not found"); } } return @results; } sub get_diff { my $ts = shift; my ($sec1, $min1, $hour1, $day1, $mon1, $year1) = strptime($ts); my ($sec2, $min2, $hour2, $day2, $mon2, $year2) = gmtime(time()); my $t1 = mktime($sec1, $min1, $hour1, $day1, $mon1, $year1); my $t2 = mktime($sec2, $min2, $hour2, $day2, $mon2, $year2); return $t1 - $t2; } sub status { my $code = shift; my $message = shift; $message =~ s/\n*$//; printf("%s: %s\n", $CODE2STRING->{$code}, $message); exit $code; } my $host = undef; my $url = 'Shibboleth.sso/Status'; my $use_ssl = 0; my $max_clock_diff = 120; my $cert_warn_days = 31; my $timeout = 10; my $verbose = 0; my $result = GetOptions("H=s" => \$host, "u=s" => \$url, "t=i" => \$timeout, "S" => \$use_ssl, "D=i" => \$max_clock_diff, "C=i" => \$cert_warn_days, 'v' => \$verbose); if (!($result)) { status(UNKNOWN, 'USAGE: -H [-u ] [-t ] [-S] [-D -C '); } if (!defined($host)) { status(UNKNOWN, 'missing manadorty parameter -H '); } local $SIG{ALRM} = sub { status(UNKNOWN, 'check timed out'); }; alarm $timeout; $url =~ s|^/+||g; $url =~ s|/+$||g; my $sp_status_url = sprintf('%s://%s/%s', ($use_ssl ? 'https' : 'http'), $host, $url); print STDERR "SP URL: $sp_status_url\n" if ($verbose > 0); my $browser = LWP::UserAgent->new; my $response = $browser->get($sp_status_url); if ($response->is_success) { my $result = undef; eval { my $parser = XML::Parser->new(Style => 'Objects'); $result = $parser->parse($response->content); }; if ($@) { my $msg = $@; $msg =~ s/^\n*(.*)\s+at.*?\n*$/$1/gs; status(CRITICAL, "error parsing XML response: " . $msg); } my $time = get($result, 'StatusHandler/@time'); my $diff = get_diff($time); if (abs($diff) > $max_clock_diff) { status(CRITICAL, sprintf('clock skew too large (IDP time: %s, skew %d)', $time, $diff)); } my $versions = { SHIBBOLETH => get($result, 'StatusHandler/Version/@Shibboleth', 0), XERCES => get($result, 'StatusHandler/Version/@Xerces-C', 0), OPENSAML => get($result, 'StatusHandler/Version/@OpenSAML-C'), XML_TOOLING => get($result, 'StatusHandler/Version/@XML-Tooling-C'), XML_SECURITY => get($result, 'StatusHandler/Version/@XML-Security-C'), }; # check certificates my $code = OK; my $message = undef; foreach my $key (get_all($result, 'StatusHandler/md:KeyDescriptor', 1)) { my $cert_data = get($key, 'ds:KeyInfo/ds:X509Data/ds:X509Certificate/Characters')->{'Text'}; my $cert = undef; eval { $cert_data =~ s/\n*//gs; $cert_data =~ s/(.{64})/$1\n/gs; $cert = Crypt::OpenSSL::X509->new_from_string( sprintf("-----BEGIN CERTIFICATE-----\n%s\n" . "-----END CERTIFICATE-----\n", $cert_data)); }; if ($@) { print $@; status(UNKNOWN, "cannot parse certificate data"); } if ($cert->checkend(0)) { status(CRITICAL, sprintf('%s certificate has expired since %s', $key->{'use'}, $cert->notAfter())); } elsif ($cert->checkend($cert_warn_days * 86400)) { my $expire_days = int(get_diff($cert->notAfter()) / 86400); $code = WARNING; $message = sprintf('%s certificate will expire in %d day(s)', $key->{'use'}, $expire_days); } } if ($code != OK) { status($code, $message); } else { my $status = get($result, 'StatusHandler/Status'); if (defined(get($status, 'OK', 1))) { status(OK, sprintf('Shibboleth SP version %s', $versions->{SHIBBOLETH})); } else { status(CRITICAL, 'status is not OK; no further information'); } } } else { my $status = $response->status_line || 'unknown'; status(CRITICAL, sprintf('error fetching status: %s', $status)); } exit OK;