We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Feature request: Add option to know what is the last domain passed to dns hook script
Hi,

certbot doesn't provide an official hook to work with the API of my DNS provider so I'm using my own script. Well, we know every time we create/modify a record on our DNS provider it needs to propagate to all the authoritative DNS servers, in my case it takes 1 minute and 30 seconds approx., right now, as far as I know, there is no option to know what is the last domain that is being issued so to be bullet proof, every time the script creates the _acme_challenge TXT record, I wait 1:30 minutes, well I use the API to know when the propagation is finished but as I said, it takes 1:30 minutes. If I want to issue a certificate with 100 domains it will take approx. 2 hours and 30 minutes to complete the request. If I had a way to know what is the last domain I could write all the TXT records and when the last domain is created, I could add the check to know when the propagation has finished (or just a simple sleep...) so it takes only 1:30 minutes instead of 2 hours and 30 minutes.

It would be great if certbot provided a variable like `CERTBOT_LAST_DOMAIN=true` or something like that so the script could have the logic to sleep the right amount of time to let finish the DNS  propagation or to perform the needed checks only when there are no more domains to add to our DNS provider.

As a workaround I pass the last domain to my script:

`./certbot-auto certonly --manual --preferred-challenges=dns --manual-auth-hook "/path/to/hook/dnsapi.sh yyy.domain.tld" --manual-cleanup-hook /path/to/hook/dnsapi-cleanup.sh -d zzz.domain.tld,aaa.domain.tld,yyy.domain.tld`

and it works perfectly but would be great to have this feature added directly to certbot.

Thank you in advance.

Cheers,
sahsanu

