#!/usr/bin/perl -w
# Check peer certificate validity
# Require perl module : IO::Socket, Net::SSLeay, Date::Parse
# Require unix programs : openssl, echo, sendmail
#
# Copyright (C) 2003 Emmanuel Lacour <elacour@home-dn.net>
#
# This file is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the
# Free Software Foundation; either version 2, or (at your option) any
# later version.
#
# This file is distributed in the hope that it will be
# useful, but WITHOUT ANY WARRANTY; without even the implied warranty
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this file; see the file COPYING.  If not, write to the Free
# Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
# 02110-1301, USA.
#


# Local variables are prefixed with "l_"

use strict;
use IO::Socket;
use Net::SSLeay;
use Getopt::Long;
use Date::Parse;

Net::SSLeay::SSLeay_add_ssl_algorithms();
Net::SSLeay::randomize();

my $VERSION = '0.6.2';
my $AUTHOR = 'Emmanuel Lacour, <elacour@home-dn.net>';
# Default values
my $opensslpath = "/usr/bin/openssl";
my $sendmailpath = "/usr/lib/sendmail";
my $mailreport = 0;
my $alert = 5;
my $mail = "root";
my $conf = "/etc/sslexpire/sslexpire.conf";

my %hosts;
my $host;
my @ports;
my $port;
my $portlist;
my $rhost;
my $rport;
my $tmp;



# Get options
# output : -m = mail, default = stdout
# host : -h host, -p port, default = from config file
# config : -f configfile, default /etc/sslexpire/sslexpire.conf 
# standard : --help, --version

my %opts;
GetOptions (\%opts,
    'host|h=s',
    'port|p=s',
    'mail|m',
    'conf|c=s',
    'verbose|v',
	'help',
	'version|');

if (($opts{'host'}) && ($opts{'port'})) {
    push @{$hosts{$opts{'host'}}}, $opts{'port'};
}

$mailreport = 1 if ($opts{'mail'});

if ($opts{'conf'}) {
    $conf = $opts{'conf'};
}

if ($opts{'help'}) {
    &usage;
}

if ($opts{'version'}) {
    print "sslexpire $VERSION\n";
    print "Written by $AUTHOR\n";
    exit;
}

# Parse config file


