We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
dns-rfc2136 challenges run infinitely for invalid DNS servers
1. Set `dns_rfc2136_server` to something invalid, e.g. `dns_rfc2136_server = 2001:db8::`.
2. Try to obtain certificates.
3. Challenges run forever.

CTRL+C during a challenge:
```
[... usual]
Performing the following challenges:
dns-01 challenge for [...]
dns-01 challenge for [...]
Encountered exception:
Traceback (most recent call last):
  File "/usr/lib/python3.7/site-packages/certbot/auth_handler.py", line 75, in handle_authorizations
    resp = self._solve_challenges(aauthzrs)
  File "/usr/lib/python3.7/site-packages/certbot/auth_handler.py", line 139, in _solve_challenges
    resp = self.auth.perform(all_achalls)
  File "/usr/lib/python3.7/site-packages/certbot/plugins/dns_common.py", line 57, in perform
    self._perform(domain, validation_domain_name, validation)
  File "/usr/lib/python3.7/site-packages/certbot_dns_rfc2136/dns_rfc2136.py", line 76, in _perform
    self._get_rfc2136_client().add_txt_record(validation_name, validation, self.ttl)
  File "/usr/lib/python3.7/site-packages/certbot_dns_rfc2136/dns_rfc2136.py", line 112, in add_txt_record
    domain = self._find_domain(record_name)
  File "/usr/lib/python3.7/site-packages/certbot_dns_rfc2136/dns_rfc2136.py", line 186, in _find_domain
    if self._query_soa(guess):
  File "/usr/lib/python3.7/site-packages/certbot_dns_rfc2136/dns_rfc2136.py", line 209, in _query_soa
    response = dns.query.udp(request, self.server, port=self.port)
  File "/usr/lib/python3.7/site-packages/dns/query.py", line 240, in udp
    _wait_for_readable(s, expiration)
  File "/usr/lib/python3.7/site-packages/dns/query.py", line 157, in _wait_for_readable
    _wait_for(s, True, False, True, expiration)
  File "/usr/lib/python3.7/site-packages/dns/query.py", line 131, in _wait_for
    if not _polling_backend(fd, readable, writable, error, timeout):
  File "/usr/lib/python3.7/site-packages/dns/query.py", line 87, in _poll_for
    event_list = pollable.poll()
KeyboardInterrupt
```

