mirror of
https://github.com/certbot/certbot.git
synced 2026-07-31 10:26:06 +02:00
delete old manual and script plugins
This commit is contained in:
+94
-201
@@ -1,56 +1,50 @@
|
||||
"""Manual plugin."""
|
||||
"""Manual authenticator plugin"""
|
||||
import os
|
||||
import logging
|
||||
import pipes
|
||||
import shutil
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
import six
|
||||
import zope.component
|
||||
import zope.interface
|
||||
|
||||
from acme import challenges
|
||||
from acme import errors as acme_errors
|
||||
|
||||
from certbot import errors
|
||||
from certbot import interfaces
|
||||
from certbot import errors
|
||||
from certbot import hooks
|
||||
from certbot.plugins import common
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@zope.interface.implementer(interfaces.IAuthenticator)
|
||||
@zope.interface.provider(interfaces.IPluginFactory)
|
||||
class Authenticator(common.Plugin):
|
||||
"""Manual Authenticator.
|
||||
"""Manual authenticator
|
||||
|
||||
This plugin requires user's manual intervention in setting up a HTTP
|
||||
server for solving http-01 challenges and thus does not need to be
|
||||
run as a privileged process. Alternatively shows instructions on how
|
||||
to use Python's built-in HTTP server.
|
||||
|
||||
.. todo:: Support for `~.challenges.TLSSNI01`.
|
||||
This plugin allows the user to perform the domain validation
|
||||
challenge(s) themselves. This can be done either be done manually
|
||||
by the user or through shell scripts provided to Certbot.
|
||||
|
||||
"""
|
||||
|
||||
description = 'Manual configuration or run your own shell scripts'
|
||||
hidden = True
|
||||
|
||||
description = "Manually configure an HTTP server"
|
||||
|
||||
MESSAGE_TEMPLATE = {
|
||||
"dns-01": """\
|
||||
long_description = (
|
||||
'Authenticate through manual configuration or custom shell scripts. '
|
||||
'When using shell scripts, an authenticator script must be provided. '
|
||||
'The environment variables available to this script are '
|
||||
'$CERTBOT_DOMAIN which contains the domain being authenticated, '
|
||||
'$CERTBOT_VALIDATION which is the validation string, and '
|
||||
'$CERTBOT_TOKEN which 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.')
|
||||
_DNS_INSTRUCTIONS = """\
|
||||
Please deploy a DNS TXT record under the name
|
||||
{domain} with the following value:
|
||||
|
||||
{validation}
|
||||
|
||||
Once this is deployed,
|
||||
""",
|
||||
"http-01": """\
|
||||
"""
|
||||
_HTTP_INSTRUCTIONS = """\
|
||||
Make sure your web server displays the following content at
|
||||
{uri} before continuing:
|
||||
|
||||
@@ -59,203 +53,102 @@ Make sure your web server displays the following content at
|
||||
If you don't have HTTP server configured, you can run the following
|
||||
command on the target server (as root):
|
||||
|
||||
{command}
|
||||
"""}
|
||||
|
||||
# a disclaimer about your current IP being transmitted to Let's Encrypt's servers.
|
||||
IP_DISCLAIMER = """\
|
||||
NOTE: The IP of this machine will be publicly logged as having requested this certificate. \
|
||||
If you're running certbot in manual mode on a machine that is not your server, \
|
||||
please ensure you're okay with that.
|
||||
|
||||
Are you OK with your IP being logged?
|
||||
"""
|
||||
|
||||
# "cd /tmp/certbot" makes sure user doesn't serve /root,
|
||||
# separate "public_html" ensures that cert.pem/key.pem are not
|
||||
# served and makes it more obvious that Python command will serve
|
||||
# anything recursively under the cwd
|
||||
|
||||
CMD_TEMPLATE = """\
|
||||
mkdir -p {root}/public_html/{achall.URI_ROOT_PATH}
|
||||
cd {root}/public_html
|
||||
mkdir -p /tmp/certbot/public_html/{achall.URI_ROOT_PATH}
|
||||
cd /tmp/certbot/public_html
|
||||
printf "%s" {validation} > {achall.URI_ROOT_PATH}/{encoded_token}
|
||||
# run only once per server:
|
||||
$(command -v python2 || command -v python2.7 || command -v python2.6) -c \\
|
||||
"import BaseHTTPServer, SimpleHTTPServer; \\
|
||||
s = BaseHTTPServer.HTTPServer(('', {port}), SimpleHTTPServer.SimpleHTTPRequestHandler); \\
|
||||
s.serve_forever()" """
|
||||
"""Command template."""
|
||||
s.serve_forever()"
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(Authenticator, self).__init__(*args, **kwargs)
|
||||
self._root = (tempfile.mkdtemp() if self.conf("test-mode")
|
||||
else "/tmp/certbot")
|
||||
self._httpd = None
|
||||
self.env = dict()
|
||||
|
||||
@classmethod
|
||||
def add_parser_arguments(cls, add):
|
||||
add("test-mode", action="store_true",
|
||||
help="Test mode. Executes the manual command in subprocess.")
|
||||
add("public-ip-logging-ok", action="store_true",
|
||||
help="Automatically allows public IP logging.")
|
||||
add('auth-script',
|
||||
help='Path or command to execute for the authentication script')
|
||||
add('cleanup-script',
|
||||
help='Path or command to execute for the cleanup script')
|
||||
add('public-ip-logging-ok', action='store_true',
|
||||
help='Automatically allows public IP logging')
|
||||
|
||||
def prepare(self): # pylint: disable=missing-docstring,no-self-use
|
||||
if self.config.noninteractive_mode and not self.conf("test-mode"):
|
||||
raise errors.PluginError("Running manual mode non-interactively is not supported")
|
||||
def prepare(self): # pylint: disable=missing-docstring
|
||||
if self.config.noninteractive_mode and not self.conf('auth-script'):
|
||||
raise errors.PluginError(
|
||||
'An authentication script must be provided with --{0} when '
|
||||
'using the manual plugin non-interactively.'.format(
|
||||
self.option_name('auth-script')))
|
||||
|
||||
def more_info(self): # pylint: disable=missing-docstring,no-self-use
|
||||
return ("This plugin requires user's manual intervention in setting "
|
||||
"up challenges to prove control of a domain and does not need "
|
||||
"to be run as a privileged process. When solving "
|
||||
"http-01 challenges, the user is responsible for setting up "
|
||||
"an HTTP server. Alternatively, instructions are shown on how "
|
||||
"to use Python's built-in HTTP server. The user is "
|
||||
"responsible for configuration of a domain's DNS when solving "
|
||||
"dns-01 challenges. The type of challenges used can be "
|
||||
"controlled through the --preferred-challenges flag.")
|
||||
return (
|
||||
'This plugin allows the user to customize setup for domain '
|
||||
'validation challenges either through shell scripts provided by '
|
||||
'the user or by performing the setup manually.')
|
||||
|
||||
def get_chall_pref(self, domain):
|
||||
# pylint: disable=missing-docstring,no-self-use,unused-argument
|
||||
return [challenges.HTTP01, challenges.DNS01]
|
||||
|
||||
def perform(self, achalls):
|
||||
# pylint: disable=missing-docstring
|
||||
self._get_ip_logging_permission()
|
||||
mapping = {"http-01": self._perform_http01_challenge,
|
||||
"dns-01": self._perform_dns01_challenge}
|
||||
def perform(self, achalls): # pylint: disable=missing-docstring
|
||||
self._verify_ip_logging_ok()
|
||||
|
||||
responses = []
|
||||
# TODO: group achalls by the same socket.gethostbyname(_ex)
|
||||
# and prompt only once per server (one "echo -n" per domain)
|
||||
for achall in achalls:
|
||||
responses.append(mapping[achall.typ](achall))
|
||||
response, validation = achall.response_and_validation()
|
||||
if self.conf('auth-script'):
|
||||
self._perform_achall_with_script(achall, validation)
|
||||
else:
|
||||
self._perform_achall_manually(achall, response, validation)
|
||||
responses.append(response)
|
||||
return responses
|
||||
|
||||
@classmethod
|
||||
def _test_mode_busy_wait(cls, port):
|
||||
while True:
|
||||
time.sleep(1)
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
try:
|
||||
sock.connect(("localhost", port))
|
||||
except socket.error: # pragma: no cover
|
||||
pass
|
||||
def _verify_ip_logging_ok(self):
|
||||
if not self.conf('public-ip-logging-ok'):
|
||||
cli_flag = self.option_name('public-ip-logging-ok')
|
||||
msg = ('NOTE: The IP of this machine will be publicly logged as '
|
||||
"having requested this certificate. If you're running "
|
||||
'certbot in manual mode on a machine that is not your '
|
||||
"server, please ensure you're okay with that.\n\n"
|
||||
'Are you OK with your IP being logged?')
|
||||
display = zope.component.getUtility(interfaces.IDisplay)
|
||||
if display.yesno(msg, cli_flag=cli_flag):
|
||||
setattr(self.config, self.dest('public-ip-logging-ok'), True)
|
||||
else:
|
||||
break
|
||||
finally:
|
||||
sock.close()
|
||||
raise errors.PluginError('Must agree to IP logging to proceed')
|
||||
|
||||
def cleanup(self, achalls):
|
||||
# pylint: disable=missing-docstring
|
||||
for achall in achalls:
|
||||
if isinstance(achall.chall, challenges.HTTP01):
|
||||
self._cleanup_http01_challenge(achall)
|
||||
def _perform_achall_with_script(self, achall, validation):
|
||||
env = dict(CERTBOT_DOMAIN=achall.domain, CERTBOT_VALIDATION=validation)
|
||||
if isinstance(achall.chall, challenges.HTTP01):
|
||||
env['CERTBOT_TOKEN'] = achall.encode("token")
|
||||
os.environ.update(env)
|
||||
_, out = hooks.execute(self.conf('auth-script'))
|
||||
env['CERTBOT_AUTH_OUTPUT'] = out.strip()
|
||||
self.env[achall.domain] = env
|
||||
|
||||
def _perform_http01_challenge(self, achall):
|
||||
# same path for each challenge response would be easier for
|
||||
# users, but will not work if multiple domains point at the
|
||||
# same server: default command doesn't support virtual hosts
|
||||
response, validation = achall.response_and_validation()
|
||||
|
||||
port = (response.port if self.config.http01_port is None
|
||||
else int(self.config.http01_port))
|
||||
command = self.CMD_TEMPLATE.format(
|
||||
root=self._root, achall=achall, response=response,
|
||||
# TODO(kuba): pipes still necessary?
|
||||
validation=pipes.quote(validation),
|
||||
encoded_token=achall.chall.encode("token"),
|
||||
port=port)
|
||||
if self.conf("test-mode"):
|
||||
logger.debug("Test mode. Executing the manual command: %s", command)
|
||||
# sh shipped with OS X does't support echo -n, but supports printf
|
||||
try:
|
||||
self._httpd = subprocess.Popen(
|
||||
command,
|
||||
# don't care about setting stdout and stderr,
|
||||
# we're in test mode anyway
|
||||
shell=True,
|
||||
executable=None,
|
||||
# "preexec_fn" is UNIX specific, but so is "command"
|
||||
preexec_fn=os.setsid)
|
||||
except OSError as error: # ValueError should not happen!
|
||||
logger.debug(
|
||||
"Couldn't execute manual command: %s", error, exc_info=True)
|
||||
return False
|
||||
logger.debug("Manual command running as PID %s.", self._httpd.pid)
|
||||
# give it some time to bootstrap, before we try to verify
|
||||
# (cert generation in case of simpleHttpS might take time)
|
||||
self._test_mode_busy_wait(port)
|
||||
|
||||
if self._httpd.poll() is not None:
|
||||
raise errors.Error("Couldn't execute manual command")
|
||||
def _perform_achall_manually(self, achall, response, validation):
|
||||
if isinstance(achall.chall, challenges.HTTP01):
|
||||
if self.config.http01_port is None:
|
||||
port = response.port
|
||||
else:
|
||||
port = self.config.http01_port
|
||||
msg = self._HTTP_INSTRUCTIONS.format(
|
||||
achall=achall, encoded_token=achall.encode('token'),
|
||||
response=response, port=port, uri=achall.uri(achall.domain),
|
||||
validation=validation)
|
||||
else:
|
||||
self._notify_and_wait(
|
||||
self._get_message(achall).format(
|
||||
validation=validation,
|
||||
response=response,
|
||||
uri=achall.chall.uri(achall.domain),
|
||||
command=command))
|
||||
assert isinstance(achall.chall, challenges.DNS01)
|
||||
msg = self._DNS_INSTRUCTIONS.format(
|
||||
domain=achall.validation_domain_name(achall.domain),
|
||||
response=response, validation=validation)
|
||||
display = zope.component.getUtility(interfaces.IDisplay)
|
||||
display.notification(msg, wrap=False)
|
||||
|
||||
if not response.simple_verify(
|
||||
achall.chall, achall.domain,
|
||||
achall.account_key.public_key(), self.config.http01_port):
|
||||
logger.warning("Self-verify of challenge failed.")
|
||||
|
||||
return response
|
||||
|
||||
def _perform_dns01_challenge(self, achall):
|
||||
response, validation = achall.response_and_validation()
|
||||
if not self.conf("test-mode"):
|
||||
self._notify_and_wait(
|
||||
self._get_message(achall).format(
|
||||
validation=validation,
|
||||
domain=achall.validation_domain_name(achall.domain),
|
||||
response=response))
|
||||
|
||||
try:
|
||||
verification_status = response.simple_verify(
|
||||
achall.chall, achall.domain,
|
||||
achall.account_key.public_key())
|
||||
except acme_errors.DependencyError:
|
||||
logger.warning("Self verification requires optional "
|
||||
"dependency `dnspython` to be installed.")
|
||||
else:
|
||||
if not verification_status:
|
||||
logger.warning("Self-verify of challenge failed.")
|
||||
|
||||
return response
|
||||
|
||||
def _cleanup_http01_challenge(self, achall):
|
||||
# pylint: disable=missing-docstring,unused-argument
|
||||
if self.conf("test-mode"):
|
||||
assert self._httpd is not None, (
|
||||
"cleanup() must be called after perform()")
|
||||
if self._httpd.poll() is None:
|
||||
logger.debug("Terminating manual command process")
|
||||
self._httpd.terminate()
|
||||
else:
|
||||
logger.debug("Manual command process already terminated "
|
||||
"with %s code", self._httpd.returncode)
|
||||
shutil.rmtree(self._root)
|
||||
|
||||
def _notify_and_wait(self, message):
|
||||
# pylint: disable=no-self-use
|
||||
# TODO: IDisplay wraps messages, breaking the command
|
||||
#answer = zope.component.getUtility(interfaces.IDisplay).notification(
|
||||
# message=message, pause=True)
|
||||
sys.stdout.write(message)
|
||||
six.moves.input("Press ENTER to continue")
|
||||
|
||||
def _get_ip_logging_permission(self):
|
||||
# pylint: disable=missing-docstring
|
||||
if not (self.conf("test-mode") or self.conf("public-ip-logging-ok")):
|
||||
if not zope.component.getUtility(interfaces.IDisplay).yesno(
|
||||
self.IP_DISCLAIMER, "Yes", "No",
|
||||
cli_flag="--manual-public-ip-logging-ok"):
|
||||
raise errors.PluginError("Must agree to IP logging to proceed")
|
||||
else:
|
||||
self.config.namespace.manual_public_ip_logging_ok = True
|
||||
|
||||
def _get_message(self, achall):
|
||||
# pylint: disable=missing-docstring,no-self-use,unused-argument
|
||||
return self.MESSAGE_TEMPLATE.get(achall.chall.typ, "")
|
||||
def cleanup(self, achalls): # pylint: disable=missing-docstring
|
||||
if self.conf('cleanup-script'):
|
||||
for achall in achalls:
|
||||
os.environ.update(self.env[achall.domain])
|
||||
hooks.execute(self.conf('cleanup-script'))
|
||||
|
||||
@@ -1,154 +0,0 @@
|
||||
"""Manual authenticator plugin"""
|
||||
import os
|
||||
|
||||
import zope.component
|
||||
import zope.interface
|
||||
|
||||
from acme import challenges
|
||||
|
||||
from certbot import interfaces
|
||||
from certbot import errors
|
||||
from certbot import hooks
|
||||
from certbot.plugins import common
|
||||
|
||||
|
||||
@zope.interface.implementer(interfaces.IAuthenticator)
|
||||
@zope.interface.provider(interfaces.IPluginFactory)
|
||||
class Authenticator(common.Plugin):
|
||||
"""Manual authenticator
|
||||
|
||||
This plugin allows the user to perform the domain validation
|
||||
challenge(s) themselves. This can be done either be done manually
|
||||
by the user or through shell scripts provided to Certbot.
|
||||
|
||||
"""
|
||||
|
||||
description = 'Manual configuration or run your own shell scripts'
|
||||
hidden = True
|
||||
long_description = (
|
||||
'Authenticate through manual configuration or custom shell scripts. '
|
||||
'When using shell scripts, an authenticator script must be provided. '
|
||||
'The environment variables available to this script are '
|
||||
'$CERTBOT_DOMAIN which contains the domain being authenticated, '
|
||||
'$CERTBOT_VALIDATION which is the validation string, and '
|
||||
'$CERTBOT_TOKEN which 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.')
|
||||
_DNS_INSTRUCTIONS = """\
|
||||
Please deploy a DNS TXT record under the name
|
||||
{domain} with the following value:
|
||||
|
||||
{validation}
|
||||
|
||||
Once this is deployed,
|
||||
"""
|
||||
_HTTP_INSTRUCTIONS = """\
|
||||
Make sure your web server displays the following content at
|
||||
{uri} before continuing:
|
||||
|
||||
{validation}
|
||||
|
||||
If you don't have HTTP server configured, you can run the following
|
||||
command on the target server (as root):
|
||||
|
||||
mkdir -p /tmp/certbot/public_html/{achall.URI_ROOT_PATH}
|
||||
cd /tmp/certbot/public_html
|
||||
printf "%s" {validation} > {achall.URI_ROOT_PATH}/{encoded_token}
|
||||
# run only once per server:
|
||||
$(command -v python2 || command -v python2.7 || command -v python2.6) -c \\
|
||||
"import BaseHTTPServer, SimpleHTTPServer; \\
|
||||
s = BaseHTTPServer.HTTPServer(('', {port}), SimpleHTTPServer.SimpleHTTPRequestHandler); \\
|
||||
s.serve_forever()"
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(Authenticator, self).__init__(*args, **kwargs)
|
||||
self.env = dict()
|
||||
|
||||
@classmethod
|
||||
def add_parser_arguments(cls, add):
|
||||
add('auth-script',
|
||||
help='Path or command to execute for the authentication script')
|
||||
add('cleanup-script',
|
||||
help='Path or command to execute for the cleanup script')
|
||||
add('public-ip-logging-ok', action='store_true',
|
||||
help='Automatically allows public IP logging')
|
||||
|
||||
def prepare(self): # pylint: disable=missing-docstring
|
||||
if self.config.noninteractive_mode and not self.conf('auth-script'):
|
||||
raise errors.PluginError(
|
||||
'An authentication script must be provided with --{0} when '
|
||||
'using the manual plugin non-interactively.'.format(
|
||||
self.option_name('auth-script')))
|
||||
|
||||
def more_info(self): # pylint: disable=missing-docstring,no-self-use
|
||||
return (
|
||||
'This plugin allows the user to customize setup for domain '
|
||||
'validation challenges either through shell scripts provided by '
|
||||
'the user or by performing the setup manually.')
|
||||
|
||||
def get_chall_pref(self, domain):
|
||||
# pylint: disable=missing-docstring,no-self-use,unused-argument
|
||||
return [challenges.HTTP01, challenges.DNS01]
|
||||
|
||||
def perform(self, achalls): # pylint: disable=missing-docstring
|
||||
self._verify_ip_logging_ok()
|
||||
|
||||
responses = []
|
||||
for achall in achalls:
|
||||
response, validation = achall.response_and_validation()
|
||||
if self.conf('auth-script'):
|
||||
self._perform_achall_with_script(achall, validation)
|
||||
else:
|
||||
self._perform_achall_manually(achall, response, validation)
|
||||
responses.append(response)
|
||||
return responses
|
||||
|
||||
def _verify_ip_logging_ok(self):
|
||||
if not self.conf('public-ip-logging-ok'):
|
||||
cli_flag = self.option_name('public-ip-logging-ok')
|
||||
msg = ('NOTE: The IP of this machine will be publicly logged as '
|
||||
"having requested this certificate. If you're running "
|
||||
'certbot in manual mode on a machine that is not your '
|
||||
"server, please ensure you're okay with that.\n\n"
|
||||
'Are you OK with your IP being logged?')
|
||||
display = zope.component.getUtility(interfaces.IDisplay)
|
||||
if display.yesno(msg, cli_flag=cli_flag):
|
||||
setattr(self.config, self.dest('public-ip-logging-ok'), True)
|
||||
else:
|
||||
raise errors.PluginError('Must agree to IP logging to proceed')
|
||||
|
||||
def _perform_achall_with_script(self, achall, validation):
|
||||
env = dict(CERTBOT_DOMAIN=achall.domain, CERTBOT_VALIDATION=validation)
|
||||
if isinstance(achall.chall, challenges.HTTP01):
|
||||
env['CERTBOT_TOKEN'] = achall.encode("token")
|
||||
os.environ.update(env)
|
||||
_, out = hooks.execute(self.conf('auth-script'))
|
||||
env['CERTBOT_AUTH_OUTPUT'] = out.strip()
|
||||
self.env[achall.domain] = env
|
||||
|
||||
def _perform_achall_manually(self, achall, response, validation):
|
||||
if isinstance(achall.chall, challenges.HTTP01):
|
||||
if self.config.http01_port is None:
|
||||
port = response.port
|
||||
else:
|
||||
port = self.config.http01_port
|
||||
msg = self._HTTP_INSTRUCTIONS.format(
|
||||
achall=achall, encoded_token=achall.encode('token'),
|
||||
response=response, port=port, uri=achall.uri(achall.domain),
|
||||
validation=validation)
|
||||
else:
|
||||
assert isinstance(achall.chall, challenges.DNS01)
|
||||
msg = self._DNS_INSTRUCTIONS.format(
|
||||
domain=achall.validation_domain_name(achall.domain),
|
||||
response=response, validation=validation)
|
||||
display = zope.component.getUtility(interfaces.IDisplay)
|
||||
display.notification(msg, wrap=False)
|
||||
|
||||
def cleanup(self, achalls): # pylint: disable=missing-docstring
|
||||
if self.conf('cleanup-script'):
|
||||
for achall in achalls:
|
||||
os.environ.update(self.env[achall.domain])
|
||||
hooks.execute(self.conf('cleanup-script'))
|
||||
@@ -1,161 +0,0 @@
|
||||
"""Script-based Authenticator."""
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
import zope.interface
|
||||
|
||||
from acme import challenges
|
||||
|
||||
from certbot import errors
|
||||
from certbot import interfaces
|
||||
from certbot import hooks
|
||||
|
||||
from certbot.plugins import common
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
CHALLENGES = ["http-01", "dns-01"]
|
||||
|
||||
|
||||
@zope.interface.implementer(interfaces.IAuthenticator)
|
||||
@zope.interface.provider(interfaces.IPluginFactory)
|
||||
class Authenticator(common.Plugin):
|
||||
"""Script authenticator
|
||||
|
||||
calls user defined script to perform authentication and
|
||||
optionally cleanup.
|
||||
|
||||
"""
|
||||
|
||||
description = "Authenticate using user provided script(s)"
|
||||
|
||||
long_description = ("Authenticate using user provided script(s). " +
|
||||
"Authenticator script has the following environment " +
|
||||
"variables available for it: " +
|
||||
"CERTBOT_DOMAIN - The domain being authenticated " +
|
||||
"CERTBOT_VALIDATION - The validation string " +
|
||||
"CERTBOT_TOKEN - Resource name part of HTTP-01 " +
|
||||
"challenge (HTTP-01 only). " +
|
||||
"Cleanup script has all the above, and additional " +
|
||||
"var: CERTBOT_AUTH_OUTPUT - stdout output from the " +
|
||||
"authenticator"
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(Authenticator, self).__init__(*args, **kwargs)
|
||||
self.cleanup_script = None
|
||||
self.auth_script = None
|
||||
self.challenges = []
|
||||
|
||||
@classmethod
|
||||
def add_parser_arguments(cls, add):
|
||||
add("auth", default=None, required=False,
|
||||
help="path or command for the authentication script")
|
||||
add("cleanup", default=None, required=False,
|
||||
help="path or command for the cleanup script")
|
||||
|
||||
@property
|
||||
def supported_challenges(self):
|
||||
"""Challenges supported by this plugin."""
|
||||
return self.challenges
|
||||
|
||||
def more_info(self): # pylint: disable=missing-docstring
|
||||
return("This authenticator enables user to perform authentication " +
|
||||
"using shell script(s).")
|
||||
|
||||
def prepare(self):
|
||||
"""Prepare script plugin, check challenge, scripts and register them"""
|
||||
pref_challenges = self.config.pref_challs
|
||||
for c in pref_challenges:
|
||||
if c.typ in CHALLENGES:
|
||||
self.challenges.append(c)
|
||||
if not self.challenges and len(pref_challenges):
|
||||
# Challenges requested, but not supported
|
||||
raise errors.PluginError(
|
||||
"Unfortunately script plugin doesn't yet support " +
|
||||
"the requested challenges")
|
||||
|
||||
# Challenge not defined on cli, set default
|
||||
if not self.challenges:
|
||||
self.challenges.append(challenges.Challenge.TYPES["http-01"])
|
||||
|
||||
if not self.conf("auth"):
|
||||
raise errors.PluginError("Parameter --script-auth is required " +
|
||||
"for script plugin")
|
||||
self._prepare_scripts()
|
||||
|
||||
def _prepare_scripts(self):
|
||||
"""Helper method for prepare, to take care of validating scripts"""
|
||||
script_path = self.conf("auth")
|
||||
cleanup_path = self.conf("cleanup")
|
||||
if self.config.validate_hooks:
|
||||
hooks.validate_hook(script_path, "script_auth")
|
||||
self.auth_script = script_path
|
||||
if cleanup_path:
|
||||
if self.config.validate_hooks:
|
||||
hooks.validate_hook(cleanup_path, "script_cleanup")
|
||||
self.cleanup_script = cleanup_path
|
||||
|
||||
def get_chall_pref(self, domain):
|
||||
"""Return challenge(s) we're answering to """
|
||||
# pylint: disable=unused-argument
|
||||
return self.challenges
|
||||
|
||||
def perform(self, achalls):
|
||||
"""Perform the authentication per challenge"""
|
||||
mapping = {"http-01": self._setup_env_http,
|
||||
"dns-01": self._setup_env_dns}
|
||||
responses = []
|
||||
for achall in achalls:
|
||||
response, validation = achall.response_and_validation()
|
||||
# Setup env vars
|
||||
mapping[achall.typ](achall, validation)
|
||||
output = self.execute(self.auth_script)
|
||||
if output:
|
||||
self._write_auth_output(output)
|
||||
responses.append(response)
|
||||
return responses
|
||||
|
||||
def _setup_env_http(self, achall, validation):
|
||||
"""Write environment variables for http challenge"""
|
||||
ev = dict()
|
||||
ev["CERTBOT_TOKEN"] = achall.chall.encode("token")
|
||||
ev["CERTBOT_VALIDATION"] = validation
|
||||
ev["CERTBOT_DOMAIN"] = achall.domain
|
||||
os.environ.update(ev)
|
||||
|
||||
def _setup_env_dns(self, achall, validation):
|
||||
"""Write environment variables for dns challenge"""
|
||||
ev = dict()
|
||||
ev["CERTBOT_VALIDATION"] = validation
|
||||
ev["CERTBOT_DOMAIN"] = achall.domain
|
||||
os.environ.update(ev)
|
||||
|
||||
def _write_auth_output(self, out):
|
||||
"""Write output from auth script to env var for
|
||||
cleanup to act upon"""
|
||||
os.environ.update({"CERTBOT_AUTH_OUTPUT": out.strip()})
|
||||
|
||||
def _normalize_string(self, value):
|
||||
"""Return string instead of bytestring for Python3.
|
||||
Helper function for writing env vars, as os.environ needs str"""
|
||||
|
||||
if isinstance(value, bytes):
|
||||
value = value.decode(sys.getdefaultencoding())
|
||||
return str(value)
|
||||
|
||||
def execute(self, shell_cmd):
|
||||
"""Run a script.
|
||||
|
||||
:param str shell_cmd: Command to run
|
||||
:returns: `str` stdout output"""
|
||||
|
||||
_, out = hooks.execute(shell_cmd)
|
||||
return self._normalize_string(out)
|
||||
|
||||
def cleanup(self, achalls): # pylint: disable=unused-argument
|
||||
"""Run cleanup.sh """
|
||||
if self.cleanup_script:
|
||||
self.execute(self.cleanup_script)
|
||||
Reference in New Issue
Block a user