New ianardap module, with support for several RDAP servers

This commit is contained in:
Stephane Bortzmeyer 2022-11-09 15:01:28 +00:00
parent 42f67a7e64
commit a117da20b1
2 changed files with 92 additions and 35 deletions

View File

@ -99,7 +99,7 @@ try:
usage()
sys.exit(STATE_OK)
elif option == "--hostname" or option == "-H":
domain = value.lower()
domain = value
elif option == "--timeout" or option == "-t":
timeout = int(value)
elif option == "--unixtime" or option == "-u":
@ -128,17 +128,33 @@ try:
database = ianardap.IanaRDAPDatabase()
except Exception as e:
unknown("Exception when retrieving the IANA database: \"%s\"" % e)
server = database.find(domain)
if server is None:
servers = database.find(domain)
if servers is None:
unknown("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
try:
response = requests.get("%s/domain/%s" % (server, domain), timeout=timeout)
except requests.exceptions.Timeout:
unknown("Timeout when trying to reach %s" % server)
if response.status_code != 200:
error("Invalid RDAP return code: %s" % response.status_code)
# find may return several RDAP servers
found = False
unknowns= ""
errors = ""
for server in servers:
if server.endswith("/"):
server = server[:-1] # Donuts RDAP server balks when there are two slashes and reply 404
try:
response = requests.get("%s/domain/%s" % (server, domain), timeout=timeout)
except requests.exceptions.Timeout:
unknowns += "Timeout when trying to reach %s " % server
if response.status_code != 200:
errors += "Invalid RDAP return code at %s: %s " % \
(server, response.status_code)
else:
found = True
break
if not found:
if errors != "":
error(errors)
elif unknowns != "":
unknown(unknowns)
else:
unknown("No working server found and no error messages")
rdap = json.loads(response.content)
for event in rdap["events"]:
if event["eventAction"] == "expiration":

View File

@ -1,8 +1,7 @@
#!/usr/bin/env python3
"""A simple module to get the RDAP server for a given domain name,
from the IANA database specified in RFC 9224.
"""A simple module to get the RDAP server for a given domain name, IP
prefix or object, from the IANA databases specified in RFC 9224/8521.
"""
# http://python-requests.org/ for easier HTTPS retrieval
@ -17,8 +16,12 @@ import fcntl
import pickle
import pathlib
IANABASE = "https://data.iana.org/rdap/dns.json"
CACHE = os.environ["HOME"] + "/.ianardapcache"
IANABASES = {"domains": "https://data.iana.org/rdap/dns.json",
"v4prefixes": "https://data.iana.org/rdap/ipv4.json",
"v6prefixes": "https://data.iana.org/rdap/ipv6.json",
"as": "https://data.iana.org/rdap/asn.json",
"objects": "https://data.iana.org/rdap/object-tags.json"}
CACHE = os.environ["HOME"] + "/.ianardapcaches"
MAXAGE = 24 # Hours. Used only if the server no longer gives the information.
IANATIMEOUT = 10 # Seconds
MAXTESTS = 3 # Maximum attempts to get the database
@ -46,13 +49,20 @@ def parse_expires(h):
class IanaRDAPDatabase():
def __init__(self, maxage=MAXAGE, cachefile=CACHE, pickleformat=False):
def __init__(self, category="domains", maxage=MAXAGE, cachedir=CACHE,
pickleformat=False):
"""Retrieves the IANA database, if not already cached. maxage is in
hours. The cache file argument should not have an extension (it will
be added automatically). pickleformat is not the default because it is
not really faster *and* it introduces security risks if someone can
write in the file (see the documentation of the module)."""
hours. The cachedir is a directory (it will be created if not already
existant). pickleformat is not the default because it is not really
faster *and* it introduces security risks if someone can write in the
file (see the documentation of the module).
"""
cache_valid = False
if not os.path.exists(cachedir):
os.mkdir(cachedir)
self.category = category
cachefile = os.path.join(cachedir, category)
if pickleformat:
self.cachefile = cachefile + ".pickle"
else:
@ -98,7 +108,7 @@ write in the file (see the documentation of the module)."""
continue
else:
self.unlock()
response = requests.get(IANABASE, timeout=IANATIMEOUT)
response = requests.get(IANABASES[category], timeout=IANATIMEOUT)
expirationtime = None
if "cache-control" in response.headers:
directives = parse_cachecontrol(response.headers["cache-control"])
@ -135,10 +145,27 @@ write in the file (see the documentation of the module)."""
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 self.category == "domains":
for service in database["services"]:
for tld in service[0]:
if tld.lower() not in self.services:
self.services[tld.lower()] =[]
for server in service[1]:
# server is an URL so case-sensitive.
self.services[tld.lower()].append(server)
elif self.category == "objects":
for service in database["services"]:
maintainer = service[0]
for registry in service[1]:
if registry.upper() not in self.services:
self.services[registry.upper()] =[]
for server in service[2]:
self.services[registry.upper()]
for server in service[2]:
self.services[registry.upper()].append(server)
else: # IP addresses will be complicated, because of the
# longest prefix rule.
raise Exception("Unsupported category %s" % self.category)
if not cache_valid:
self.lock()
cache = open(self.cachefile, "wb")
@ -157,16 +184,30 @@ write in the file (see the documentation of the module)."""
fcntl.lockf(self.lockhandle, fcntl.LOCK_UN)
self.lockhandle.close()
def find(self, domain):
""" Get the RDAP server for a given domain name. None if there is none."""
if domain.endswith("."):
domain = domain[:-1]
labels = domain.split(".")
tld = labels[len(labels)-1]
if tld in self.services:
return self.services[tld]
def find(self, id):
"""Get the RDAP server(s), as an array, for a given identifier. None
if there is none. TODO port check_expire on that"""
if self.category == "domains":
domain = id
if domain.endswith("."):
domain = domain[:-1]
labels = domain.lower().split(".")
tld = labels[len(labels)-1]
if tld in self.services:
return self.services[tld]
else:
return None
elif self.category == "objects":
try:
(handle, registry) = id.rsplit("-", maxsplit=1)
except ValueError:
raise Exception("Not a valid RFC 8521 identifier: \"%s\"" % id)
if registry.upper() in self.services:
return self.services[registry.upper()]
else:
return None
else:
return None
raise Exception("Unsupported category %s" % self.category)
if __name__ == "__main__":
rdap = IanaRDAPDatabase(maxage=1)