Module simple_acme_dns.tools
DNS tools to assist ACME verification.
Expand source code
# Copyright 2023 Jared Hendrickson
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""DNS tools to assist ACME verification."""
import dns.resolver
class DNSQuery:
"""A basic class to make DNS queries"""
def __init__(
self,
domain: str,
rtype: str = "A",
nameservers: list = None,
authoritative: bool = False,
round_robin: bool = False
) -> None:
"""
Initializes and executes our DNS query.
Args:
domain (list): A list of fully qualified domain names to list in the certificate.
rtype (str): The DNS request type (e.g. `A`, `TXT`, `CNAME`, etc.).
nameservers (list): Nameservers to query when making DNS requests.
authoritative (bool): Use the authoritative nameserver for each domain.
round_robin (`bool): rotate between each nameserver instead of the default fail-over method.
"""
self.round_robin = round_robin
self.type = rtype.upper()
self.domain = domain
self.nameservers = nameservers if nameservers else dns.resolver.Resolver().nameservers
self.nameservers = self.__get_authoritative_nameservers__() if authoritative else self.nameservers
self.values = []
self.answers = []
self.last_nameserver = ""
def resolve(self) -> list:
"""
Queries the nameservers with our configured object values.
Returns:
list: A list of DNS resolution answers.
"""
# Resolve the DNS query
try:
self.answers = DNSQuery.__resolve__(self.domain, rtype=self.type, nameservers=self.nameservers)
except (dns.resolver.NoAnswer, dns.resolver.NXDOMAIN):
self.answers = []
# Rotate the nameservers if round robin mode is enabled
if self.round_robin and len(self.nameservers) > 1:
self.last_nameserver = self.nameservers[0]
self.nameservers = self.nameservers[1:] + [self.last_nameserver]
self.values = self.__parse_values__(self.answers)
return self.values
def __get_authoritative_nameservers__(self) -> list:
"""
Checks the domain's SOA record for the authoritative nameserver of this domain.
Returns:
list: A list of authoritative nameservers
"""
# Local variables
nameserver = []
domain_sections = self.domain.split(".")
# Loop through each level of the subdomain to find the SOA for this FQDN.
while domain_sections:
# Piece together the remaining domain sections to create our next target domain
domain = ".".join(domain_sections)
# Get our SOA record values for this domain and remove the trailing dot from each
try:
nameserver = self.__parse_values__(self.__resolve__(domain, rtype="SOA", nameservers=self.nameservers))
break
except (dns.resolver.NoAnswer, dns.resolver.NXDOMAIN):
domain_sections.pop(0)
continue
# Extract the authoritative nameserver's IP from the response
nameserver = nameserver[0].split("SOA ")
nameserver = nameserver[0].split(" ")[0]
nameserver = nameserver[:-1]
nameserver = self.__parse_values__(self.__resolve__(nameserver, rtype="A", nameservers=self.nameservers))
return nameserver
@staticmethod
def __resolve__(domain: str, rtype: str = "A", nameservers: list = None) -> list:
"""
Internal function-like DNS request method.
Returns:
list: A list of answer values from the request.
"""
resolver = dns.resolver.Resolver()
resolver.nameservers = nameservers if nameservers else resolver.nameservers
# Resolve the DNS query
return DNSQuery.__filter_list__(resolver.resolve(domain, rtype).response.answer[0].to_text().split("\n"))
@staticmethod
def __filter_list__(data: list) -> list:
"""
Filters our list properties to remove blank entries.
Args:
data (list): The list to remove blank entries from.
Returns:
list: The data list stripped of any blank entries.
"""
return list(filter(None, data))
@staticmethod
def __parse_values__(answers: list) -> list:
"""
Parses the value portion of the query answer into it's own list.
Args:
answers (list): the answers list returned by `__resolve__()` method.
Returns:
list: A parsed list of values for each answer.
"""
values = []
# Loop through each answer and parse it's value section to the values property
for answer in answers:
# Save the fourth space separated item as the
value = answer.split(" ", 4)[-1]
value = value.replace("\"", "", 1) if value.startswith("\"") else value
value = value.replace("\"", "", -1) if value.endswith("\"") else value
values.append(value)
return DNSQuery.__filter_list__(values)
Classes
class DNSQuery (domain: str, rtype: str = 'A', nameservers: list = None, authoritative: bool = False, round_robin: bool = False)
-
A basic class to make DNS queries
Initializes and executes our DNS query.
Args
domain
:list
- A list of fully qualified domain names to list in the certificate.
rtype
:str
- The DNS request type (e.g.
A
,TXT
,CNAME
, etc.). nameservers
:list
- Nameservers to query when making DNS requests.
authoritative
:bool
- Use the authoritative nameserver for each domain.
round_robin (`bool): rotate between each nameserver instead of the default fail-over method.
Expand source code
class DNSQuery: """A basic class to make DNS queries""" def __init__( self, domain: str, rtype: str = "A", nameservers: list = None, authoritative: bool = False, round_robin: bool = False ) -> None: """ Initializes and executes our DNS query. Args: domain (list): A list of fully qualified domain names to list in the certificate. rtype (str): The DNS request type (e.g. `A`, `TXT`, `CNAME`, etc.). nameservers (list): Nameservers to query when making DNS requests. authoritative (bool): Use the authoritative nameserver for each domain. round_robin (`bool): rotate between each nameserver instead of the default fail-over method. """ self.round_robin = round_robin self.type = rtype.upper() self.domain = domain self.nameservers = nameservers if nameservers else dns.resolver.Resolver().nameservers self.nameservers = self.__get_authoritative_nameservers__() if authoritative else self.nameservers self.values = [] self.answers = [] self.last_nameserver = "" def resolve(self) -> list: """ Queries the nameservers with our configured object values. Returns: list: A list of DNS resolution answers. """ # Resolve the DNS query try: self.answers = DNSQuery.__resolve__(self.domain, rtype=self.type, nameservers=self.nameservers) except (dns.resolver.NoAnswer, dns.resolver.NXDOMAIN): self.answers = [] # Rotate the nameservers if round robin mode is enabled if self.round_robin and len(self.nameservers) > 1: self.last_nameserver = self.nameservers[0] self.nameservers = self.nameservers[1:] + [self.last_nameserver] self.values = self.__parse_values__(self.answers) return self.values def __get_authoritative_nameservers__(self) -> list: """ Checks the domain's SOA record for the authoritative nameserver of this domain. Returns: list: A list of authoritative nameservers """ # Local variables nameserver = [] domain_sections = self.domain.split(".") # Loop through each level of the subdomain to find the SOA for this FQDN. while domain_sections: # Piece together the remaining domain sections to create our next target domain domain = ".".join(domain_sections) # Get our SOA record values for this domain and remove the trailing dot from each try: nameserver = self.__parse_values__(self.__resolve__(domain, rtype="SOA", nameservers=self.nameservers)) break except (dns.resolver.NoAnswer, dns.resolver.NXDOMAIN): domain_sections.pop(0) continue # Extract the authoritative nameserver's IP from the response nameserver = nameserver[0].split("SOA ") nameserver = nameserver[0].split(" ")[0] nameserver = nameserver[:-1] nameserver = self.__parse_values__(self.__resolve__(nameserver, rtype="A", nameservers=self.nameservers)) return nameserver @staticmethod def __resolve__(domain: str, rtype: str = "A", nameservers: list = None) -> list: """ Internal function-like DNS request method. Returns: list: A list of answer values from the request. """ resolver = dns.resolver.Resolver() resolver.nameservers = nameservers if nameservers else resolver.nameservers # Resolve the DNS query return DNSQuery.__filter_list__(resolver.resolve(domain, rtype).response.answer[0].to_text().split("\n")) @staticmethod def __filter_list__(data: list) -> list: """ Filters our list properties to remove blank entries. Args: data (list): The list to remove blank entries from. Returns: list: The data list stripped of any blank entries. """ return list(filter(None, data)) @staticmethod def __parse_values__(answers: list) -> list: """ Parses the value portion of the query answer into it's own list. Args: answers (list): the answers list returned by `__resolve__()` method. Returns: list: A parsed list of values for each answer. """ values = [] # Loop through each answer and parse it's value section to the values property for answer in answers: # Save the fourth space separated item as the value = answer.split(" ", 4)[-1] value = value.replace("\"", "", 1) if value.startswith("\"") else value value = value.replace("\"", "", -1) if value.endswith("\"") else value values.append(value) return DNSQuery.__filter_list__(values)
Methods
def resolve(self) ‑> list
-
Queries the nameservers with our configured object values.
Returns
list
- A list of DNS resolution answers.
Expand source code
def resolve(self) -> list: """ Queries the nameservers with our configured object values. Returns: list: A list of DNS resolution answers. """ # Resolve the DNS query try: self.answers = DNSQuery.__resolve__(self.domain, rtype=self.type, nameservers=self.nameservers) except (dns.resolver.NoAnswer, dns.resolver.NXDOMAIN): self.answers = [] # Rotate the nameservers if round robin mode is enabled if self.round_robin and len(self.nameservers) > 1: self.last_nameserver = self.nameservers[0] self.nameservers = self.nameservers[1:] + [self.last_nameserver] self.values = self.__parse_values__(self.answers) return self.values