**Expected:**  
Certbot should set a reasonable query timeout ([this is supported by `dnspython`](https://stackoverflow.com/questions/8989457/dnspython-setting-query-timeout-lifetime)) and alert the user of an issue with the DNS server.

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `certbot-dns-rfc2136/certbot_dns_rfc2136/_internal/dns_rfc2136.py`
Content:
```
1 """DNS Authenticator using RFC 2136 Dynamic Updates."""
2 import logging
3 
4 import dns.flags
5 import dns.message
6 import dns.name
7 import dns.query
8 import dns.rdataclass
9 import dns.rdatatype
10 import dns.tsig
11 import dns.tsigkeyring
12 import dns.update
13 import zope.interface
14 
15 from certbot import errors
16 from certbot import interfaces
17 from certbot.plugins import dns_common
18 
19 logger = logging.getLogger(__name__)
20 
21 
22 @zope.interface.implementer(interfaces.IAuthenticator)
23 @zope.interface.provider(interfaces.IPluginFactory)
24 class Authenticator(dns_common.DNSAuthenticator):
25     """DNS Authenticator using RFC 2136 Dynamic Updates
26 
27     This Authenticator uses RFC 2136 Dynamic Updates to fulfull a dns-01 challenge.
28     """
29 
30     ALGORITHMS = {
31       'HMAC-MD5': dns.tsig.HMAC_MD5,
32       'HMAC-SHA1': dns.tsig.HMAC_SHA1,
33       'HMAC-SHA224': dns.tsig.HMAC_SHA224,
34       'HMAC-SHA256': dns.tsig.HMAC_SHA256,
35       'HMAC-SHA384': dns.tsig.HMAC_SHA384,
36       'HMAC-SHA512': dns.tsig.HMAC_SHA512
37     }
38 
39     PORT = 53
40 
41     description = 'Obtain certificates using a DNS TXT record (if you are using BIND for DNS).'
42     ttl = 120
43 
44     def __init__(self, *args, **kwargs):
45         super(Authenticator, self).__init__(*args, **kwargs)
46         self.credentials = None
47 
48     @classmethod
49     def add_parser_arguments(cls, add):  # pylint: disable=arguments-differ
50         super(Authenticator, cls).add_parser_arguments(add, default_propagation_seconds=60)
51         add('credentials', help='RFC 2136 credentials INI file.')
52 
53     def more_info(self):  # pylint: disable=missing-function-docstring
54         return 'This plugin configures a DNS TXT record to respond to a dns-01 challenge using ' + \
55                'RFC 2136 Dynamic Updates.'
56 
57     def _validate_algorithm(self, credentials):
58         algorithm = credentials.conf('algorithm')
59         if algorithm:
60             if not self.ALGORITHMS.get(algorithm.upper()):
61                 raise errors.PluginError("Unknown algorithm: {0}.".format(algorithm))
62 
63     def _setup_credentials(self):
64         self.credentials = self._configure_credentials(
65             'credentials',
66             'RFC 2136 credentials INI file',
67             {
68                 'name': 'TSIG key name',
69                 'secret': 'TSIG key secret',
70                 'server': 'The target DNS server'
71             },
72             self._validate_algorithm
73         )
74 
75     def _perform(self, _domain, validation_name, validation):
76         self._get_rfc2136_client().add_txt_record(validation_name, validation, self.ttl)
77 
78     def _cleanup(self, _domain, validation_name, validation):
79         self._get_rfc2136_client().del_txt_record(validation_name, validation)
80 
81     def _get_rfc2136_client(self):
82         return _RFC2136Client(self.credentials.conf('server'),
83                               int(self.credentials.conf('port') or self.PORT),
84                               self.credentials.conf('name'),
85                               self.credentials.conf('secret'),
86                               self.ALGORITHMS.get(self.credentials.conf('algorithm'),
87                                                   dns.tsig.HMAC_MD5))
88 
89 
90 class _RFC2136Client(object):
91     """
92     Encapsulates all communication with the target DNS server.
93     """
94     def __init__(self, server, port, key_name, key_secret, key_algorithm):
95         self.server = server
96         self.port = port
97         self.keyring = dns.tsigkeyring.from_text({
98             key_name: key_secret
99         })
100         self.algorithm = key_algorithm
101 
102     def add_txt_record(self, record_name, record_content, record_ttl):
103         """
104         Add a TXT record using the supplied information.
105 
106         :param str record_name: The record name (typically beginning with '_acme-challenge.').
107         :param str record_content: The record content (typically the challenge validation).
108         :param int record_ttl: The record TTL (number of seconds that the record may be cached).
109         :raises certbot.errors.PluginError: if an error occurs communicating with the DNS server
110         """
111 
112         domain = self._find_domain(record_name)
113 
114         n = dns.name.from_text(record_name)
115         o = dns.name.from_text(domain)
116         rel = n.relativize(o)
117 
118         update = dns.update.Update(
119             domain,
120             keyring=self.keyring,
121             keyalgorithm=self.algorithm)
122         update.add(rel, record_ttl, dns.rdatatype.TXT, record_content)
123 
124         try:
125             response = dns.query.tcp(update, self.server, port=self.port)
126         except Exception as e:
127             raise errors.PluginError('Encountered error adding TXT record: {0}'
128                                      .format(e))
129         rcode = response.rcode()
130 
131         if rcode == dns.rcode.NOERROR:
132             logger.debug('Successfully added TXT record %s', record_name)
133         else:
134             raise errors.PluginError('Received response from server: {0}'
135                                      .format(dns.rcode.to_text(rcode)))
136 
137     def del_txt_record(self, record_name, record_content):
138         """
139         Delete a TXT record using the supplied information.
140 
141         :param str record_name: The record name (typically beginning with '_acme-challenge.').
142         :param str record_content: The record content (typically the challenge validation).
143         :param int record_ttl: The record TTL (number of seconds that the record may be cached).
144         :raises certbot.errors.PluginError: if an error occurs communicating with the DNS server
145         """
146 
147         domain = self._find_domain(record_name)
148 
149         n = dns.name.from_text(record_name)
150         o = dns.name.from_text(domain)
151         rel = n.relativize(o)
152 
153         update = dns.update.Update(
154             domain,
155             keyring=self.keyring,
156             keyalgorithm=self.algorithm)
157         update.delete(rel, dns.rdatatype.TXT, record_content)
158 
159         try:
160             response = dns.query.tcp(update, self.server, port=self.port)
161         except Exception as e:
162             raise errors.PluginError('Encountered error deleting TXT record: {0}'
163                                      .format(e))
164         rcode = response.rcode()
165 
166         if rcode == dns.rcode.NOERROR:
167             logger.debug('Successfully deleted TXT record %s', record_name)
168         else:
169             raise errors.PluginError('Received response from server: {0}'
170                                      .format(dns.rcode.to_text(rcode)))
171 
172     def _find_domain(self, record_name):
173         """
174         Find the closest domain with an SOA record for a given domain name.
175 
176         :param str record_name: The record name for which to find the closest SOA record.
177         :returns: The domain, if found.
178         :rtype: str
179         :raises certbot.errors.PluginError: if no SOA record can be found.
180         """
181 
182         domain_name_guesses = dns_common.base_domain_name_guesses(record_name)
183 
184         # Loop through until we find an authoritative SOA record
185         for guess in domain_name_guesses:
186             if self._query_soa(guess):
187                 return guess
188 
189         raise errors.PluginError('Unable to determine base domain for {0} using names: {1}.'
190                                  .format(record_name, domain_name_guesses))
191 
192     def _query_soa(self, domain_name):
193         """
194         Query a domain name for an authoritative SOA record.
195 
196         :param str domain_name: The domain name to query for an SOA record.
197         :returns: True if found, False otherwise.
198         :rtype: bool
199         :raises certbot.errors.PluginError: if no response is received.
200         """
201 
202         domain = dns.name.from_text(domain_name)
203 
204         request = dns.message.make_query(domain, dns.rdatatype.SOA, dns.rdataclass.IN)
205         # Turn off Recursion Desired bit in query
206         request.flags ^= dns.flags.RD
207 
208         try:
209             try:
210                 response = dns.query.tcp(request, self.server, port=self.port)
211             except OSError as e:
212                 logger.debug('TCP query failed, fallback to UDP: %s', e)
213                 response = dns.query.udp(request, self.server, port=self.port)
214             rcode = response.rcode()
215 
216             # Authoritative Answer bit should be set
217             if (rcode == dns.rcode.NOERROR and response.get_rrset(response.answer,
218                 domain, dns.rdataclass.IN, dns.rdatatype.SOA) and response.flags & dns.flags.AA):
219                 logger.debug('Received authoritative SOA response for %s', domain_name)
220                 return True
221 
222             logger.debug('No authoritative SOA record found for %s', domain_name)
223             return False
224         except Exception as e:
225             raise errors.PluginError('Encountered error when making query: {0}'
226                                      .format(e))
227
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/certbot-dns-rfc2136/certbot_dns_rfc2136/_internal/dns_rfc2136.py b/certbot-dns-rfc2136/certbot_dns_rfc2136/_internal/dns_rfc2136.py
--- a/certbot-dns-rfc2136/certbot_dns_rfc2136/_internal/dns_rfc2136.py
+++ b/certbot-dns-rfc2136/certbot_dns_rfc2136/_internal/dns_rfc2136.py
@@ -18,6 +18,7 @@
 
 logger = logging.getLogger(__name__)
 
+DEFAULT_NETWORK_TIMEOUT = 45
 
 @zope.interface.implementer(interfaces.IAuthenticator)
 @zope.interface.provider(interfaces.IPluginFactory)
@@ -91,13 +92,15 @@
     """
     Encapsulates all communication with the target DNS server.
     """
-    def __init__(self, server, port, key_name, key_secret, key_algorithm):
+    def __init__(self, server, port, key_name, key_secret, key_algorithm,
+        timeout=DEFAULT_NETWORK_TIMEOUT):
         self.server = server
         self.port = port
         self.keyring = dns.tsigkeyring.from_text({
             key_name: key_secret
         })
         self.algorithm = key_algorithm
+        self._default_timeout = timeout
 
     def add_txt_record(self, record_name, record_content, record_ttl):
         """
@@ -122,7 +125,7 @@
         update.add(rel, record_ttl, dns.rdatatype.TXT, record_content)
 
         try:
-            response = dns.query.tcp(update, self.server, port=self.port)
+            response = dns.query.tcp(update, self.server, self._default_timeout, self.port)
         except Exception as e:
             raise errors.PluginError('Encountered error adding TXT record: {0}'
                                      .format(e))
@@ -157,7 +160,7 @@
         update.delete(rel, dns.rdatatype.TXT, record_content)
 
         try:
-            response = dns.query.tcp(update, self.server, port=self.port)
+            response = dns.query.tcp(update, self.server, self._default_timeout, self.port)
         except Exception as e:
             raise errors.PluginError('Encountered error deleting TXT record: {0}'
                                      .format(e))
@@ -207,10 +210,10 @@
 
         try:
             try:
-                response = dns.query.tcp(request, self.server, port=self.port)
-            except OSError as e:
+                response = dns.query.tcp(request, self.server, self._default_timeout, self.port)
+            except (OSError, dns.exception.Timeout) as e:
                 logger.debug('TCP query failed, fallback to UDP: %s', e)
-                response = dns.query.udp(request, self.server, port=self.port)
+                response = dns.query.udp(request, self.server, self._default_timeout, self.port)
             rcode = response.rcode()
 
             # Authoritative Answer bit should be set

{"golden_diff": "diff --git a/certbot-dns-rfc2136/certbot_dns_rfc2136/_internal/dns_rfc2136.py b/certbot-dns-rfc2136/certbot_dns_rfc2136/_internal/dns_rfc2136.py\n--- a/certbot-dns-rfc2136/certbot_dns_rfc2136/_internal/dns_rfc2136.py\n+++ b/certbot-dns-rfc2136/certbot_dns_rfc2136/_internal/dns_rfc2136.py\n@@ -18,6 +18,7 @@\n \n logger = logging.getLogger(__name__)\n \n+DEFAULT_NETWORK_TIMEOUT = 45\n \n @zope.interface.implementer(interfaces.IAuthenticator)\n @zope.interface.provider(interfaces.IPluginFactory)\n@@ -91,13 +92,15 @@\n     \"\"\"\n     Encapsulates all communication with the target DNS server.\n     \"\"\"\n-    def __init__(self, server, port, key_name, key_secret, key_algorithm):\n+    def __init__(self, server, port, key_name, key_secret, key_algorithm,\n+        timeout=DEFAULT_NETWORK_TIMEOUT):\n         self.server = server\n         self.port = port\n         self.keyring = dns.tsigkeyring.from_text({\n             key_name: key_secret\n         })\n         self.algorithm = key_algorithm\n+        self._default_timeout = timeout\n \n     def add_txt_record(self, record_name, record_content, record_ttl):\n         \"\"\"\n@@ -122,7 +125,7 @@\n         update.add(rel, record_ttl, dns.rdatatype.TXT, record_content)\n \n         try:\n-            response = dns.query.tcp(update, self.server, port=self.port)\n+            response = dns.query.tcp(update, self.server, self._default_timeout, self.port)\n         except Exception as e:\n             raise errors.PluginError('Encountered error adding TXT record: {0}'\n                                      .format(e))\n@@ -157,7 +160,7 @@\n         update.delete(rel, dns.rdatatype.TXT, record_content)\n \n         try:\n-            response = dns.query.tcp(update, self.server, port=self.port)\n+            response = dns.query.tcp(update, self.server, self._default_timeout, self.port)\n         except Exception as e:\n             raise errors.PluginError('Encountered error deleting TXT record: {0}'\n                                      .format(e))\n@@ -207,10 +210,10 @@\n \n         try:\n             try:\n-                response = dns.query.tcp(request, self.server, port=self.port)\n-            except OSError as e:\n+                response = dns.query.tcp(request, self.server, self._default_timeout, self.port)\n+            except (OSError, dns.exception.Timeout) as e:\n                 logger.debug('TCP query failed, fallback to UDP: %s', e)\n-                response = dns.query.udp(request, self.server, port=self.port)\n+                response = dns.query.udp(request, self.server, self._default_timeout, self.port)\n             rcode = response.rcode()\n \n             # Authoritative Answer bit should be set\n", "issue": "dns-rfc2136 challenges run infinitely for invalid DNS servers\n1. Set `dns_rfc2136_server` to something invalid, e.g. `dns_rfc2136_server = 2001:db8::`.\r\n2. Try to obtain certificates.\r\n3. Challenges run forever.\r\n\r\nCTRL+C during a challenge:\r\n```\r\n[... usual]\r\nPerforming the following challenges:\r\ndns-01 challenge for [...]\r\ndns-01 challenge for [...]\r\nEncountered exception:\r\nTraceback (most recent call last):\r\n  File \"/usr/lib/python3.7/site-packages/certbot/auth_handler.py\", line 75, in handle_authorizations\r\n    resp = self._solve_challenges(aauthzrs)\r\n  File \"/usr/lib/python3.7/site-packages/certbot/auth_handler.py\", line 139, in _solve_challenges\r\n    resp = self.auth.perform(all_achalls)\r\n  File \"/usr/lib/python3.7/site-packages/certbot/plugins/dns_common.py\", line 57, in perform\r\n    self._perform(domain, validation_domain_name, validation)\r\n  File \"/usr/lib/python3.7/site-packages/certbot_dns_rfc2136/dns_rfc2136.py\", line 76, in _perform\r\n    self._get_rfc2136_client().add_txt_record(validation_name, validation, self.ttl)\r\n  File \"/usr/lib/python3.7/site-packages/certbot_dns_rfc2136/dns_rfc2136.py\", line 112, in add_txt_record\r\n    domain = self._find_domain(record_name)\r\n  File \"/usr/lib/python3.7/site-packages/certbot_dns_rfc2136/dns_rfc2136.py\", line 186, in _find_domain\r\n    if self._query_soa(guess):\r\n  File \"/usr/lib/python3.7/site-packages/certbot_dns_rfc2136/dns_rfc2136.py\", line 209, in _query_soa\r\n    response = dns.query.udp(request, self.server, port=self.port)\r\n  File \"/usr/lib/python3.7/site-packages/dns/query.py\", line 240, in udp\r\n    _wait_for_readable(s, expiration)\r\n  File \"/usr/lib/python3.7/site-packages/dns/query.py\", line 157, in _wait_for_readable\r\n    _wait_for(s, True, False, True, expiration)\r\n  File \"/usr/lib/python3.7/site-packages/dns/query.py\", line 131, in _wait_for\r\n    if not _polling_backend(fd, readable, writable, error, timeout):\r\n  File \"/usr/lib/python3.7/site-packages/dns/query.py\", line 87, in _poll_for\r\n    event_list = pollable.poll()\r\nKeyboardInterrupt\r\n```\r\n\r\n**Expected:**  \r\nCertbot should set a reasonable query timeout ([this is supported by `dnspython`](https://stackoverflow.com/questions/8989457/dnspython-setting-query-timeout-lifetime)) and alert the user of an issue with the DNS server.\n", "before_files": [{"content": "\"\"\"DNS Authenticator using RFC 2136 Dynamic Updates.\"\"\"\nimport logging\n\nimport dns.flags\nimport dns.message\nimport dns.name\nimport dns.query\nimport dns.rdataclass\nimport dns.rdatatype\nimport dns.tsig\nimport dns.tsigkeyring\nimport dns.update\nimport zope.interface\n\nfrom certbot import errors\nfrom certbot import interfaces\nfrom certbot.plugins import dns_common\n\nlogger = logging.getLogger(__name__)\n\n\n@zope.interface.implementer(interfaces.IAuthenticator)\n@zope.interface.provider(interfaces.IPluginFactory)\nclass Authenticator(dns_common.DNSAuthenticator):\n    \"\"\"DNS Authenticator using RFC 2136 Dynamic Updates\n\n    This Authenticator uses RFC 2136 Dynamic Updates to fulfull a dns-01 challenge.\n    \"\"\"\n\n    ALGORITHMS = {\n      'HMAC-MD5': dns.tsig.HMAC_MD5,\n      'HMAC-SHA1': dns.tsig.HMAC_SHA1,\n      'HMAC-SHA224': dns.tsig.HMAC_SHA224,\n      'HMAC-SHA256': dns.tsig.HMAC_SHA256,\n      'HMAC-SHA384': dns.tsig.HMAC_SHA384,\n      'HMAC-SHA512': dns.tsig.HMAC_SHA512\n    }\n\n    PORT = 53\n\n    description = 'Obtain certificates using a DNS TXT record (if you are using BIND for DNS).'\n    ttl = 120\n\n    def __init__(self, *args, **kwargs):\n        super(Authenticator, self).__init__(*args, **kwargs)\n        self.credentials = None\n\n    @classmethod\n    def add_parser_arguments(cls, add):  # pylint: disable=arguments-differ\n        super(Authenticator, cls).add_parser_arguments(add, default_propagation_seconds=60)\n        add('credentials', help='RFC 2136 credentials INI file.')\n\n    def more_info(self):  # pylint: disable=missing-function-docstring\n        return 'This plugin configures a DNS TXT record to respond to a dns-01 challenge using ' + \\\n               'RFC 2136 Dynamic Updates.'\n\n    def _validate_algorithm(self, credentials):\n        algorithm = credentials.conf('algorithm')\n        if algorithm:\n            if not self.ALGORITHMS.get(algorithm.upper()):\n                raise errors.PluginError(\"Unknown algorithm: {0}.\".format(algorithm))\n\n    def _setup_credentials(self):\n        self.credentials = self._configure_credentials(\n            'credentials',\n            'RFC 2136 credentials INI file',\n            {\n                'name': 'TSIG key name',\n                'secret': 'TSIG key secret',\n                'server': 'The target DNS server'\n            },\n            self._validate_algorithm\n        )\n\n    def _perform(self, _domain, validation_name, validation):\n        self._get_rfc2136_client().add_txt_record(validation_name, validation, self.ttl)\n\n    def _cleanup(self, _domain, validation_name, validation):\n        self._get_rfc2136_client().del_txt_record(validation_name, validation)\n\n    def _get_rfc2136_client(self):\n        return _RFC2136Client(self.credentials.conf('server'),\n                              int(self.credentials.conf('port') or self.PORT),\n                              self.credentials.conf('name'),\n                              self.credentials.conf('secret'),\n                              self.ALGORITHMS.get(self.credentials.conf('algorithm'),\n                                                  dns.tsig.HMAC_MD5))\n\n\nclass _RFC2136Client(object):\n    \"\"\"\n    Encapsulates all communication with the target DNS server.\n    \"\"\"\n    def __init__(self, server, port, key_name, key_secret, key_algorithm):\n        self.server = server\n        self.port = port\n        self.keyring = dns.tsigkeyring.from_text({\n            key_name: key_secret\n        })\n        self.algorithm = key_algorithm\n\n    def add_txt_record(self, record_name, record_content, record_ttl):\n        \"\"\"\n        Add a TXT record using the supplied information.\n\n        :param str record_name: The record name (typically beginning with '_acme-challenge.').\n        :param str record_content: The record content (typically the challenge validation).\n        :param int record_ttl: The record TTL (number of seconds that the record may be cached).\n        :raises certbot.errors.PluginError: if an error occurs communicating with the DNS server\n        \"\"\"\n\n        domain = self._find_domain(record_name)\n\n        n = dns.name.from_text(record_name)\n        o = dns.name.from_text(domain)\n        rel = n.relativize(o)\n\n        update = dns.update.Update(\n            domain,\n            keyring=self.keyring,\n            keyalgorithm=self.algorithm)\n        update.add(rel, record_ttl, dns.rdatatype.TXT, record_content)\n\n        try:\n            response = dns.query.tcp(update, self.server, port=self.port)\n        except Exception as e:\n            raise errors.PluginError('Encountered error adding TXT record: {0}'\n                                     .format(e))\n        rcode = response.rcode()\n\n        if rcode == dns.rcode.NOERROR:\n            logger.debug('Successfully added TXT record %s', record_name)\n        else:\n            raise errors.PluginError('Received response from server: {0}'\n                                     .format(dns.rcode.to_text(rcode)))\n\n    def del_txt_record(self, record_name, record_content):\n        \"\"\"\n        Delete a TXT record using the supplied information.\n\n        :param str record_name: The record name (typically beginning with '_acme-challenge.').\n        :param str record_content: The record content (typically the challenge validation).\n        :param int record_ttl: The record TTL (number of seconds that the record may be cached).\n        :raises certbot.errors.PluginError: if an error occurs communicating with the DNS server\n        \"\"\"\n\n        domain = self._find_domain(record_name)\n\n        n = dns.name.from_text(record_name)\n        o = dns.name.from_text(domain)\n        rel = n.relativize(o)\n\n        update = dns.update.Update(\n            domain,\n            keyring=self.keyring,\n            keyalgorithm=self.algorithm)\n        update.delete(rel, dns.rdatatype.TXT, record_content)\n\n        try:\n            response = dns.query.tcp(update, self.server, port=self.port)\n        except Exception as e:\n            raise errors.PluginError('Encountered error deleting TXT record: {0}'\n                                     .format(e))\n        rcode = response.rcode()\n\n        if rcode == dns.rcode.NOERROR:\n            logger.debug('Successfully deleted TXT record %s', record_name)\n        else:\n            raise errors.PluginError('Received response from server: {0}'\n                                     .format(dns.rcode.to_text(rcode)))\n\n    def _find_domain(self, record_name):\n        \"\"\"\n        Find the closest domain with an SOA record for a given domain name.\n\n        :param str record_name: The record name for which to find the closest SOA record.\n        :returns: The domain, if found.\n        :rtype: str\n        :raises certbot.errors.PluginError: if no SOA record can be found.\n        \"\"\"\n\n        domain_name_guesses = dns_common.base_domain_name_guesses(record_name)\n\n        # Loop through until we find an authoritative SOA record\n        for guess in domain_name_guesses:\n            if self._query_soa(guess):\n                return guess\n\n        raise errors.PluginError('Unable to determine base domain for {0} using names: {1}.'\n                                 .format(record_name, domain_name_guesses))\n\n    def _query_soa(self, domain_name):\n        \"\"\"\n        Query a domain name for an authoritative SOA record.\n\n        :param str domain_name: The domain name to query for an SOA record.\n        :returns: True if found, False otherwise.\n        :rtype: bool\n        :raises certbot.errors.PluginError: if no response is received.\n        \"\"\"\n\n        domain = dns.name.from_text(domain_name)\n\n        request = dns.message.make_query(domain, dns.rdatatype.SOA, dns.rdataclass.IN)\n        # Turn off Recursion Desired bit in query\n        request.flags ^= dns.flags.RD\n\n        try:\n            try:\n                response = dns.query.tcp(request, self.server, port=self.port)\n            except OSError as e:\n                logger.debug('TCP query failed, fallback to UDP: %s', e)\n                response = dns.query.udp(request, self.server, port=self.port)\n            rcode = response.rcode()\n\n            # Authoritative Answer bit should be set\n            if (rcode == dns.rcode.NOERROR and response.get_rrset(response.answer,\n                domain, dns.rdataclass.IN, dns.rdatatype.SOA) and response.flags & dns.flags.AA):\n                logger.debug('Received authoritative SOA response for %s', domain_name)\n                return True\n\n            logger.debug('No authoritative SOA record found for %s', domain_name)\n            return False\n        except Exception as e:\n            raise errors.PluginError('Encountered error when making query: {0}'\n                                     .format(e))\n", "path": "certbot-dns-rfc2136/certbot_dns_rfc2136/_internal/dns_rfc2136.py"}], "after_files": [{"content": "\"\"\"DNS Authenticator using RFC 2136 Dynamic Updates.\"\"\"\nimport logging\n\nimport dns.flags\nimport dns.message\nimport dns.name\nimport dns.query\nimport dns.rdataclass\nimport dns.rdatatype\nimport dns.tsig\nimport dns.tsigkeyring\nimport dns.update\nimport zope.interface\n\nfrom certbot import errors\nfrom certbot import interfaces\nfrom certbot.plugins import dns_common\n\nlogger = logging.getLogger(__name__)\n\nDEFAULT_NETWORK_TIMEOUT = 45\n\n@zope.interface.implementer(interfaces.IAuthenticator)\n@zope.interface.provider(interfaces.IPluginFactory)\nclass Authenticator(dns_common.DNSAuthenticator):\n    \"\"\"DNS Authenticator using RFC 2136 Dynamic Updates\n\n    This Authenticator uses RFC 2136 Dynamic Updates to fulfull a dns-01 challenge.\n    \"\"\"\n\n    ALGORITHMS = {\n      'HMAC-MD5': dns.tsig.HMAC_MD5,\n      'HMAC-SHA1': dns.tsig.HMAC_SHA1,\n      'HMAC-SHA224': dns.tsig.HMAC_SHA224,\n      'HMAC-SHA256': dns.tsig.HMAC_SHA256,\n      'HMAC-SHA384': dns.tsig.HMAC_SHA384,\n      'HMAC-SHA512': dns.tsig.HMAC_SHA512\n    }\n\n    PORT = 53\n\n    description = 'Obtain certificates using a DNS TXT record (if you are using BIND for DNS).'\n    ttl = 120\n\n    def __init__(self, *args, **kwargs):\n        super(Authenticator, self).__init__(*args, **kwargs)\n        self.credentials = None\n\n    @classmethod\n    def add_parser_arguments(cls, add):  # pylint: disable=arguments-differ\n        super(Authenticator, cls).add_parser_arguments(add, default_propagation_seconds=60)\n        add('credentials', help='RFC 2136 credentials INI file.')\n\n    def more_info(self):  # pylint: disable=missing-function-docstring\n        return 'This plugin configures a DNS TXT record to respond to a dns-01 challenge using ' + \\\n               'RFC 2136 Dynamic Updates.'\n\n    def _validate_algorithm(self, credentials):\n        algorithm = credentials.conf('algorithm')\n        if algorithm:\n            if not self.ALGORITHMS.get(algorithm.upper()):\n                raise errors.PluginError(\"Unknown algorithm: {0}.\".format(algorithm))\n\n    def _setup_credentials(self):\n        self.credentials = self._configure_credentials(\n            'credentials',\n            'RFC 2136 credentials INI file',\n            {\n                'name': 'TSIG key name',\n                'secret': 'TSIG key secret',\n                'server': 'The target DNS server'\n            },\n            self._validate_algorithm\n        )\n\n    def _perform(self, _domain, validation_name, validation):\n        self._get_rfc2136_client().add_txt_record(validation_name, validation, self.ttl)\n\n    def _cleanup(self, _domain, validation_name, validation):\n        self._get_rfc2136_client().del_txt_record(validation_name, validation)\n\n    def _get_rfc2136_client(self):\n        return _RFC2136Client(self.credentials.conf('server'),\n                              int(self.credentials.conf('port') or self.PORT),\n                              self.credentials.conf('name'),\n                              self.credentials.conf('secret'),\n                              self.ALGORITHMS.get(self.credentials.conf('algorithm'),\n                                                  dns.tsig.HMAC_MD5))\n\n\nclass _RFC2136Client(object):\n    \"\"\"\n    Encapsulates all communication with the target DNS server.\n    \"\"\"\n    def __init__(self, server, port, key_name, key_secret, key_algorithm,\n        timeout=DEFAULT_NETWORK_TIMEOUT):\n        self.server = server\n        self.port = port\n        self.keyring = dns.tsigkeyring.from_text({\n            key_name: key_secret\n        })\n        self.algorithm = key_algorithm\n        self._default_timeout = timeout\n\n    def add_txt_record(self, record_name, record_content, record_ttl):\n        \"\"\"\n        Add a TXT record using the supplied information.\n\n        :param str record_name: The record name (typically beginning with '_acme-challenge.').\n        :param str record_content: The record content (typically the challenge validation).\n        :param int record_ttl: The record TTL (number of seconds that the record may be cached).\n        :raises certbot.errors.PluginError: if an error occurs communicating with the DNS server\n        \"\"\"\n\n        domain = self._find_domain(record_name)\n\n        n = dns.name.from_text(record_name)\n        o = dns.name.from_text(domain)\n        rel = n.relativize(o)\n\n        update = dns.update.Update(\n            domain,\n            keyring=self.keyring,\n            keyalgorithm=self.algorithm)\n        update.add(rel, record_ttl, dns.rdatatype.TXT, record_content)\n\n        try:\n            response = dns.query.tcp(update, self.server, self._default_timeout, self.port)\n        except Exception as e:\n            raise errors.PluginError('Encountered error adding TXT record: {0}'\n                                     .format(e))\n        rcode = response.rcode()\n\n        if rcode == dns.rcode.NOERROR:\n            logger.debug('Successfully added TXT record %s', record_name)\n        else:\n            raise errors.PluginError('Received response from server: {0}'\n                                     .format(dns.rcode.to_text(rcode)))\n\n    def del_txt_record(self, record_name, record_content):\n        \"\"\"\n        Delete a TXT record using the supplied information.\n\n        :param str record_name: The record name (typically beginning with '_acme-challenge.').\n        :param str record_content: The record content (typically the challenge validation).\n        :param int record_ttl: The record TTL (number of seconds that the record may be cached).\n        :raises certbot.errors.PluginError: if an error occurs communicating with the DNS server\n        \"\"\"\n\n        domain = self._find_domain(record_name)\n\n        n = dns.name.from_text(record_name)\n        o = dns.name.from_text(domain)\n        rel = n.relativize(o)\n\n        update = dns.update.Update(\n            domain,\n            keyring=self.keyring,\n            keyalgorithm=self.algorithm)\n        update.delete(rel, dns.rdatatype.TXT, record_content)\n\n        try:\n            response = dns.query.tcp(update, self.server, self._default_timeout, self.port)\n        except Exception as e:\n            raise errors.PluginError('Encountered error deleting TXT record: {0}'\n                                     .format(e))\n        rcode = response.rcode()\n\n        if rcode == dns.rcode.NOERROR:\n            logger.debug('Successfully deleted TXT record %s', record_name)\n        else:\n            raise errors.PluginError('Received response from server: {0}'\n                                     .format(dns.rcode.to_text(rcode)))\n\n    def _find_domain(self, record_name):\n        \"\"\"\n        Find the closest domain with an SOA record for a given domain name.\n\n        :param str record_name: The record name for which to find the closest SOA record.\n        :returns: The domain, if found.\n        :rtype: str\n        :raises certbot.errors.PluginError: if no SOA record can be found.\n        \"\"\"\n\n        domain_name_guesses = dns_common.base_domain_name_guesses(record_name)\n\n        # Loop through until we find an authoritative SOA record\n        for guess in domain_name_guesses:\n            if self._query_soa(guess):\n                return guess\n\n        raise errors.PluginError('Unable to determine base domain for {0} using names: {1}.'\n                                 .format(record_name, domain_name_guesses))\n\n    def _query_soa(self, domain_name):\n        \"\"\"\n        Query a domain name for an authoritative SOA record.\n\n        :param str domain_name: The domain name to query for an SOA record.\n        :returns: True if found, False otherwise.\n        :rtype: bool\n        :raises certbot.errors.PluginError: if no response is received.\n        \"\"\"\n\n        domain = dns.name.from_text(domain_name)\n\n        request = dns.message.make_query(domain, dns.rdatatype.SOA, dns.rdataclass.IN)\n        # Turn off Recursion Desired bit in query\n        request.flags ^= dns.flags.RD\n\n        try:\n            try:\n                response = dns.query.tcp(request, self.server, self._default_timeout, self.port)\n            except (OSError, dns.exception.Timeout) as e:\n                logger.debug('TCP query failed, fallback to UDP: %s', e)\n                response = dns.query.udp(request, self.server, self._default_timeout, self.port)\n            rcode = response.rcode()\n\n            # Authoritative Answer bit should be set\n            if (rcode == dns.rcode.NOERROR and response.get_rrset(response.answer,\n                domain, dns.rdataclass.IN, dns.rdatatype.SOA) and response.flags & dns.flags.AA):\n                logger.debug('Received authoritative SOA response for %s', domain_name)\n                return True\n\n            logger.debug('No authoritative SOA record found for %s', domain_name)\n            return False\n        except Exception as e:\n            raise errors.PluginError('Encountered error when making query: {0}'\n                                     .format(e))\n", "path": "certbot-dns-rfc2136/certbot_dns_rfc2136/_internal/dns_rfc2136.py"}]}