Package: whoisit
whoisitA Python client to RDAP WHOIS-like services for internet resources (IPs, ASNs, domains,
etc.).whoisitis a simple library that makes requests to the "new" RDAP (Registration
Data Access Protocol) query services for internet resource information. These services
started to appear in 2017 and have become more widespread since 2020.whoisitis designed to abstract over RDAP. While RDAP is a basic HTTP and JSON based
protocol which can be implemented in a single line of Python withrequeststhe
bootstrapping (which RDAP service to query for what item) and extracting useful
information from the RDAP responses is extensive enough that a library like this is
useful.Installationwhoisitis pure Python and only has a dependancy on therequestsanddateutillibraries. You can installwhoisitvia pip:$pipinstallwhoisitAny modern version of Python3 will be compatible.Usagewhoisitsupports the 4 main types of lookups supported by RDAP services. These are:ASNs (autonomous systems numbers) known asautnumobjectsDNS registrations known asdomainobjects - only some TLDs are supportedIPv4 and IPv6 addresses and CIDRs / prefixes known asipobjectsEntities (People, organisations etc. by ENTITY-HANDLES) known asentityobjectswhoisitreturns parsed RDAP formatted JSON as (mostly) flat dictionaries by default.Basic examples:importwhoisitwhoisit.bootstrap()results=whoisit.asn(1234)print(results['name'])results=whoisit.domain('example.com')print(results['nameservers'])results=whoisit.ip('1.2.3.4')print(results['name'])results=whoisit.ip('1.2.3.0/24')print(results['name'])results=whoisit.ip('2404:1234:1234:1234:1234:1234:1234:1234')print(results['name'])results=whoisit.ip('2404:1234::/32')print(results['name'])results=whoisit.entity('ARIN-CHA-1')print(results['last_changed_date'])Raw response dataIn each caseresultswill be a dictionary containing the most useful information for
each request type. If the data you want is not in the response you can request the raw,
unparsed and large RDAP JSON data by adding theraw=Trueargument to the request, for
example:results=whoisit.domain('example.com',raw=True)# 'results' is now the full, raw response from the RDAP serviceIf for some reason you accidentally end up querying the wrong RDAP endpoint your query
should end up still working, for example if you query ARIN for information on the IP
address1.1.1.1it will redirect you to APNIC (where1.1.1.1is allocated)
automatically.Some resources, most notably entity handles, do not redirect or have assigned obvious
namespaces linked to particular registries. For these querieswhoisitwill attempt to
guess the RDAP service to query by examining the name for prefixes or postfix, such as
many RIPE entities are namedRIPE-SOMETHING. If your entity does not have an obvious
prefix or postfix likeARIN-*or*-APyou will need to tellwhoisitwhich registry
to make the request to by specifying therir=nameargument. Therirargument stands
for "Regional Internet Registry". For example:# This will work OK because the entity is prefixed with an obvious RIR nameresults=whoisit.entity('RIPE-NCC-MNT')# This will cause a QueryError to be raised because ARIN returns a 404 for RIPE-NCC-MNTresults=whoisit.entity('RIPE-NCC-MNT',rir='arin')# This will cause a UnsupportedError to be raised because we have no way to detect# which RDAP service to query as the entity has no RIR prefix or postfixresults=whoisit.entity('AS5089-MNT')# This will work OK because the entity is registered at RIPEresults=whoisit.entity('AS5089-MNT',rir='ripe')Weaken SSL ciphersSome RDAP servers do not have particularly secure SSL implementations. As RDAP returns
read-only and public information it may be acceptable for you to want to downgrade the
security of yourwhoisitrequests to successfully return data.You can use theallow_insecure_ssl=Trueargument to your queries to enable this.For example (as of 2021-07-25):# This will result in an SSL errorresults=whoisit.domain('nic.work')# ... SSLError(SSLError(1, '[SSL: DH_KEY_TOO_SMALL] dh key too small (_ssl.c:1129)')))# This will workresults=whoisit.domain('nic.work',allow_insecure_ssl=True)Note that withallow_insecure_ssl=Truethe upstream RDAP server certificate is
still validated, it just permits weaker SSL ciphers during the handshake. You should
only useallow_insecure_ssl=Trueif your request fails with an SSL cipher or
handshake error first.Bootstrappingwhoisitneeds to know which RDAP service to query for a resource. This information is
provided by the IANA as bootstrapping information. Bootstrapping data simply says things
like "this CIDR is allocated to ARIN, this CIDR is allocated to RIPE" and so on for all
resources. The bootstrap data means you should be directly querying the correct RDAP
server for your request at all times. You should cache the bootstrap information locally
if you plan to make more than a single request otherwise you'll make additional requests
to the IANA every time you run a query. Example bootstrap information caching:importwhoisitprint(whoisit.is_bootstrapped())# -> Falsewhoisit.bootstrap()# Slow, makes several HTTP requests to the IANAprint(whoisit.is_bootstrapped())# -> True# bootstrap_info returned here is a string of JSON serialised bootstap information# You can store it in a memory cache or write it to disk for a few daysbootstrap_info=whoisit.save_bootstrap_data()# Clear bootstrapping datawhoisit.clear_bootstrapping()# Later, you can doprint(whoisit.is_bootstrapped())# -> Falseifnotwhoisit.is_bootstrapped():whoisit.load_bootstrap_data(bootstrap_info)# Fast, no HTTP requests madeprint(whoisit.is_bootstrapped())# -> True# For convenience internally whoisit stores a timestamp of when the bootstrap data was# last updated and has a "is older than" helper methodifwhoisit.bootstrap_is_older_than(days=3):# Bootstrap data was last updated over 3 days ago, refresh itwhoisit.clear_bootstrapping()whoisit.bootstrap()bootstrap_info=whoisit.save_bootstrap_data()# and save it to upload your cacheA reasonable suggested way to handle bootstrapping data would be to use Memcached or
Redis, for example:importwhoisitimportredisr=redis.Redis(host='localhost',port=6379,db=0)bootstrap_info=r.get('whoisit_bootstrap_info')ifbootstrap_info:whoisit.load_bootstrap_data(bootstrap_info)else:whoisit.bootstrap()bootstrap_info=whoisit.save_bootstrap_data()expire_in_3_days=60*60*24*3r.set('whoisit_bootstrap_info',bootstrap_info,ex=expire_in_3_days)# Send queries as normal once bootstrappedwhoisit.asn(12345)Some services, most notably TLDs, do have RDAP servers which may not be set properly
in the IANA bootstrap data.whoisitmaintains a record of these and can patch the
IANA data to allow more TLDs to be queried. You can enable this with theoverrides=Trueparameter when loading bootstrap data:whoisit.bootstrap(overrides=True)orwhoisit.load_bootstrap_data(bootstrap_info,overrides=True)Important: when using the overrides you may recieve non-standard data, data that
is not in the same format as officially listed IANA data and you may not recieve a copy
of any required terms of service or terms of use. You will have to manually verify data
returned by overridden endpoints.Insecure (HTTP) RDAP endpointsSome RDAP servers are only available over HTTP and not HTTPS. This is disabled by
default. When you bootstrapwhoisitadebugnotice will be emitted for any RDAP
endpoint that is not loaded because it is insecure. For example:# Enable debug loggingimportosos.environ['DEBUG']='true'# Load and boostrap whoisitimportwhoisit# > [datetime] bootstrap [DEBUG] Cleared bootstrap datawhoisit.bootstrap()# > ... debug logs ...# > [datetime] bootstrap [DEBUG] No valid RDAP service URLs could be parsed#              from: ['http://cctld.uz:9000/'] (insecure scheme,#              try whoisit.bootstrap(allow_insecure=True))# > ... debug logs ...# > [datetime] bootstrap [DEBUG] BootstrappedThis line informs you that an RDAP endpoint has been skipped because it is only
available over HTTP. You can opt-in to allow insecure endpoints by calling the
bootstrap methodsbootstrap()andload_bootstrap_data()with the optionalallow_insecure=Trueargument. For example:# Bootstrap with allowing insecure endpointswhoisit.bootstrap(allow_insecure=True)or# Load saved bootstrap data with allowing insecure endpointswhoisit.load_bootstrap_data(bootstrap_info,allow_insecure=True)Response dataBy defaultwhoisitreturns parsed, summary useful information. This information issimplified. This means that some information is lost from the raw, original data. For
example,whoisitdoesn't return the date that nameservers were last updated. If you
need more information thanwhoisitreturns by default remember to addraw=Trueto
your query and parse the RDAP response yourself.Data fromwhoisitis returned, where possible, as rich data types such asdatetime,IPv4NetworkandIPv6Networkobjects.The following values are returned for every successful response:response={'handle':str,# Entity handle for the object, always set'parent_handle':str,# Parent entity handle for the object'name':str,# Name of the object'whois_server':str,# WHOIS server hostname object data can be found on'type':str,# Object type, such as autnum or domain'terms_of_service_url':str,# URL to the terms of service for using the object data'copyright_notice',str,# Copyright notice for the object data'description':list,# List of text lines that describe the object'last_changed_date':datetimeorNone,# Date and time the object was last updated'registration_date':datetimeorNone,# Date and time the object was registered'expiration_date':datetimeorNone,# Date and time the object expires'rir':str,# Short name of the RIR for the object, such as 'arin''url':str,# URL to the RDAP query which was made for this request'entities':dict,# A dict of entities linked to the object}The entities dictionary has the following format, note there may be multiple entities
for each role:response['entities']['some_role'][]={# Role names are strings, like 'registrant''email':str,# Email address of the entity'handle':str,# Handle of the entity'name':str,# Name of the entity'rir':str,# Short name of the RIR where the entity is registered'type':str,# Type of the entity, usually 'entity''url':str,# URL to an RDAP service to query this entity'whois_server':str,# WHOIS server hostname entity data can be found on}In addition to the default data for all responses listed above requests have additional
extra fields in their responses, these are:Additional ASN response data# ASN response data includes all shared general response fields above and also:response={'asn_range':list,# A list of the start and end range for an AS allocation# For example, [123,134] or [123,123]}Additional domain response data# Domain response data includes all shared general response fields above and also:response={'nameservers':list,# List of name servers for the domain as strings'status':list,# List of the domain states as strings}Additional IP response data# IP response data includes all shared general response fields above and also:response={'country':str,# Two letter country code for the IP block'ip_version':int,# 4 or 6 to denote the IP version'assignment_type':str,# Assignment type, such as 'assigned portable''network':IPvXNetwork,# A IPv4Network or IPv6Network object for the prefix}Additional entity response data# Entity response data includes all shared general response fields above and also:response={'email':str,# If the entity as a root vcard the email address}Full response exampleA full example response for an IP query for the IPv4 address1.1.1.1:importwhoisitwhoisit.bootstrap()response=whoisit.ip('1.1.1.1')print(response){'handle':'1.1.1.0 - 1.1.1.255','parent_handle':'','name':'APNIC-LABS','whois_server':'whois.apnic.net','type':'ip network','terms_of_service_url':'http://www.apnic.net/db/dbcopyright.html','copyright_notice':'','description':['APNIC and Cloudflare DNS Resolver project','Routed globally by AS13335/Cloudflare','Research prefix for APNIC Labs'],'last_changed_date':datetime.datetime(2020,7,15,13,10,57,tzinfo=tzutc()),'registration_date':None,'expiration_date':None,'url':'https://rdap.apnic.net/ip/1.1.1.0/24','rir':'apnic','entities':{'abuse':[{'handle':'IRT-APNICRANDNET-AU','url':'https://rdap.apnic.net/entity/IRT-APNICRANDNET-AU','type':'entity','name':'IRT-APNICRANDNET-AU','email':'helpdesk@apnic.net','rir':'apnic'}],'administrative':[{'handle':'AR302-AP','url':'https://rdap.apnic.net/entity/AR302-AP','type':'entity','name':'APNIC RESEARCH','email':'research@apnic.net','rir':'apnic'}],'technical':[{'handle':'AR302-AP','url':'https://rdap.apnic.net/entity/AR302-AP','type':'entity','name':'APNIC RESEARCH','email':'research@apnic.net','rir':'apnic']},'country':'AU','ip_version':4,'assignment_type':'assigned portable','network':IPv4Network('1.1.1.0/24')}Full API synopsiswhoisit.is_bootstrapped()->boolReturns boolean True or False if yourwhoisitinstance is bootstrapped or not.whoisit.bootstrap(overrides=bool, allow_insecure=bool)->boolBootstraps yourwhoisitinstance with remote IANA bootstrap information. Returns
True or raises awhoisit.errors.BootstrapErrorexception if it fails. This method
makes HTTP requests to the IANA.whoisit.clear_bootstrapping()->boolClears any stored bootstrap information. Always returns boolean True.whoisit.save_bootstrap_data()->strReturns a string of JSON serialised bootstrap information if any is loaded. If no
bootstrap information loaded awhoisit.errors.BootstrapErrorwill be raised.whoisit.load_bootstrap_data(data=str, overrides=bool, allow_insecure=bool)->boolLoads a string of JSON serialised bootstrap data as returned bysave_bootstrap_data().
Returns True if the data is loaded or raises awhoisit.errors.BootstrapErrorif
loading fails.whoisit.bootstrap_is_older_than(days=int)->boolTests if the loaded bootstrap data is older than the specified number of days as an
integer. Returns True or False. If no bootstrap information is loaded awhoisit.errors.BootstrapErrorexception will be raised.whoisit.asn(asn=int, rir=str, raw=bool, allow_insecure_ssl=bool)->dictQueries a remote RDAP server for information about the specified AS number. AS number
must be an integer. Returns a dict of information. Ifraw=Trueis passed a large dict
of the raw RDAP response will be returned. If the query fails awhoisit.errors.QueryErrorexception will be raised. If no bootstrap data is loaded
awhoisit.errors.BootstrapErrorexception will be raised. ifallow_insecure_ssl=Trueis passed the RDAP queries will allow weaker SSL handshakes. Examples:whoisit.asn(12345)whoisit.asn(12345,rir='arin')whoisit.asn(12345,raw=True)whoisit.asn(12345,rir='arin',raw=True)whoisit.asn(12345,allow_insecure_ssl=True)whoisit.domain(domain=str, raw=bool, allow_insecure_ssl=bool)->dictQueries a remote RDAP server for information about the specified domain name. The domain
name must be a string and in a valid domain name "something.tld" style format. Returns a
dict of information. Ifraw=Trueis passed a large dict of the raw RDAP response will
be returned. If the query fails awhoisit.errors.QueryErrorexception will be raised.
If no bootstrap data is loaded awhoisit.errors.BootstrapErrorexception will be
raised. If the TLD is unsupported awhoisit.errors.UnsupportedErrorexception will be
raised.  ifallow_insecure_ssl=Trueis passed the RDAP queries will allow weaker SSL
handshakes.Note that not all TLDs are supported, only some have RDAP services!Examples:whoisit.domain('example.com')whoisit.domain('example.com',raw=True)whoisit.domain('example.com',allow_insecure_ssl=True)whoisit.ip(ip="1.1.1.1", rir=str, raw=bool, allow_insecure_ssl=bool)->dictQueries a remote RDAP server for information about the specified IP address or CIDR. The
IP address or CIDR must be a string and in the correct IP address or CIDR format or
any one of IPv4Address, IPv4Network, IPv6Address or IPv6Network objects. Returns a dict
of information. Ifraw=Trueis passed a large dict of the raw RDAP response will be
returned. If the query fails awhoisit.errors.QueryErrorexception will be raised. If
no bootstrap data is loaded awhoisit.errors.BootstrapErrorexception will be raised.
ifallow_insecure_ssl=Trueis passed the RDAP queries will allow weaker SSL handshakes.
Examples:whoisit.ip('1.1.1.1')whoisit.ip('1.1.1.1',rir='apnic')whoisit.ip('1.1.1.1',raw=True,rir='apnic')whoisit.ip('1.1.1.0/24')whoisit.ip(IPv4Address('1.1.1.1'))whoisit.ip(IPv4Network('1.1.1.0/24'))whoisit.ip(IPv6Address('2001:4860:4860::8888'))whoisit.ip(IPv6Network('2001:4860::/32'),rir='arin')whoisit.ip('1.1.1.1',allow_insecure_ssl=True)whoisit.entity(entity=str, rir=str, raw=bool, allow_insecure_ssl=bool)->dictQueries a remote RDAP server for information about the specified entity name. The
entity name must be a string and in the correct entity format. Returns a dict of
information. Ifraw=Trueis passed a large dict of the raw RDAP response will be
returned. If the query fails awhoisit.errors.QueryErrorexception will be raised.
If no bootstrap data is loaded awhoisit.errors.BootstrapErrorexception will be
raised. ifallow_insecure_ssl=Trueis passed the RDAP queries will allow weaker
SSL handshakes. Examples:whoisit.entity('ZG39-ARIN')whoisit.entity('ZG39-ARIN',rir='arin')whoisit.entity('ZG39-ARIN',rir='arin',raw=True)whoisit.entity('ZG39-ARIN',allow_insecure_ssl=True)Data usageAll data returned by RDAP servers are covered by the various policies embeddd in the
results. As such you should carefuly review your usage of the data to make sure it
complies with the policy of the RDAP server you are querying.Excessive useAs an API clientwhoisitis entirely subject to the resource and request limits
applied by the remote RDAP servers it queries. If you recieve request errors for rate
limiting you should slow down your requests. Different servers have different limits.
The LACNIC RDAP server in particular only permits a low number of requests per minute.TestsThere is a test suite that you can run by cloning this repository, installing the
required dependancies and execuiting:$maketestDebuggingwhoisitwill check for aDEBUGenvironment variable and if set, will output debug
logs that detail the internals for the bootstrapping, requests and parsing operations.
If you want to enable debug logging, setDEBUG=true(or1oryetc.). For example:$exportDEBUG=true$python3some-script-that-uses-whoisit.pyContributingAll properly formatted and sensible pull requests, issues and comments are welcome.