We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
manual: when not using hooks, tell the user they need to wait for DNS propagation
One of the usability things I came across in #8514 was that it's not terribly obvious when setting up TXT records manually that with some DNS hosts, you need to wait a while. 

https://github.com/certbot/certbot/blob/adb7e5e62f4c96be678c5af3e55a82ddcc717590/certbot/certbot/_internal/plugins/manual.py#L45-L51

This bit of vague text could be rewritten in order to convey the advice more directly: 

> Before continuing, verify the record is deployed

I think it would be ideal if we only showed this on the very last DNS challenge, but I'm not sure whether the logic of Certbot will permit that. I don't think it's a big deal to show it every time. 

--- 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/certbot/_internal/plugins/manual.py`
Content:
```
1 """Manual authenticator plugin"""
2 from typing import Dict
3 
4 import zope.component
5 import zope.interface
6 
7 from acme import challenges
8 from certbot import achallenges
9 from certbot import errors
10 from certbot import interfaces
11 from certbot import reverter
12 from certbot import util
13 from certbot._internal import hooks
14 from certbot.compat import misc
15 from certbot.compat import os
16 from certbot.plugins import common
17 
18 
19 @zope.interface.implementer(interfaces.IAuthenticator)
20 @zope.interface.provider(interfaces.IPluginFactory)
21 class Authenticator(common.Plugin):
22     """Manual authenticator
23 
24     This plugin allows the user to perform the domain validation
25     challenge(s) themselves. This either be done manually by the user or
26     through shell scripts provided to Certbot.
27 
28     """
29 
30     description = 'Manual configuration or run your own shell scripts'
31     hidden = True
32     long_description = (
33         'Authenticate through manual configuration or custom shell scripts. '
34         'When using shell scripts, an authenticator script must be provided. '
35         'The environment variables available to this script depend on the '
36         'type of challenge. $CERTBOT_DOMAIN will always contain the domain '
37         'being authenticated. For HTTP-01 and DNS-01, $CERTBOT_VALIDATION '
38         'is the validation string, and $CERTBOT_TOKEN is the filename of the '
39         'resource requested when performing an HTTP-01 challenge. An additional '
40         'cleanup script can also be provided and can use the additional variable '
41         '$CERTBOT_AUTH_OUTPUT which contains the stdout output from the auth script. '
42         'For both authenticator and cleanup script, on HTTP-01 and DNS-01 challenges, '
43         '$CERTBOT_REMAINING_CHALLENGES will be equal to the number of challenges that '
44         'remain after the current one, and $CERTBOT_ALL_DOMAINS contains a comma-separated '
45         'list of all domains that are challenged for the current certificate.')
46     _DNS_INSTRUCTIONS = """\
47 Please deploy a DNS TXT record under the name
48 {domain} with the following value:
49 
50 {validation}
51 
52 Before continuing, verify the record is deployed."""
53     _HTTP_INSTRUCTIONS = """\
54 Create a file containing just this data:
55 
56 {validation}
57 
58 And make it available on your web server at this URL:
59 
60 {uri}
61 """
62     _SUBSEQUENT_CHALLENGE_INSTRUCTIONS = """
63 (This must be set up in addition to the previous challenges; do not remove,
64 replace, or undo the previous challenge tasks yet.)
65 """
66     _SUBSEQUENT_DNS_CHALLENGE_INSTRUCTIONS = """
67 (This must be set up in addition to the previous challenges; do not remove,
68 replace, or undo the previous challenge tasks yet. Note that you might be
69 asked to create multiple distinct TXT records with the same name. This is
70 permitted by DNS standards.)
71 """
72 
73     def __init__(self, *args, **kwargs):
74         super(Authenticator, self).__init__(*args, **kwargs)
75         self.reverter = reverter.Reverter(self.config)
76         self.reverter.recovery_routine()
77         self.env: Dict[achallenges.KeyAuthorizationAnnotatedChallenge, Dict[str, str]] = {}
78         self.subsequent_dns_challenge = False
79         self.subsequent_any_challenge = False
80 
81     @classmethod
82     def add_parser_arguments(cls, add):
83         add('auth-hook',
84             help='Path or command to execute for the authentication script')
85         add('cleanup-hook',
86             help='Path or command to execute for the cleanup script')
87         util.add_deprecated_argument(add, 'public-ip-logging-ok', 0)
88 
89     def prepare(self):  # pylint: disable=missing-function-docstring
90         if self.config.noninteractive_mode and not self.conf('auth-hook'):
91             raise errors.PluginError(
92                 'An authentication script must be provided with --{0} when '
93                 'using the manual plugin non-interactively.'.format(
94                     self.option_name('auth-hook')))
95         self._validate_hooks()
96 
97     def _validate_hooks(self):
98         if self.config.validate_hooks:
99             for name in ('auth-hook', 'cleanup-hook'):
100                 hook = self.conf(name)
101                 if hook is not None:
102                     hook_prefix = self.option_name(name)[:-len('-hook')]
103                     hooks.validate_hook(hook, hook_prefix)
104 
105     def more_info(self):  # pylint: disable=missing-function-docstring
106         return (
107             'This plugin allows the user to customize setup for domain '
108             'validation challenges either through shell scripts provided by '
109             'the user or by performing the setup manually.')
110 
111     def get_chall_pref(self, domain):
112         # pylint: disable=unused-argument,missing-function-docstring
113         return [challenges.HTTP01, challenges.DNS01]
114 
115     def perform(self, achalls):  # pylint: disable=missing-function-docstring
116         responses = []
117         for achall in achalls:
118             if self.conf('auth-hook'):
119                 self._perform_achall_with_script(achall, achalls)
120             else:
121                 self._perform_achall_manually(achall)
122             responses.append(achall.response(achall.account_key))
123         return responses
124 
125     def _perform_achall_with_script(self, achall, achalls):
126         env = dict(CERTBOT_DOMAIN=achall.domain,
127                    CERTBOT_VALIDATION=achall.validation(achall.account_key),
128                    CERTBOT_ALL_DOMAINS=','.join(one_achall.domain for one_achall in achalls),
129                    CERTBOT_REMAINING_CHALLENGES=str(len(achalls) - achalls.index(achall) - 1))
130         if isinstance(achall.chall, challenges.HTTP01):
131             env['CERTBOT_TOKEN'] = achall.chall.encode('token')
132         else:
133             os.environ.pop('CERTBOT_TOKEN', None)
134         os.environ.update(env)
135         _, out = self._execute_hook('auth-hook')
136         env['CERTBOT_AUTH_OUTPUT'] = out.strip()
137         self.env[achall] = env
138 
139     def _perform_achall_manually(self, achall):
140         validation = achall.validation(achall.account_key)
141         if isinstance(achall.chall, challenges.HTTP01):
142             msg = self._HTTP_INSTRUCTIONS.format(
143                 achall=achall, encoded_token=achall.chall.encode('token'),
144                 port=self.config.http01_port,
145                 uri=achall.chall.uri(achall.domain), validation=validation)
146         else:
147             assert isinstance(achall.chall, challenges.DNS01)
148             msg = self._DNS_INSTRUCTIONS.format(
149                 domain=achall.validation_domain_name(achall.domain),
150                 validation=validation)
151         if isinstance(achall.chall, challenges.DNS01):
152             if self.subsequent_dns_challenge:
153                 # 2nd or later dns-01 challenge
154                 msg += self._SUBSEQUENT_DNS_CHALLENGE_INSTRUCTIONS
155             self.subsequent_dns_challenge = True
156         elif self.subsequent_any_challenge:
157             # 2nd or later challenge of another type
158             msg += self._SUBSEQUENT_CHALLENGE_INSTRUCTIONS
159         display = zope.component.getUtility(interfaces.IDisplay)
160         display.notification(msg, wrap=False, force_interactive=True)
161         self.subsequent_any_challenge = True
162 
163     def cleanup(self, achalls):  # pylint: disable=missing-function-docstring
164         if self.conf('cleanup-hook'):
165             for achall in achalls:
166                 env = self.env.pop(achall)
167                 if 'CERTBOT_TOKEN' not in env:
168                     os.environ.pop('CERTBOT_TOKEN', None)
169                 os.environ.update(env)
170                 self._execute_hook('cleanup-hook')
171         self.reverter.recovery_routine()
172 
173     def _execute_hook(self, hook_name):
174         return misc.execute_command(self.option_name(hook_name), self.conf(hook_name),
175                                     env=util.env_no_snap_for_external_calls())
176
```
--- 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/certbot/_internal/plugins/manual.py b/certbot/certbot/_internal/plugins/manual.py
--- a/certbot/certbot/_internal/plugins/manual.py
+++ b/certbot/certbot/_internal/plugins/manual.py
@@ -43,13 +43,30 @@
         '$CERTBOT_REMAINING_CHALLENGES will be equal to the number of challenges that '
         'remain after the current one, and $CERTBOT_ALL_DOMAINS contains a comma-separated '
         'list of all domains that are challenged for the current certificate.')
+    # Include the full stop at the end of the FQDN in the instructions below for the null
+    # label of the DNS root, as stated in section 3.1 of RFC 1035. While not necessary
+    # for most day to day usage of hostnames, when adding FQDNs to a DNS zone editor, this
+    # full stop is often mandatory. Without a full stop, the entered name is often seen as
+    # relative to the DNS zone origin, which could lead to entries for, e.g.:
+    # _acme-challenge.example.com.example.com. For users unaware of this subtle detail,
+    # including the trailing full stop in the DNS instructions below might avert this issue.
     _DNS_INSTRUCTIONS = """\
-Please deploy a DNS TXT record under the name
-{domain} with the following value:
+Please deploy a DNS TXT record under the name:
 
-{validation}
+{domain}.
+
+with the following value:
 
-Before continuing, verify the record is deployed."""
+{validation}
+"""
+    _DNS_VERIFY_INSTRUCTIONS = """
+Before continuing, verify the TXT record has been deployed. Depending on the DNS 
+provider, this may take some time, from a few seconds to multiple minutes. You can
+check if it has finished deploying with aid of online tools, such as the Google
+Admin Toolbox: https://toolbox.googleapps.com/apps/dig/#TXT/{domain}.
+Look for one or more bolded line(s) below the line ';ANSWER'. It should show the
+value(s) you've just added.
+"""
     _HTTP_INSTRUCTIONS = """\
 Create a file containing just this data:
 
@@ -114,11 +131,15 @@
 
     def perform(self, achalls):  # pylint: disable=missing-function-docstring
         responses = []
-        for achall in achalls:
+        last_dns_achall = 0
+        for i, achall in enumerate(achalls):
+            if isinstance(achall.chall, challenges.DNS01):
+                last_dns_achall = i
+        for i, achall in enumerate(achalls):
             if self.conf('auth-hook'):
                 self._perform_achall_with_script(achall, achalls)
             else:
-                self._perform_achall_manually(achall)
+                self._perform_achall_manually(achall, i == last_dns_achall)
             responses.append(achall.response(achall.account_key))
         return responses
 
@@ -136,7 +157,7 @@
         env['CERTBOT_AUTH_OUTPUT'] = out.strip()
         self.env[achall] = env
 
-    def _perform_achall_manually(self, achall):
+    def _perform_achall_manually(self, achall, last_dns_achall=False):
         validation = achall.validation(achall.account_key)
         if isinstance(achall.chall, challenges.HTTP01):
             msg = self._HTTP_INSTRUCTIONS.format(
@@ -152,7 +173,15 @@
             if self.subsequent_dns_challenge:
                 # 2nd or later dns-01 challenge
                 msg += self._SUBSEQUENT_DNS_CHALLENGE_INSTRUCTIONS
+            elif self.subsequent_any_challenge:
+                # 1st dns-01 challenge, but 2nd or later *any* challenge, so
+                # instruct user not to remove any previous http-01 challenge
+                msg += self._SUBSEQUENT_CHALLENGE_INSTRUCTIONS
             self.subsequent_dns_challenge = True
+            if last_dns_achall:
+                # last dns-01 challenge
+                msg += self._DNS_VERIFY_INSTRUCTIONS.format(
+                    domain=achall.validation_domain_name(achall.domain))
         elif self.subsequent_any_challenge:
             # 2nd or later challenge of another type
             msg += self._SUBSEQUENT_CHALLENGE_INSTRUCTIONS

{"golden_diff": "diff --git a/certbot/certbot/_internal/plugins/manual.py b/certbot/certbot/_internal/plugins/manual.py\n--- a/certbot/certbot/_internal/plugins/manual.py\n+++ b/certbot/certbot/_internal/plugins/manual.py\n@@ -43,13 +43,30 @@\n         '$CERTBOT_REMAINING_CHALLENGES will be equal to the number of challenges that '\n         'remain after the current one, and $CERTBOT_ALL_DOMAINS contains a comma-separated '\n         'list of all domains that are challenged for the current certificate.')\n+    # Include the full stop at the end of the FQDN in the instructions below for the null\n+    # label of the DNS root, as stated in section 3.1 of RFC 1035. While not necessary\n+    # for most day to day usage of hostnames, when adding FQDNs to a DNS zone editor, this\n+    # full stop is often mandatory. Without a full stop, the entered name is often seen as\n+    # relative to the DNS zone origin, which could lead to entries for, e.g.:\n+    # _acme-challenge.example.com.example.com. For users unaware of this subtle detail,\n+    # including the trailing full stop in the DNS instructions below might avert this issue.\n     _DNS_INSTRUCTIONS = \"\"\"\\\n-Please deploy a DNS TXT record under the name\n-{domain} with the following value:\n+Please deploy a DNS TXT record under the name:\n \n-{validation}\n+{domain}.\n+\n+with the following value:\n \n-Before continuing, verify the record is deployed.\"\"\"\n+{validation}\n+\"\"\"\n+    _DNS_VERIFY_INSTRUCTIONS = \"\"\"\n+Before continuing, verify the TXT record has been deployed. Depending on the DNS \n+provider, this may take some time, from a few seconds to multiple minutes. You can\n+check if it has finished deploying with aid of online tools, such as the Google\n+Admin Toolbox: https://toolbox.googleapps.com/apps/dig/#TXT/{domain}.\n+Look for one or more bolded line(s) below the line ';ANSWER'. It should show the\n+value(s) you've just added.\n+\"\"\"\n     _HTTP_INSTRUCTIONS = \"\"\"\\\n Create a file containing just this data:\n \n@@ -114,11 +131,15 @@\n \n     def perform(self, achalls):  # pylint: disable=missing-function-docstring\n         responses = []\n-        for achall in achalls:\n+        last_dns_achall = 0\n+        for i, achall in enumerate(achalls):\n+            if isinstance(achall.chall, challenges.DNS01):\n+                last_dns_achall = i\n+        for i, achall in enumerate(achalls):\n             if self.conf('auth-hook'):\n                 self._perform_achall_with_script(achall, achalls)\n             else:\n-                self._perform_achall_manually(achall)\n+                self._perform_achall_manually(achall, i == last_dns_achall)\n             responses.append(achall.response(achall.account_key))\n         return responses\n \n@@ -136,7 +157,7 @@\n         env['CERTBOT_AUTH_OUTPUT'] = out.strip()\n         self.env[achall] = env\n \n-    def _perform_achall_manually(self, achall):\n+    def _perform_achall_manually(self, achall, last_dns_achall=False):\n         validation = achall.validation(achall.account_key)\n         if isinstance(achall.chall, challenges.HTTP01):\n             msg = self._HTTP_INSTRUCTIONS.format(\n@@ -152,7 +173,15 @@\n             if self.subsequent_dns_challenge:\n                 # 2nd or later dns-01 challenge\n                 msg += self._SUBSEQUENT_DNS_CHALLENGE_INSTRUCTIONS\n+            elif self.subsequent_any_challenge:\n+                # 1st dns-01 challenge, but 2nd or later *any* challenge, so\n+                # instruct user not to remove any previous http-01 challenge\n+                msg += self._SUBSEQUENT_CHALLENGE_INSTRUCTIONS\n             self.subsequent_dns_challenge = True\n+            if last_dns_achall:\n+                # last dns-01 challenge\n+                msg += self._DNS_VERIFY_INSTRUCTIONS.format(\n+                    domain=achall.validation_domain_name(achall.domain))\n         elif self.subsequent_any_challenge:\n             # 2nd or later challenge of another type\n             msg += self._SUBSEQUENT_CHALLENGE_INSTRUCTIONS\n", "issue": "manual: when not using hooks, tell the user they need to wait for DNS propagation\nOne of the usability things I came across in #8514 was that it's not terribly obvious when setting up TXT records manually that with some DNS hosts, you need to wait a while. \r\n\r\nhttps://github.com/certbot/certbot/blob/adb7e5e62f4c96be678c5af3e55a82ddcc717590/certbot/certbot/_internal/plugins/manual.py#L45-L51\r\n\r\nThis bit of vague text could be rewritten in order to convey the advice more directly: \r\n\r\n> Before continuing, verify the record is deployed\r\n\r\nI think it would be ideal if we only showed this on the very last DNS challenge, but I'm not sure whether the logic of Certbot will permit that. I don't think it's a big deal to show it every time. \n", "before_files": [{"content": "\"\"\"Manual authenticator plugin\"\"\"\nfrom typing import Dict\n\nimport zope.component\nimport zope.interface\n\nfrom acme import challenges\nfrom certbot import achallenges\nfrom certbot import errors\nfrom certbot import interfaces\nfrom certbot import reverter\nfrom certbot import util\nfrom certbot._internal import hooks\nfrom certbot.compat import misc\nfrom certbot.compat import os\nfrom certbot.plugins import common\n\n\n@zope.interface.implementer(interfaces.IAuthenticator)\n@zope.interface.provider(interfaces.IPluginFactory)\nclass Authenticator(common.Plugin):\n    \"\"\"Manual authenticator\n\n    This plugin allows the user to perform the domain validation\n    challenge(s) themselves. This either be done manually by the user or\n    through shell scripts provided to Certbot.\n\n    \"\"\"\n\n    description = 'Manual configuration or run your own shell scripts'\n    hidden = True\n    long_description = (\n        'Authenticate through manual configuration or custom shell scripts. '\n        'When using shell scripts, an authenticator script must be provided. '\n        'The environment variables available to this script depend on the '\n        'type of challenge. $CERTBOT_DOMAIN will always contain the domain '\n        'being authenticated. For HTTP-01 and DNS-01, $CERTBOT_VALIDATION '\n        'is the validation string, and $CERTBOT_TOKEN is the filename of the '\n        'resource requested when performing an HTTP-01 challenge. An additional '\n        'cleanup script can also be provided and can use the additional variable '\n        '$CERTBOT_AUTH_OUTPUT which contains the stdout output from the auth script. '\n        'For both authenticator and cleanup script, on HTTP-01 and DNS-01 challenges, '\n        '$CERTBOT_REMAINING_CHALLENGES will be equal to the number of challenges that '\n        'remain after the current one, and $CERTBOT_ALL_DOMAINS contains a comma-separated '\n        'list of all domains that are challenged for the current certificate.')\n    _DNS_INSTRUCTIONS = \"\"\"\\\nPlease deploy a DNS TXT record under the name\n{domain} with the following value:\n\n{validation}\n\nBefore continuing, verify the record is deployed.\"\"\"\n    _HTTP_INSTRUCTIONS = \"\"\"\\\nCreate a file containing just this data:\n\n{validation}\n\nAnd make it available on your web server at this URL:\n\n{uri}\n\"\"\"\n    _SUBSEQUENT_CHALLENGE_INSTRUCTIONS = \"\"\"\n(This must be set up in addition to the previous challenges; do not remove,\nreplace, or undo the previous challenge tasks yet.)\n\"\"\"\n    _SUBSEQUENT_DNS_CHALLENGE_INSTRUCTIONS = \"\"\"\n(This must be set up in addition to the previous challenges; do not remove,\nreplace, or undo the previous challenge tasks yet. Note that you might be\nasked to create multiple distinct TXT records with the same name. This is\npermitted by DNS standards.)\n\"\"\"\n\n    def __init__(self, *args, **kwargs):\n        super(Authenticator, self).__init__(*args, **kwargs)\n        self.reverter = reverter.Reverter(self.config)\n        self.reverter.recovery_routine()\n        self.env: Dict[achallenges.KeyAuthorizationAnnotatedChallenge, Dict[str, str]] = {}\n        self.subsequent_dns_challenge = False\n        self.subsequent_any_challenge = False\n\n    @classmethod\n    def add_parser_arguments(cls, add):\n        add('auth-hook',\n            help='Path or command to execute for the authentication script')\n        add('cleanup-hook',\n            help='Path or command to execute for the cleanup script')\n        util.add_deprecated_argument(add, 'public-ip-logging-ok', 0)\n\n    def prepare(self):  # pylint: disable=missing-function-docstring\n        if self.config.noninteractive_mode and not self.conf('auth-hook'):\n            raise errors.PluginError(\n                'An authentication script must be provided with --{0} when '\n                'using the manual plugin non-interactively.'.format(\n                    self.option_name('auth-hook')))\n        self._validate_hooks()\n\n    def _validate_hooks(self):\n        if self.config.validate_hooks:\n            for name in ('auth-hook', 'cleanup-hook'):\n                hook = self.conf(name)\n                if hook is not None:\n                    hook_prefix = self.option_name(name)[:-len('-hook')]\n                    hooks.validate_hook(hook, hook_prefix)\n\n    def more_info(self):  # pylint: disable=missing-function-docstring\n        return (\n            'This plugin allows the user to customize setup for domain '\n            'validation challenges either through shell scripts provided by '\n            'the user or by performing the setup manually.')\n\n    def get_chall_pref(self, domain):\n        # pylint: disable=unused-argument,missing-function-docstring\n        return [challenges.HTTP01, challenges.DNS01]\n\n    def perform(self, achalls):  # pylint: disable=missing-function-docstring\n        responses = []\n        for achall in achalls:\n            if self.conf('auth-hook'):\n                self._perform_achall_with_script(achall, achalls)\n            else:\n                self._perform_achall_manually(achall)\n            responses.append(achall.response(achall.account_key))\n        return responses\n\n    def _perform_achall_with_script(self, achall, achalls):\n        env = dict(CERTBOT_DOMAIN=achall.domain,\n                   CERTBOT_VALIDATION=achall.validation(achall.account_key),\n                   CERTBOT_ALL_DOMAINS=','.join(one_achall.domain for one_achall in achalls),\n                   CERTBOT_REMAINING_CHALLENGES=str(len(achalls) - achalls.index(achall) - 1))\n        if isinstance(achall.chall, challenges.HTTP01):\n            env['CERTBOT_TOKEN'] = achall.chall.encode('token')\n        else:\n            os.environ.pop('CERTBOT_TOKEN', None)\n        os.environ.update(env)\n        _, out = self._execute_hook('auth-hook')\n        env['CERTBOT_AUTH_OUTPUT'] = out.strip()\n        self.env[achall] = env\n\n    def _perform_achall_manually(self, achall):\n        validation = achall.validation(achall.account_key)\n        if isinstance(achall.chall, challenges.HTTP01):\n            msg = self._HTTP_INSTRUCTIONS.format(\n                achall=achall, encoded_token=achall.chall.encode('token'),\n                port=self.config.http01_port,\n                uri=achall.chall.uri(achall.domain), validation=validation)\n        else:\n            assert isinstance(achall.chall, challenges.DNS01)\n            msg = self._DNS_INSTRUCTIONS.format(\n                domain=achall.validation_domain_name(achall.domain),\n                validation=validation)\n        if isinstance(achall.chall, challenges.DNS01):\n            if self.subsequent_dns_challenge:\n                # 2nd or later dns-01 challenge\n                msg += self._SUBSEQUENT_DNS_CHALLENGE_INSTRUCTIONS\n            self.subsequent_dns_challenge = True\n        elif self.subsequent_any_challenge:\n            # 2nd or later challenge of another type\n            msg += self._SUBSEQUENT_CHALLENGE_INSTRUCTIONS\n        display = zope.component.getUtility(interfaces.IDisplay)\n        display.notification(msg, wrap=False, force_interactive=True)\n        self.subsequent_any_challenge = True\n\n    def cleanup(self, achalls):  # pylint: disable=missing-function-docstring\n        if self.conf('cleanup-hook'):\n            for achall in achalls:\n                env = self.env.pop(achall)\n                if 'CERTBOT_TOKEN' not in env:\n                    os.environ.pop('CERTBOT_TOKEN', None)\n                os.environ.update(env)\n                self._execute_hook('cleanup-hook')\n        self.reverter.recovery_routine()\n\n    def _execute_hook(self, hook_name):\n        return misc.execute_command(self.option_name(hook_name), self.conf(hook_name),\n                                    env=util.env_no_snap_for_external_calls())\n", "path": "certbot/certbot/_internal/plugins/manual.py"}], "after_files": [{"content": "\"\"\"Manual authenticator plugin\"\"\"\nfrom typing import Dict\n\nimport zope.component\nimport zope.interface\n\nfrom acme import challenges\nfrom certbot import achallenges\nfrom certbot import errors\nfrom certbot import interfaces\nfrom certbot import reverter\nfrom certbot import util\nfrom certbot._internal import hooks\nfrom certbot.compat import misc\nfrom certbot.compat import os\nfrom certbot.plugins import common\n\n\n@zope.interface.implementer(interfaces.IAuthenticator)\n@zope.interface.provider(interfaces.IPluginFactory)\nclass Authenticator(common.Plugin):\n    \"\"\"Manual authenticator\n\n    This plugin allows the user to perform the domain validation\n    challenge(s) themselves. This either be done manually by the user or\n    through shell scripts provided to Certbot.\n\n    \"\"\"\n\n    description = 'Manual configuration or run your own shell scripts'\n    hidden = True\n    long_description = (\n        'Authenticate through manual configuration or custom shell scripts. '\n        'When using shell scripts, an authenticator script must be provided. '\n        'The environment variables available to this script depend on the '\n        'type of challenge. $CERTBOT_DOMAIN will always contain the domain '\n        'being authenticated. For HTTP-01 and DNS-01, $CERTBOT_VALIDATION '\n        'is the validation string, and $CERTBOT_TOKEN is the filename of the '\n        'resource requested when performing an HTTP-01 challenge. An additional '\n        'cleanup script can also be provided and can use the additional variable '\n        '$CERTBOT_AUTH_OUTPUT which contains the stdout output from the auth script. '\n        'For both authenticator and cleanup script, on HTTP-01 and DNS-01 challenges, '\n        '$CERTBOT_REMAINING_CHALLENGES will be equal to the number of challenges that '\n        'remain after the current one, and $CERTBOT_ALL_DOMAINS contains a comma-separated '\n        'list of all domains that are challenged for the current certificate.')\n    # Include the full stop at the end of the FQDN in the instructions below for the null\n    # label of the DNS root, as stated in section 3.1 of RFC 1035. While not necessary\n    # for most day to day usage of hostnames, when adding FQDNs to a DNS zone editor, this\n    # full stop is often mandatory. Without a full stop, the entered name is often seen as\n    # relative to the DNS zone origin, which could lead to entries for, e.g.:\n    # _acme-challenge.example.com.example.com. For users unaware of this subtle detail,\n    # including the trailing full stop in the DNS instructions below might avert this issue.\n    _DNS_INSTRUCTIONS = \"\"\"\\\nPlease deploy a DNS TXT record under the name:\n\n{domain}.\n\nwith the following value:\n\n{validation}\n\"\"\"\n    _DNS_VERIFY_INSTRUCTIONS = \"\"\"\nBefore continuing, verify the TXT record has been deployed. Depending on the DNS \nprovider, this may take some time, from a few seconds to multiple minutes. You can\ncheck if it has finished deploying with aid of online tools, such as the Google\nAdmin Toolbox: https://toolbox.googleapps.com/apps/dig/#TXT/{domain}.\nLook for one or more bolded line(s) below the line ';ANSWER'. It should show the\nvalue(s) you've just added.\n\"\"\"\n    _HTTP_INSTRUCTIONS = \"\"\"\\\nCreate a file containing just this data:\n\n{validation}\n\nAnd make it available on your web server at this URL:\n\n{uri}\n\"\"\"\n    _SUBSEQUENT_CHALLENGE_INSTRUCTIONS = \"\"\"\n(This must be set up in addition to the previous challenges; do not remove,\nreplace, or undo the previous challenge tasks yet.)\n\"\"\"\n    _SUBSEQUENT_DNS_CHALLENGE_INSTRUCTIONS = \"\"\"\n(This must be set up in addition to the previous challenges; do not remove,\nreplace, or undo the previous challenge tasks yet. Note that you might be\nasked to create multiple distinct TXT records with the same name. This is\npermitted by DNS standards.)\n\"\"\"\n\n    def __init__(self, *args, **kwargs):\n        super(Authenticator, self).__init__(*args, **kwargs)\n        self.reverter = reverter.Reverter(self.config)\n        self.reverter.recovery_routine()\n        self.env: Dict[achallenges.KeyAuthorizationAnnotatedChallenge, Dict[str, str]] = {}\n        self.subsequent_dns_challenge = False\n        self.subsequent_any_challenge = False\n\n    @classmethod\n    def add_parser_arguments(cls, add):\n        add('auth-hook',\n            help='Path or command to execute for the authentication script')\n        add('cleanup-hook',\n            help='Path or command to execute for the cleanup script')\n        util.add_deprecated_argument(add, 'public-ip-logging-ok', 0)\n\n    def prepare(self):  # pylint: disable=missing-function-docstring\n        if self.config.noninteractive_mode and not self.conf('auth-hook'):\n            raise errors.PluginError(\n                'An authentication script must be provided with --{0} when '\n                'using the manual plugin non-interactively.'.format(\n                    self.option_name('auth-hook')))\n        self._validate_hooks()\n\n    def _validate_hooks(self):\n        if self.config.validate_hooks:\n            for name in ('auth-hook', 'cleanup-hook'):\n                hook = self.conf(name)\n                if hook is not None:\n                    hook_prefix = self.option_name(name)[:-len('-hook')]\n                    hooks.validate_hook(hook, hook_prefix)\n\n    def more_info(self):  # pylint: disable=missing-function-docstring\n        return (\n            'This plugin allows the user to customize setup for domain '\n            'validation challenges either through shell scripts provided by '\n            'the user or by performing the setup manually.')\n\n    def get_chall_pref(self, domain):\n        # pylint: disable=unused-argument,missing-function-docstring\n        return [challenges.HTTP01, challenges.DNS01]\n\n    def perform(self, achalls):  # pylint: disable=missing-function-docstring\n        responses = []\n        last_dns_achall = 0\n        for i, achall in enumerate(achalls):\n            if isinstance(achall.chall, challenges.DNS01):\n                last_dns_achall = i\n        for i, achall in enumerate(achalls):\n            if self.conf('auth-hook'):\n                self._perform_achall_with_script(achall, achalls)\n            else:\n                self._perform_achall_manually(achall, i == last_dns_achall)\n            responses.append(achall.response(achall.account_key))\n        return responses\n\n    def _perform_achall_with_script(self, achall, achalls):\n        env = dict(CERTBOT_DOMAIN=achall.domain,\n                   CERTBOT_VALIDATION=achall.validation(achall.account_key),\n                   CERTBOT_ALL_DOMAINS=','.join(one_achall.domain for one_achall in achalls),\n                   CERTBOT_REMAINING_CHALLENGES=str(len(achalls) - achalls.index(achall) - 1))\n        if isinstance(achall.chall, challenges.HTTP01):\n            env['CERTBOT_TOKEN'] = achall.chall.encode('token')\n        else:\n            os.environ.pop('CERTBOT_TOKEN', None)\n        os.environ.update(env)\n        _, out = self._execute_hook('auth-hook')\n        env['CERTBOT_AUTH_OUTPUT'] = out.strip()\n        self.env[achall] = env\n\n    def _perform_achall_manually(self, achall, last_dns_achall=False):\n        validation = achall.validation(achall.account_key)\n        if isinstance(achall.chall, challenges.HTTP01):\n            msg = self._HTTP_INSTRUCTIONS.format(\n                achall=achall, encoded_token=achall.chall.encode('token'),\n                port=self.config.http01_port,\n                uri=achall.chall.uri(achall.domain), validation=validation)\n        else:\n            assert isinstance(achall.chall, challenges.DNS01)\n            msg = self._DNS_INSTRUCTIONS.format(\n                domain=achall.validation_domain_name(achall.domain),\n                validation=validation)\n        if isinstance(achall.chall, challenges.DNS01):\n            if self.subsequent_dns_challenge:\n                # 2nd or later dns-01 challenge\n                msg += self._SUBSEQUENT_DNS_CHALLENGE_INSTRUCTIONS\n            elif self.subsequent_any_challenge:\n                # 1st dns-01 challenge, but 2nd or later *any* challenge, so\n                # instruct user not to remove any previous http-01 challenge\n                msg += self._SUBSEQUENT_CHALLENGE_INSTRUCTIONS\n            self.subsequent_dns_challenge = True\n            if last_dns_achall:\n                # last dns-01 challenge\n                msg += self._DNS_VERIFY_INSTRUCTIONS.format(\n                    domain=achall.validation_domain_name(achall.domain))\n        elif self.subsequent_any_challenge:\n            # 2nd or later challenge of another type\n            msg += self._SUBSEQUENT_CHALLENGE_INSTRUCTIONS\n        display = zope.component.getUtility(interfaces.IDisplay)\n        display.notification(msg, wrap=False, force_interactive=True)\n        self.subsequent_any_challenge = True\n\n    def cleanup(self, achalls):  # pylint: disable=missing-function-docstring\n        if self.conf('cleanup-hook'):\n            for achall in achalls:\n                env = self.env.pop(achall)\n                if 'CERTBOT_TOKEN' not in env:\n                    os.environ.pop('CERTBOT_TOKEN', None)\n                os.environ.update(env)\n                self._execute_hook('cleanup-hook')\n        self.reverter.recovery_routine()\n\n    def _execute_hook(self, hook_name):\n        return misc.execute_command(self.option_name(hook_name), self.conf(hook_name),\n                                    env=util.env_no_snap_for_external_calls())\n", "path": "certbot/certbot/_internal/plugins/manual.py"}]}