First version
This commit is contained in:
parent
887e30d86a
commit
ab3feeec4a
29
README.md
29
README.md
@ -1,3 +1,32 @@
|
||||
# check_expire
|
||||
|
||||
A Nagios (and compatible) plugin to check the impending expiration of a domain name. It relies on RDAP exclusively (no whois) and works with every top-level domain with RDAP (which includes all the ICANN ones).
|
||||
|
||||
## Usage
|
||||
|
||||
check_expire follows the usual Nagios rules. The options are:
|
||||
|
||||
* -H: domain name to monitor
|
||||
* -c: critical threshold in days
|
||||
* -w: warning threshold in days
|
||||
* -v: verbose details in output
|
||||
|
||||
## Installation
|
||||
|
||||
You need Python 3 and [Requests](http://python-requests.org/). You can install Requests, for instance, with pip `pip3 install requests`.
|
||||
|
||||
Then, copy the script `check_expire` and the file `ianardap.py`to the directory of local plugins.
|
||||
|
||||
## License
|
||||
|
||||
GPL. See LICENSE.
|
||||
|
||||
## Authors
|
||||
|
||||
Stéphane Bortzmeyer <stephane+chapril@bortzmeyer.org>.
|
||||
|
||||
## Reference site
|
||||
|
||||
https://forge.chapril.org/bortzmeyer/check_expire Use the Gitea issue tracker to report bugs or wishes.
|
||||
|
||||
|
||||
|
150
check_expire
Executable file
150
check_expire
Executable file
@ -0,0 +1,150 @@
|
||||
#!/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)
|
||||
# TODO implement timeout for HTTPS requests. -t option. Does Requests allow it?
|
||||
|
||||
def usage(msg=None):
|
||||
print("Usage: %s -H domain-name [-c critical -w warning]" % sys.argv[0], end="")
|
||||
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:
|
||||
optlist, args = getopt.getopt (sys.argv[1:], "c:hH:vVw:",
|
||||
["critical=", "expiration", "help", "verbose", "version", "warning="])
|
||||
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))
|
||||
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)
|
||||
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")
|
||||
|
||||
|
68
ianardap.py
Normal file
68
ianardap.py
Normal file
@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""A simple module to get the RDAP server for a given domain name,
|
||||
from the IANA database specified in RFC 7484.
|
||||
|
||||
"""
|
||||
|
||||
# http://python-requests.org/ for easier HTTPS retrieval
|
||||
import requests
|
||||
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
IANABASE = "https://data.iana.org/rdap/dns.json"
|
||||
CACHE = os.environ["HOME"] + "/.ianardapcache.json"
|
||||
MAXAGE = 24 # Hours
|
||||
|
||||
class IanaRDAPDatabase():
|
||||
|
||||
def __init__(self, maxage=MAXAGE, cachefile=CACHE):
|
||||
""" Retrieves the IANA databse, if not already cached. maxage is in hours. """
|
||||
cache_valid = False
|
||||
# TODO we should lock it
|
||||
if os.path.exists(cachefile) and \
|
||||
datetime.datetime.fromtimestamp(os.path.getmtime(cachefile)) >= \
|
||||
(datetime.datetime.utcnow() - datetime.timedelta(hours = maxage)):
|
||||
cache = open(cachefile, "rb")
|
||||
content = cache.read()
|
||||
cache.close()
|
||||
cache_valid = True
|
||||
else:
|
||||
response = requests.get(IANABASE)
|
||||
if response.status_code != 200:
|
||||
raise Exception("Invalid HTTPS return code when trying to get %s: %s" % (IANABASE, response.status_code))
|
||||
content = response.content
|
||||
database = json.loads(content)
|
||||
self.description = database["description"]
|
||||
self.publication = database["publication"]
|
||||
self.version = database["version"]
|
||||
self.services = {}
|
||||
for service in database["services"]:
|
||||
for tld in service[0]:
|
||||
for server in service[1]:
|
||||
self.services[tld] = server
|
||||
if not cache_valid:
|
||||
# TODO we should lock it
|
||||
cache = open(cachefile, "wb")
|
||||
cache.write(content)
|
||||
cache.close()
|
||||
|
||||
def find(self, domain):
|
||||
""" Get the RDAP server for a given domain name. None if there is none."""
|
||||
labels = domain.split(".")
|
||||
tld = labels[len(labels)-1]
|
||||
if tld in self.services:
|
||||
return self.services[tld]
|
||||
else:
|
||||
return None
|
||||
|
||||
if __name__ == "__main__":
|
||||
rdap = IanaRDAPDatabase(maxage=1)
|
||||
print("Database \"%s\", version %s published on %s, %i services" % \
|
||||
(rdap.description, rdap.version, rdap.publication, len(rdap.services)))
|
||||
for domain in sys.argv[1:]:
|
||||
print("%s -> %s" % (domain, rdap.find(domain)))
|
||||
|
175
tests.yaml
Normal file
175
tests.yaml
Normal file
@ -0,0 +1,175 @@
|
||||
# Run these tests from test_exe_matrix (reference Web site is
|
||||
# <https://framagit.org/feth/test_exe_matrix>). You can install it
|
||||
# with 'pip3 install test_exe_matrix'.
|
||||
|
||||
---
|
||||
config:
|
||||
timeout: 20
|
||||
|
||||
tests:
|
||||
- exe: './check_expire'
|
||||
args:
|
||||
- '-h'
|
||||
retcode: 0
|
||||
partstdout: '-H domain-name'
|
||||
|
||||
- exe: './check_expire'
|
||||
args:
|
||||
- '--zzz'
|
||||
retcode: 3
|
||||
partstdout: 'option --zzz not recognized'
|
||||
|
||||
- exe: './check_expire'
|
||||
args:
|
||||
- '-c'
|
||||
- 'foobar'
|
||||
- '-H'
|
||||
- 'bortzmeyer.org'
|
||||
retcode: 1
|
||||
partstderr: 'ValueError'
|
||||
|
||||
# 2021-07-05: no RDAP server for this TLD
|
||||
- exe: './check_expire'
|
||||
args:
|
||||
- '-H'
|
||||
- 'bie.re'
|
||||
retcode: 2
|
||||
partstdout: 'No RDAP server'
|
||||
|
||||
# 2021-07-05: no expiration in RDAP for this TLD (but there is one
|
||||
# in whois)
|
||||
- exe: './check_expire'
|
||||
args:
|
||||
- '-H'
|
||||
- 'hopkaup.is'
|
||||
retcode: 2
|
||||
partstdout: 'No expiration found'
|
||||
|
||||
- exe: './check_expire'
|
||||
args:
|
||||
- '-H'
|
||||
- 'bortzmeyer.org'
|
||||
retcode: 0
|
||||
stderr: ''
|
||||
partstdout: 'OK'
|
||||
|
||||
- exe: './check_expire'
|
||||
args:
|
||||
- '-v'
|
||||
- '-H'
|
||||
- 'bortzmeyer.org'
|
||||
retcode: 0
|
||||
stderr: ''
|
||||
partstdout: 'RDAP server is'
|
||||
|
||||
- exe: './check_expire'
|
||||
args:
|
||||
- '-c'
|
||||
- '1000'
|
||||
- '-H'
|
||||
- 'sources.org'
|
||||
retcode: 2
|
||||
stderr: ''
|
||||
partstdout: 'HURRY UP'
|
||||
|
||||
- exe: './check_expire'
|
||||
args:
|
||||
- '-w'
|
||||
- '1000'
|
||||
- '-H'
|
||||
- 'sources.org'
|
||||
retcode: 1
|
||||
stderr: ''
|
||||
partstdout: 'renew now'
|
||||
|
||||
# Now, test various RDAP servers
|
||||
|
||||
# Verisign
|
||||
- exe: './check_expire'
|
||||
args:
|
||||
- '-H'
|
||||
- 'nagios.com'
|
||||
retcode: 0
|
||||
stderr: ''
|
||||
|
||||
# CentralNIC
|
||||
- exe: './check_expire'
|
||||
args:
|
||||
- '-H'
|
||||
- 'botsin.space'
|
||||
retcode: 0
|
||||
stderr: ''
|
||||
|
||||
# Fred
|
||||
- exe: './check_expire'
|
||||
args:
|
||||
- '-H'
|
||||
- 'turris.cz'
|
||||
retcode: 0
|
||||
stderr: ''
|
||||
|
||||
# Afnic
|
||||
- exe: './check_expire'
|
||||
args:
|
||||
- '-H'
|
||||
- 'brest.bzh'
|
||||
retcode: 0
|
||||
stderr: ''
|
||||
|
||||
# Donuts
|
||||
- exe: './check_expire'
|
||||
args:
|
||||
- '-H'
|
||||
- 'mastodon.social'
|
||||
retcode: 0
|
||||
stderr: ''
|
||||
|
||||
# Iran, expiration date in the past
|
||||
- exe: './check_expire'
|
||||
args:
|
||||
- '-H'
|
||||
- 'nic.pars'
|
||||
retcode: 2
|
||||
partstdout: "already expired"
|
||||
stderr: ''
|
||||
|
||||
# Brazil
|
||||
- exe: './check_expire'
|
||||
args:
|
||||
- '-H'
|
||||
- 'nic.globo'
|
||||
retcode: 0
|
||||
stderr: ''
|
||||
|
||||
# Fury (Canada)
|
||||
- exe: './check_expire'
|
||||
args:
|
||||
- '-H'
|
||||
- 'blackbox.kiwi'
|
||||
retcode: 0
|
||||
stderr: ''
|
||||
|
||||
# JPRS
|
||||
- exe: './check_expire'
|
||||
args:
|
||||
- '-H'
|
||||
- 'nic.jprs'
|
||||
retcode: 0
|
||||
stderr: ''
|
||||
|
||||
# Nominet
|
||||
- exe: './check_expire'
|
||||
args:
|
||||
- '-H'
|
||||
- 'nic.abogado'
|
||||
retcode: 0
|
||||
stderr: ''
|
||||
|
||||
# .at
|
||||
- exe: './check_expire'
|
||||
args:
|
||||
- '-H'
|
||||
- 'nic.wien'
|
||||
retcode: 0
|
||||
stderr: ''
|
||||
|
Loading…
Reference in New Issue
Block a user