--- 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 import zope.component
3 import zope.interface
4 
5 from acme import challenges
6 from acme.magic_typing import Dict
7 from certbot import achallenges  # pylint: disable=unused-import
8 from certbot import errors
9 from certbot import interfaces
10 from certbot import reverter
11 from certbot._internal import hooks
12 from certbot.compat import os
13 from certbot.plugins import common
14 
15 
16 @zope.interface.implementer(interfaces.IAuthenticator)
17 @zope.interface.provider(interfaces.IPluginFactory)
18 class Authenticator(common.Plugin):
19     """Manual authenticator
20 
21     This plugin allows the user to perform the domain validation
22     challenge(s) themselves. This either be done manually by the user or
23     through shell scripts provided to Certbot.
24 
25     """
26 
27     description = 'Manual configuration or run your own shell scripts'
28     hidden = True
29     long_description = (
30         'Authenticate through manual configuration or custom shell scripts. '
31         'When using shell scripts, an authenticator script must be provided. '
32         'The environment variables available to this script depend on the '
33         'type of challenge. $CERTBOT_DOMAIN will always contain the domain '
34         'being authenticated. For HTTP-01 and DNS-01, $CERTBOT_VALIDATION '
35         'is the validation string, and $CERTBOT_TOKEN is the filename of the '
36         'resource requested when performing an HTTP-01 challenge. An additional '
37         'cleanup script can also be provided and can use the additional variable '
38         '$CERTBOT_AUTH_OUTPUT which contains the stdout output from the auth script.')
39     _DNS_INSTRUCTIONS = """\
40 Please deploy a DNS TXT record under the name
41 {domain} with the following value:
42 
43 {validation}
44 
45 Before continuing, verify the record is deployed."""
46     _HTTP_INSTRUCTIONS = """\
47 Create a file containing just this data:
48 
49 {validation}
50 
51 And make it available on your web server at this URL:
52 
53 {uri}
54 """
55     _SUBSEQUENT_CHALLENGE_INSTRUCTIONS = """
56 (This must be set up in addition to the previous challenges; do not remove,
57 replace, or undo the previous challenge tasks yet.)
58 """
59     _SUBSEQUENT_DNS_CHALLENGE_INSTRUCTIONS = """
60 (This must be set up in addition to the previous challenges; do not remove,
61 replace, or undo the previous challenge tasks yet. Note that you might be
62 asked to create multiple distinct TXT records with the same name. This is
63 permitted by DNS standards.)
64 """
65 
66     def __init__(self, *args, **kwargs):
67         super(Authenticator, self).__init__(*args, **kwargs)
68         self.reverter = reverter.Reverter(self.config)
69         self.reverter.recovery_routine()
70         self.env = dict() \
71         # type: Dict[achallenges.KeyAuthorizationAnnotatedChallenge, Dict[str, str]]
72         self.subsequent_dns_challenge = False
73         self.subsequent_any_challenge = False
74 
75     @classmethod
76     def add_parser_arguments(cls, add):
77         add('auth-hook',
78             help='Path or command to execute for the authentication script')
79         add('cleanup-hook',
80             help='Path or command to execute for the cleanup script')
81         add('public-ip-logging-ok', action='store_true',
82             help='Automatically allows public IP logging (default: Ask)')
83 
84     def prepare(self):  # pylint: disable=missing-function-docstring
85         if self.config.noninteractive_mode and not self.conf('auth-hook'):
86             raise errors.PluginError(
87                 'An authentication script must be provided with --{0} when '
88                 'using the manual plugin non-interactively.'.format(
89                     self.option_name('auth-hook')))
90         self._validate_hooks()
91 
92     def _validate_hooks(self):
93         if self.config.validate_hooks:
94             for name in ('auth-hook', 'cleanup-hook'):
95                 hook = self.conf(name)
96                 if hook is not None:
97                     hook_prefix = self.option_name(name)[:-len('-hook')]
98                     hooks.validate_hook(hook, hook_prefix)
99 
100     def more_info(self):  # pylint: disable=missing-function-docstring
101         return (
102             'This plugin allows the user to customize setup for domain '
103             'validation challenges either through shell scripts provided by '
104             'the user or by performing the setup manually.')
105 
106     def get_chall_pref(self, domain):
107         # pylint: disable=unused-argument,missing-function-docstring
108         return [challenges.HTTP01, challenges.DNS01]
109 
110     def perform(self, achalls):  # pylint: disable=missing-function-docstring
111         self._verify_ip_logging_ok()
112         if self.conf('auth-hook'):
113             perform_achall = self._perform_achall_with_script
114         else:
115             perform_achall = self._perform_achall_manually
116 
117         responses = []
118         for achall in achalls:
119             perform_achall(achall)
120             responses.append(achall.response(achall.account_key))
121         return responses
122 
123     def _verify_ip_logging_ok(self):
124         if not self.conf('public-ip-logging-ok'):
125             cli_flag = '--{0}'.format(self.option_name('public-ip-logging-ok'))
126             msg = ('NOTE: The IP of this machine will be publicly logged as '
127                    "having requested this certificate. If you're running "
128                    'certbot in manual mode on a machine that is not your '
129                    "server, please ensure you're okay with that.\n\n"
130                    'Are you OK with your IP being logged?')
131             display = zope.component.getUtility(interfaces.IDisplay)
132             if display.yesno(msg, cli_flag=cli_flag, force_interactive=True):
133                 setattr(self.config, self.dest('public-ip-logging-ok'), True)
134             else:
135                 raise errors.PluginError('Must agree to IP logging to proceed')
136 
137     def _perform_achall_with_script(self, achall):
138         env = dict(CERTBOT_DOMAIN=achall.domain,
139                    CERTBOT_VALIDATION=achall.validation(achall.account_key))
140         if isinstance(achall.chall, challenges.HTTP01):
141             env['CERTBOT_TOKEN'] = achall.chall.encode('token')
142         else:
143             os.environ.pop('CERTBOT_TOKEN', None)
144         os.environ.update(env)
145         _, out = self._execute_hook('auth-hook')
146         env['CERTBOT_AUTH_OUTPUT'] = out.strip()
147         self.env[achall] = env
148 
149     def _perform_achall_manually(self, achall):
150         validation = achall.validation(achall.account_key)
151         if isinstance(achall.chall, challenges.HTTP01):
152             msg = self._HTTP_INSTRUCTIONS.format(
153                 achall=achall, encoded_token=achall.chall.encode('token'),
154                 port=self.config.http01_port,
155                 uri=achall.chall.uri(achall.domain), validation=validation)
156         else:
157             assert isinstance(achall.chall, challenges.DNS01)
158             msg = self._DNS_INSTRUCTIONS.format(
159                 domain=achall.validation_domain_name(achall.domain),
160                 validation=validation)
161         if isinstance(achall.chall, challenges.DNS01):
162             if self.subsequent_dns_challenge:
163                 # 2nd or later dns-01 challenge
164                 msg += self._SUBSEQUENT_DNS_CHALLENGE_INSTRUCTIONS
165             self.subsequent_dns_challenge = True
166         elif self.subsequent_any_challenge:
167             # 2nd or later challenge of another type
168             msg += self._SUBSEQUENT_CHALLENGE_INSTRUCTIONS
169         display = zope.component.getUtility(interfaces.IDisplay)
170         display.notification(msg, wrap=False, force_interactive=True)
171         self.subsequent_any_challenge = True
172 
173     def cleanup(self, achalls):  # pylint: disable=missing-function-docstring
174         if self.conf('cleanup-hook'):
175             for achall in achalls:
176                 env = self.env.pop(achall)
177                 if 'CERTBOT_TOKEN' not in env:
178                     os.environ.pop('CERTBOT_TOKEN', None)
179                 os.environ.update(env)
180                 self._execute_hook('cleanup-hook')
181         self.reverter.recovery_routine()
182 
183     def _execute_hook(self, hook_name):
184         return hooks.execute(self.option_name(hook_name), self.conf(hook_name))
185
```
--- 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
@@ -35,7 +35,11 @@
         'is the validation string, and $CERTBOT_TOKEN is the filename of the '
         'resource requested when performing an HTTP-01 challenge. An additional '
         'cleanup script can also be provided and can use the additional variable '
-        '$CERTBOT_AUTH_OUTPUT which contains the stdout output from the auth script.')
+        '$CERTBOT_AUTH_OUTPUT which contains the stdout output from the auth script.'
+        'For both authenticator and cleanup script, on HTTP-01 and DNS-01 challenges,'
+        '$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.')
     _DNS_INSTRUCTIONS = """\
 Please deploy a DNS TXT record under the name
 {domain} with the following value:
@@ -109,14 +113,13 @@
 
     def perform(self, achalls):  # pylint: disable=missing-function-docstring
         self._verify_ip_logging_ok()
-        if self.conf('auth-hook'):
-            perform_achall = self._perform_achall_with_script
-        else:
-            perform_achall = self._perform_achall_manually
 
         responses = []
         for achall in achalls:
-            perform_achall(achall)
+            if self.conf('auth-hook'):
+                self._perform_achall_with_script(achall, achalls)
+            else:
+                self._perform_achall_manually(achall)
             responses.append(achall.response(achall.account_key))
         return responses
 
@@ -134,9 +137,11 @@
             else:
                 raise errors.PluginError('Must agree to IP logging to proceed')
 
-    def _perform_achall_with_script(self, achall):
+    def _perform_achall_with_script(self, achall, achalls):
         env = dict(CERTBOT_DOMAIN=achall.domain,
-                   CERTBOT_VALIDATION=achall.validation(achall.account_key))
+                   CERTBOT_VALIDATION=achall.validation(achall.account_key),
+                   CERTBOT_ALL_DOMAINS=','.join(one_achall.domain for one_achall in achalls),
+                   CERTBOT_REMAINING_CHALLENGES=str(len(achalls) - achalls.index(achall) - 1))
         if isinstance(achall.chall, challenges.HTTP01):
             env['CERTBOT_TOKEN'] = achall.chall.encode('token')
         else:

{"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@@ -35,7 +35,11 @@\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+        '$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 = \"\"\"\\\n Please deploy a DNS TXT record under the name\n {domain} with the following value:\n@@ -109,14 +113,13 @@\n \n     def perform(self, achalls):  # pylint: disable=missing-function-docstring\n         self._verify_ip_logging_ok()\n-        if self.conf('auth-hook'):\n-            perform_achall = self._perform_achall_with_script\n-        else:\n-            perform_achall = self._perform_achall_manually\n \n         responses = []\n         for achall in achalls:\n-            perform_achall(achall)\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@@ -134,9 +137,11 @@\n             else:\n                 raise errors.PluginError('Must agree to IP logging to proceed')\n \n-    def _perform_achall_with_script(self, achall):\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_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", "issue": "Feature request: Add option to know what is the last domain passed to dns hook script\nHi,\r\n\r\ncertbot doesn't provide an official hook to work with the API of my DNS provider so I'm using my own script. Well, we know every time we create/modify a record on our DNS provider it needs to propagate to all the authoritative DNS servers, in my case it takes 1 minute and 30 seconds approx., right now, as far as I know, there is no option to know what is the last domain that is being issued so to be bullet proof, every time the script creates the _acme_challenge TXT record, I wait 1:30 minutes, well I use the API to know when the propagation is finished but as I said, it takes 1:30 minutes. If I want to issue a certificate with 100 domains it will take approx. 2 hours and 30 minutes to complete the request. If I had a way to know what is the last domain I could write all the TXT records and when the last domain is created, I could add the check to know when the propagation has finished (or just a simple sleep...) so it takes only 1:30 minutes instead of 2 hours and 30 minutes.\r\n\r\nIt would be great if certbot provided a variable like `CERTBOT_LAST_DOMAIN=true` or something like that so the script could have the logic to sleep the right amount of time to let finish the DNS  propagation or to perform the needed checks only when there are no more domains to add to our DNS provider.\r\n\r\nAs a workaround I pass the last domain to my script:\r\n\r\n`./certbot-auto certonly --manual --preferred-challenges=dns --manual-auth-hook \"/path/to/hook/dnsapi.sh yyy.domain.tld\" --manual-cleanup-hook /path/to/hook/dnsapi-cleanup.sh -d zzz.domain.tld,aaa.domain.tld,yyy.domain.tld`\r\n\r\nand it works perfectly but would be great to have this feature added directly to certbot.\r\n\r\nThank you in advance.\r\n\r\nCheers,\r\nsahsanu\n", "before_files": [{"content": "\"\"\"Manual authenticator plugin\"\"\"\nimport zope.component\nimport zope.interface\n\nfrom acme import challenges\nfrom acme.magic_typing import Dict\nfrom certbot import achallenges  # pylint: disable=unused-import\nfrom certbot import errors\nfrom certbot import interfaces\nfrom certbot import reverter\nfrom certbot._internal import hooks\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    _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() \\\n        # type: 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        add('public-ip-logging-ok', action='store_true',\n            help='Automatically allows public IP logging (default: Ask)')\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        self._verify_ip_logging_ok()\n        if self.conf('auth-hook'):\n            perform_achall = self._perform_achall_with_script\n        else:\n            perform_achall = self._perform_achall_manually\n\n        responses = []\n        for achall in achalls:\n            perform_achall(achall)\n            responses.append(achall.response(achall.account_key))\n        return responses\n\n    def _verify_ip_logging_ok(self):\n        if not self.conf('public-ip-logging-ok'):\n            cli_flag = '--{0}'.format(self.option_name('public-ip-logging-ok'))\n            msg = ('NOTE: The IP of this machine will be publicly logged as '\n                   \"having requested this certificate. If you're running \"\n                   'certbot in manual mode on a machine that is not your '\n                   \"server, please ensure you're okay with that.\\n\\n\"\n                   'Are you OK with your IP being logged?')\n            display = zope.component.getUtility(interfaces.IDisplay)\n            if display.yesno(msg, cli_flag=cli_flag, force_interactive=True):\n                setattr(self.config, self.dest('public-ip-logging-ok'), True)\n            else:\n                raise errors.PluginError('Must agree to IP logging to proceed')\n\n    def _perform_achall_with_script(self, achall):\n        env = dict(CERTBOT_DOMAIN=achall.domain,\n                   CERTBOT_VALIDATION=achall.validation(achall.account_key))\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 hooks.execute(self.option_name(hook_name), self.conf(hook_name))\n", "path": "certbot/certbot/_internal/plugins/manual.py"}], "after_files": [{"content": "\"\"\"Manual authenticator plugin\"\"\"\nimport zope.component\nimport zope.interface\n\nfrom acme import challenges\nfrom acme.magic_typing import Dict\nfrom certbot import achallenges  # pylint: disable=unused-import\nfrom certbot import errors\nfrom certbot import interfaces\nfrom certbot import reverter\nfrom certbot._internal import hooks\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() \\\n        # type: 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        add('public-ip-logging-ok', action='store_true',\n            help='Automatically allows public IP logging (default: Ask)')\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        self._verify_ip_logging_ok()\n\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 _verify_ip_logging_ok(self):\n        if not self.conf('public-ip-logging-ok'):\n            cli_flag = '--{0}'.format(self.option_name('public-ip-logging-ok'))\n            msg = ('NOTE: The IP of this machine will be publicly logged as '\n                   \"having requested this certificate. If you're running \"\n                   'certbot in manual mode on a machine that is not your '\n                   \"server, please ensure you're okay with that.\\n\\n\"\n                   'Are you OK with your IP being logged?')\n            display = zope.component.getUtility(interfaces.IDisplay)\n            if display.yesno(msg, cli_flag=cli_flag, force_interactive=True):\n                setattr(self.config, self.dest('public-ip-logging-ok'), True)\n            else:\n                raise errors.PluginError('Must agree to IP logging to proceed')\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 hooks.execute(self.option_name(hook_name), self.conf(hook_name))\n", "path": "certbot/certbot/_internal/plugins/manual.py"}]}