#!/bin/bash
#
# Author: Roman Drahtmueller draht at schaltsekun.de
#
# checks a certificate across a network for expiration
#
# License: GPLv2
# 

now=$( date "+%s" )
warnbefore=$(( 24 * 28 * 3600 ))
defport=443
ret=0
export c ret defport warnbefore now r

#N=$( basename $0 )
N=check_certificate_validity

function usage() {
echo "usage:
$N [-h|-s|-d <warntime days>] <object> ...
with <object> == (<host:port>|https://<host:port>|-f <file>)
options:
-h              this usage
-d X            warn if the certificate expires within the next X days
                The default is 28 days.
-s              show how many days are left until certificate expiry
-f <certfile>   read the certificate from the file <certfile>

return value:
$N returns the number of errors encountered, 
plus the number of expiration warnings.

If no query object is given on the commandline, $N
will check localhost:443, but will remain silent upon failure. This makes
$N suitable to run from a daily cron job.

examples:
$N -d 14 https://google.com/ 
$N -s mail.local:993
$N https://foo.bar https://google.com/ mail.local:993
"
}

function check_connect() {
# only used if no object was specified on the commandline
# failure to connect aborts without error in this case.
    a=${c/:/\/}
    ( echo > /dev/tcp/$a ) 2> /dev/null
    return $?
}

function get_certificate() {
  lret=0
  sh=${c//:*}
  r=$( echo | openssl s_client -connect "$c" -servername $sh 2>&1 )
  case "$r" in
        *:error:*unknown" "protocol*)
                echo $N: TLS handshake failed: $c > /dev/stderr
                lret=$(( $lret + 1 ))
                return $lret
                break
        ;;
        *"gethostbyname failure"*)
                echo $N: gethostbyname failure: ${c%%:*} > /dev/stderr
                lret=$(( $lret + 1 ))
                return $lret
                break
        ;;
        *:error:*)
                echo $N: error processing object $c > /dev/stderr
                lret=$(( $lret + 1 ))
                return $lret
                break
        ;;
        *"BEGIN CERTIFICATE"*)
                # got it...
                return $lret
        ;;
        *)
                echo $N: unknown error > /dev/stderr
                lret=$(( $lret + 1 ))
                return $lret
        ;;
  esac
}

function check_certificate() {

  r=$( echo "$r" | openssl x509 -noout -dates 2>&1 )
  case "$r" in
        *:error:*)
                echo $N: no certificate was received from $c
                ret=$(( $ret + 1 ))
                shift
                return $ret
                break
        ;;
        *)
        ;;
  esac

  r=$( echo "$r" | awk -F= '/^notAfter=.*/ { print $2; exit; } { next; }' )

  s=$( date -d "$r" "+%s"  )

  timeleft=$(( $s - $now ))
  daysleft=$(( $timeleft / 24 / 3600 ))

  if [ "$show" = yes ]; then
    echo "$daysleft days left until expiry of $c"
  else
    if [ "$timeleft" -lt 0 ]; then
      echo "$c certificate has expired $daysleft days ago"
      ret=$(( $ret + 1 ))
      return $ret
    fi

    if [ "$timeleft" -lt "$warnbefore" ]; then
      echo "$c certificate will expire in $daysleft days"
      ret=$(( $ret + 1 ))
      return $ret
    fi
  fi

}

while [ ! -z "$1" ]; do

# within the case, "continue" means to go to the next cmdline arg.
# break aborts the case ... in, but continues the loop and has a shift 
# on the end.
  case "$1" in
        -h|--help)
          usage
          exit 1
        ;;
        -s)
          show=yes
          shift
          continue
        ;;
        -d)
          shift
          warnbefore=$(( $1 * 24 * 3600 ))
          shift
          # FTR: This is isnum().
          if [[ "$warnbefore" =~ ^[0-9]+$ ]]; then
                if [ $warnbefore -lt 1 ]; then
                    usage
                    exit 1
                fi
          else
                usage
                exit 1
          fi
          continue
        ;;
        https://*)
          c=${1##https://}
          c=${c%%/*}
          port=${c##*:}
          if [[ "$port" =~ ^[0-9]+$ ]]; then
                :
          else
                port=$defport
          fi
          c=${c%%:*}
          c="$c:$port"
          check_connect && get_certificate
        ;;
        -f)
          shift
          c=$1
          if [ ! -f "$1" ]; then
                # this shall not be fatal, there may be more on the cmdline.
                echo "$N: $1: no such file or directory" > /dev/stderr
                ret=$(( $ret + 1 ))
                shift
                continue # main loop
          else
                  r=$( cat "$1" )
                  if [ -z "$r" ]; then
                        echo "$N: error with file $1"
                        ret=$(( $ret + 1 ))
                        shift
                        continue
                fi
          fi
        ;;
        *)
          c="$1"
	  if [ "${c##*:}" = "$c" ]; then # no port given
	    c="${c}:$defport"
	  fi
          get_certificate
        ;;
  esac

  if [ "$r" != "" ]; then
    check_certificate
  fi

  shift
done

if [ -z "$c" ]; then
# end up in here if no target was specified.
  c=localhost:443
  check_connect $c
  if [ $? = 0 ]; then
    get_certificate
    check_certificate
  else
    exit 0
  fi
fi

exit $ret
