2021-07-05 20:49:49 +02:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
|
|
|
"""Monitoring plugin (Nagios-compatible) to check the impeding
|
|
|
|
expiration of a domain name, through RDAP.
|
|
|
|
|
|
|
|
The monitoring plugin API is documented at
|
|
|
|
<https://www.monitoring-plugins.org/doc/guidelines.html>.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
import sys
|
|
|
|
import json
|
|
|
|
import datetime
|
|
|
|
import getopt
|
|
|
|
import re
|
|
|
|
|
|
|
|
# http://python-requests.org/ for easier HTTPS retrieval
|
|
|
|
import requests
|
|
|
|
|
|
|
|
# Local module
|
|
|
|
import ianardap
|
|
|
|
|
|
|
|
VERSION = "0.0"
|
|
|
|
|
|
|
|
# Funny diversity in RDAP servers' output...
|
|
|
|
RFC3339 = "%Y-%m-%dT%H:%M:%SZ%z"
|
|
|
|
RFC3339TZ = "%Y-%m-%dT%H:%M:%S%z" # We cannot use it directly, Python uses HHMM as offset, not HH:MM as RFC 3339 does :-(
|
|
|
|
RFC3339LONG = "%Y-%m-%dT%H:%M:%S.%fZ%z" # Microseconds support (%f) does not seem documented
|
|
|
|
|
|
|
|
UTCINFO = datetime.timezone(offset=datetime.timedelta(seconds=0))
|
|
|
|
|
|
|
|
# Do not touch
|
|
|
|
# https://www.monitoring-plugins.org/doc/guidelines.html#AEN78
|
|
|
|
STATE_OK = 0
|
|
|
|
STATE_WARNING = 1
|
|
|
|
STATE_CRITICAL = 2
|
|
|
|
STATE_UNKNOWN = 3
|
|
|
|
STATE_DEPENDENT = 4
|
|
|
|
|
|
|
|
# Can be changed on the command-line
|
|
|
|
verbose = False
|
|
|
|
domain = None
|
|
|
|
critical_t = datetime.timedelta(days=2)
|
|
|
|
warning_t = datetime.timedelta(days=7)
|
2021-07-06 12:22:38 +02:00
|
|
|
unixtime = False
|
2021-07-05 20:49:49 +02:00
|
|
|
# TODO implement timeout for HTTPS requests. -t option. Does Requests allow it?
|
|
|
|
|
|
|
|
def usage(msg=None):
|
2021-07-07 11:04:04 +02:00
|
|
|
print("Usage: %s -H domain-name [-c critical -w warning -u]" % sys.argv[0], end="")
|
2021-07-05 20:49:49 +02:00
|
|
|
if msg is not None and msg != "":
|
|
|
|
print(" (%s)" % msg)
|
|
|
|
else:
|
|
|
|
print("")
|
|
|
|
|
|
|
|
def details():
|
|
|
|
if verbose:
|
|
|
|
print(" RDAP database \"%s\", version %s published on %s, RDAP server is %s" % \
|
|
|
|
(database.description, database.version, database.publication, server))
|
|
|
|
else:
|
|
|
|
print("")
|
|
|
|
|
|
|
|
def error(msg=None):
|
|
|
|
if msg is None:
|
|
|
|
msg = "Unknown error"
|
|
|
|
print("%s CRITICAL: %s" % (domain, msg), end="")
|
|
|
|
details()
|
|
|
|
sys.exit(STATE_CRITICAL)
|
|
|
|
|
|
|
|
def warning(msg=None):
|
|
|
|
if msg is None:
|
|
|
|
msg = "Unknown warning"
|
|
|
|
print("%s WARNING: %s" % (domain, msg), end="")
|
|
|
|
details()
|
|
|
|
sys.exit(STATE_WARNING)
|
|
|
|
|
|
|
|
def ok(msg=None):
|
|
|
|
if msg is None:
|
|
|
|
msg = "Unknown message but everything is OK"
|
|
|
|
print("%s OK: %s" % (domain, msg), end="")
|
|
|
|
details()
|
|
|
|
sys.exit(STATE_OK)
|
|
|
|
|
|
|
|
try:
|
2021-07-07 11:04:04 +02:00
|
|
|
optlist, args = getopt.getopt (sys.argv[1:], "c:hH:uvVw:",
|
|
|
|
["critical=", "expiration", "help", "unixtime", "verbose", "version", "warning="])
|
2021-07-05 20:49:49 +02:00
|
|
|
for option, value in optlist:
|
|
|
|
if option == "--critical" or option == "-c":
|
|
|
|
critical_t = datetime.timedelta(days=int(value))
|
|
|
|
elif option == "--help" or option == "-h":
|
|
|
|
usage()
|
|
|
|
sys.exit(STATE_OK)
|
|
|
|
elif option == "--hostname" or option == "-H":
|
|
|
|
domain = value
|
|
|
|
elif option == "--verbose" or option == "-v":
|
|
|
|
verbose = True
|
|
|
|
elif option == "--version" or option == "-V":
|
|
|
|
print("%s version %s" % (sys.argv[0], VERSION))
|
|
|
|
sys.exit(STATE_OK)
|
|
|
|
elif option == "--warning" or option == "-w":
|
|
|
|
warning_t = datetime.timedelta(days=int(value))
|
2021-07-06 12:22:38 +02:00
|
|
|
elif option == "--unixtime" or option == "-u":
|
|
|
|
unixtime = True
|
2021-07-05 20:49:49 +02:00
|
|
|
else:
|
|
|
|
# Should never occur, it is trapped by getopt
|
|
|
|
print("Unknown option %s" % option)
|
|
|
|
sys.exit(STATE_UNKNOWN)
|
|
|
|
except getopt.error as reason:
|
|
|
|
usage(reason)
|
|
|
|
sys.exit(STATE_UNKNOWN)
|
|
|
|
if len(args) != 0:
|
|
|
|
usage("Extraneous arguments")
|
|
|
|
sys.exit(STATE_UNKNOWN)
|
|
|
|
if domain is None:
|
|
|
|
usage("-H is mandatory")
|
|
|
|
sys.exit(STATE_UNKNOWN)
|
|
|
|
database = ianardap.IanaRDAPDatabase()
|
|
|
|
server = database.find(domain)
|
|
|
|
if server is None:
|
|
|
|
error("No RDAP server found for %s" % domain)
|
|
|
|
if server.endswith("/"):
|
|
|
|
server = server[:-1] # Donuts RDAP server balks when there are two slashes and reply 404
|
|
|
|
response = requests.get("%s/domain/%s" % (server, domain))
|
|
|
|
if response.status_code != 200:
|
|
|
|
error("Invalid RDAP return code: %s" % response.status_code)
|
|
|
|
rdap = json.loads(response.content)
|
|
|
|
for event in rdap["events"]:
|
|
|
|
if event["eventAction"] == "expiration":
|
|
|
|
try:
|
|
|
|
expiration = datetime.datetime.strptime(event["eventDate"] + "+0000", RFC3339LONG)
|
|
|
|
except ValueError:
|
|
|
|
try:
|
|
|
|
expiration = datetime.datetime.strptime(event["eventDate"] + "+0000", RFC3339)
|
|
|
|
except ValueError:
|
|
|
|
# Python standard library offers no way to parse all RFC3339-compliant datetimes, so we have to hack.
|
|
|
|
if re.search(r"\+[0-9]{2}:[0-9]{2}$", event["eventDate"]):
|
|
|
|
dt = re.sub(r"\+([0-9]{2}):([0-9]{2})$", r"+\1\2", event["eventDate"])
|
|
|
|
expiration = datetime.datetime.strptime(dt, RFC3339TZ)
|
|
|
|
else:
|
|
|
|
print("No recognized format for datetime \"%s\"" % event["eventDate"])
|
|
|
|
sys.exit(STATE_UNKNOWN)
|
|
|
|
now = datetime.datetime.now(tz=UTCINFO)
|
2021-07-06 12:22:38 +02:00
|
|
|
if unixtime == True:
|
|
|
|
print(int(expiration.strftime("%s")))
|
|
|
|
sys.exit(STATE_OK)
|
2021-07-05 20:49:49 +02:00
|
|
|
if expiration < now:
|
|
|
|
error("domain %s is already expired (%s ago)" % (domain, (now-expiration)))
|
|
|
|
rest = expiration-now
|
|
|
|
if rest < critical_t:
|
|
|
|
error("expires in %s, HURRY UP!!!" % (rest))
|
|
|
|
elif rest < warning_t:
|
|
|
|
warning("expires in %s, renew now" % (rest))
|
|
|
|
else:
|
|
|
|
ok("expires in %s." % (rest))
|
|
|
|
error("No expiration found")
|