if (-f $conf) {
    open (CONF,$conf);
    
    while (<CONF>) {
        # Skip comments
        next if (/^[ 	]*#/);
        # Alert param.
        if (/^alert[ 	]*=/) { 
            ($tmp,$alert) = split /=/, $_;
        # Mail param.
        } elsif (/^mail[ 	]*=/) { 
            ($tmp,$mail) = split /=/, $_;
        # Use hosts from config file if noone are given by command line
        } elsif ((!$opts{'host'}) && (!$opts{'port'}) && (/:/)) {
            ($tmp,$portlist) = split /:/, $_;
            chomp ($tmp);
            chomp ($portlist);
            # There is multiple ports
            if (/,/) {
                @ports = split /,/, $portlist;
                foreach (@ports) {
                    push @{$hosts{$tmp}}, $_;
                }
            # There is only one port
            } else {
               push @{$hosts{$tmp}},$portlist;
            }
        }
            
    }
    
    close CONF;
}

# Print program usage
sub usage {
    print "Usage: sslexpire [OPTION]...
-h, --host=HOST        check only this host
-p, --port=TCPPORT     check this port on the previous host
-m, --mail             report by mail instead of STDOUT
-c, --conf=FILE        use this config file
    --help             print this help, then exit
";
    exit;
}


# This will return the expiration date
sub getExpire {

    my ($l_host,$l_port) = @_;
    my ($l_expdate,$l_comment);

    # Connect to $l_host:$l_port
    my $socket = IO::Socket::INET->new(
        Proto => "tcp",
        PeerAddr => $l_host,
        PeerPort => $l_port
        );
    # If we connected successfully
    if ($socket) {
        # Intiate ssl
        my $l_ctx = Net::SSLeay::CTX_new();
        my $l_ssl = Net::SSLeay::new($l_ctx);

        Net::SSLeay::set_fd($l_ssl, fileno($socket));
        my $res = Net::SSLeay::connect($l_ssl);

        # Get peer certificate
        my $l_x509 = Net::SSLeay::get_peer_certificate($l_ssl);
        if ($l_x509) {
            my $l_string = Net::SSLeay::PEM_get_string_X509($l_x509);
            # Get the expiration date, using openssl
	    ($l_expdate,$l_comment) = split(/\n/, `echo "$l_string" | $opensslpath x509 -enddate -subject -noout 2>&1`);
            $l_expdate =~ s/.*=//;
            chomp($l_expdate);
        } else {
            $l_expdate = 1;
            $l_comment = 1;
        }

        # Close and cleanup
        Net::SSLeay::free($l_ssl);
        Net::SSLeay::CTX_free($l_ctx);
        close $socket;
    } else {
            $l_expdate = 1;
            $l_comment = 1;
    }
    return ($l_expdate,$l_comment);
}


# Report if needed
# 
#
sub report {
    # Convert date into epoch using date command
    my ($l_expdate,$l_comment,$l_host,$l_port) = @_;
    my $l_subject = "";
    
    if ($l_expdate ne "1") {
        # The current date
        my $l_today =  time;
	my $l_epochdate = str2time($l_expdate);
    
        # Calculate diff between expiration date and today
	my $l_diff = ($l_epochdate - $l_today)/(3600*24);

        # Report if needed
        if ($l_diff < $alert) {
            $l_subject = "Warning ssl certificate on $l_host:$l_port expires in $l_diff days:" if ($l_diff > 1);
            $l_subject = "Warning ssl certificate on $l_host:$l_port expires today:" if (($l_diff > 0) && ($l_diff < 1));
            $l_subject = "Warning ssl certificate on $l_host:$l_port expired:" if ($l_diff <= 0);
	    my $l_mesg = "Expiration date: $l_expdate\n$l_comment\n";
            # Mail report
            if ($mailreport) {
		sendmail($mail, $l_subject, $l_mesg);
            } else {
                print "$l_subject\n";
                print "$l_mesg\n";
            }
        }
    } else {
        $l_subject = "Unable to read certificate on $l_host:$l_port!";
            if ($mailreport) {
	    	sendmail($mail, $l_subject, "");
            } else {
                print "$l_subject\n";
            }
    }
}


# Send mail - sendmail (to,subject,body)
sub sendmail {
    my $to = shift;
    my $subj = shift;
    my $mesg = shift;
    chomp ($to);
    chomp ($subj);
    chomp ($mesg);
    open (MAIL,"| $sendmailpath -t") or die "Couldn't open $sendmailpath";
    print MAIL "To: $to\n";
    print MAIL "Subject: $subj\n";
    print MAIL "\n";
    print MAIL "$mesg\n" if $mesg;
    close MAIL;
    if ((my $status = $?>>8) != 0) {
       die "sendmail: exit status $status\n";
    }
}

# Main
#


# We haven't hosts to check...
if (!%hosts) {&usage;};


# Parse hosts
foreach $host (keys %hosts) {
    # Parse ports for each hosts
    foreach $port (@{$hosts{$host}}) {
        if ($opts{'verbose'}) {
            print "Checking\t$host:$port\n";
        }
    
        # Get expiration date
        my ($expdate,$comment) = &getExpire($host,$port);
    
        # Report
        &report("$expdate","$comment","$host","$port");
    }
}


# script documentation (POD style)

=head1 NAME

sslexpire - Remotely check ssl certificate expiration date.

=head1 DESCRIPTION

This program connect to an host:port to retrieve the expiration date of the ssl
certificate. It gives a report to STDOUT or by email using configuration file.

=head1 COMMAND LINE PARAMETERS

Optional command line parameters are the host and the port to connect. This
allow checking a single host instead of using those given in the configuration
file for periodically checks.

=head1 OPTIONS

=head2 B<-c> I<FILE>, B<--conf>=I<FILE>
    
Specify an alternate config file.

=head2 B<-h> I<HOST>, B<--host>=I<HOST>

Connect to I<HOST> instead of those given in the config file.

=head2 B<-p> I<PORT>, B<--port>=I<PORT>

Specify the port to connect to (used in conjonction with --host).

=head2 B<-v>, B<--verbose>

Prints out verbose messages.

=head2 B<-m>, B<--mail>

Send report by mail instead of STDOUT. It will use the address given in the
config file or root by default.

=head2 B<--help>

Prints out command-line help.

=head2 B<--version>

Prints out version information.

=head1 FILES
	
/etc/sslexpire/sslexpire.conf

=head1 AUTHOR

Emmanuel Lacour, elacour@home-dn.net

=cut
