mirror of
https://github.com/certbot/certbot.git
synced 2026-08-04 08:03:44 +02:00
Merge the manual and script plugins (#3890)
* Start of combined manual/script plugin * Return str from hooks.execute, not bytes * finish manual/script rewrite * delete old manual and script plugins * manually specify we want chall.token * use consistent quotes * specify chall for uri * s/script/hook * fix spacing on instructions * remove unneeded response argument * make achall more helpful * simplify perform * remove old test files * add start of manual_tests * fix ParseTest.test_help * stop using manual_test_mode in cli tests * Revert "make achall more helpful" This reverts commit54b01cea30. * use bad response/validation methods on achalls * simplify perform and cleanup environment * finish manual tests * Add HTTP manual hook integration test * add manual http scripts * Add manual DNS script integration test * remove references to the script plugin * they're hooks, not scripts * add --manual-public-ip-logging-ok to integration tests * use --pref-chall for dns integration * does dns work? * validate hooks * test hook validation * Revert "does dns work?" This reverts commit1224cc2961. * busy wait in manual-http-auth * remove DNS script test for now * Fix challenge prefix and add trailing . * Add comment about universal_newlines * Fix typo from0464ba2c4* fix nits and typos * Generalize HookCOmmandNotFound error * Add verify_exe_exists * Don't duplicate code in hooks.py * Revert changes to hooks.py * Use consistent hook error messages
This commit is contained in:
+2
-4
@@ -72,7 +72,7 @@ obtain, install, and renew certificates:
|
|||||||
--standalone Run a standalone webserver for authentication
|
--standalone Run a standalone webserver for authentication
|
||||||
%s
|
%s
|
||||||
--webroot Place files in a server's webroot folder for authentication
|
--webroot Place files in a server's webroot folder for authentication
|
||||||
--manual Obtain certs interactively, or using shell script hoooks
|
--manual Obtain certs interactively, or using shell script hooks
|
||||||
|
|
||||||
-n Run non-interactively
|
-n Run non-interactively
|
||||||
--test-cert Obtain a test cert from a staging server
|
--test-cert Obtain a test cert from a staging server
|
||||||
@@ -100,7 +100,7 @@ More detailed help:
|
|||||||
|
|
||||||
all, automation, commands, paths, security, testing, or any of the
|
all, automation, commands, paths, security, testing, or any of the
|
||||||
subcommands or plugins (certonly, renew, install, register, nginx,
|
subcommands or plugins (certonly, renew, install, register, nginx,
|
||||||
apache, standalone, webroot, script, etc.)
|
apache, standalone, webroot, etc.)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
@@ -1153,8 +1153,6 @@ def _plugins_parsing(helpful, plugins):
|
|||||||
"--nginx", action="store_true", help="Obtain and install certs using Nginx")
|
"--nginx", action="store_true", help="Obtain and install certs using Nginx")
|
||||||
helpful.add(["plugins", "certonly"], "--standalone", action="store_true",
|
helpful.add(["plugins", "certonly"], "--standalone", action="store_true",
|
||||||
help='Obtain certs using a "standalone" webserver.')
|
help='Obtain certs using a "standalone" webserver.')
|
||||||
helpful.add(["plugins", "certonly"], "--script", action="store_true",
|
|
||||||
help='Obtain certs using shell script(s)')
|
|
||||||
helpful.add(["plugins", "certonly"], "--manual", action="store_true",
|
helpful.add(["plugins", "certonly"], "--manual", action="store_true",
|
||||||
help='Provide laborious manual instructions for obtaining a cert')
|
help='Provide laborious manual instructions for obtaining a cert')
|
||||||
helpful.add(["plugins", "certonly"], "--webroot", action="store_true",
|
helpful.add(["plugins", "certonly"], "--webroot", action="store_true",
|
||||||
|
|||||||
+4
-1
@@ -84,7 +84,10 @@ def execute(shell_cmd):
|
|||||||
|
|
||||||
:returns: `tuple` (`str` stderr, `str` stdout)"""
|
:returns: `tuple` (`str` stderr, `str` stdout)"""
|
||||||
|
|
||||||
cmd = Popen(shell_cmd, shell=True, stdout=PIPE, stderr=PIPE)
|
# universal_newlines causes Popen.communicate()
|
||||||
|
# to return str objects instead of bytes in Python 3
|
||||||
|
cmd = Popen(shell_cmd, shell=True, stdout=PIPE,
|
||||||
|
stderr=PIPE, universal_newlines=True)
|
||||||
out, err = cmd.communicate()
|
out, err = cmd.communicate()
|
||||||
if cmd.returncode != 0:
|
if cmd.returncode != 0:
|
||||||
logger.error('Hook command "%s" returned error code %d',
|
logger.error('Hook command "%s" returned error code %d',
|
||||||
|
|||||||
+103
-200
@@ -1,56 +1,49 @@
|
|||||||
"""Manual plugin."""
|
"""Manual authenticator plugin"""
|
||||||
import os
|
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.component
|
||||||
import zope.interface
|
import zope.interface
|
||||||
|
|
||||||
from acme import challenges
|
from acme import challenges
|
||||||
from acme import errors as acme_errors
|
|
||||||
|
|
||||||
from certbot import errors
|
|
||||||
from certbot import interfaces
|
from certbot import interfaces
|
||||||
|
from certbot import errors
|
||||||
|
from certbot import hooks
|
||||||
from certbot.plugins import common
|
from certbot.plugins import common
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
@zope.interface.implementer(interfaces.IAuthenticator)
|
@zope.interface.implementer(interfaces.IAuthenticator)
|
||||||
@zope.interface.provider(interfaces.IPluginFactory)
|
@zope.interface.provider(interfaces.IPluginFactory)
|
||||||
class Authenticator(common.Plugin):
|
class Authenticator(common.Plugin):
|
||||||
"""Manual Authenticator.
|
"""Manual authenticator
|
||||||
|
|
||||||
This plugin requires user's manual intervention in setting up a HTTP
|
This plugin allows the user to perform the domain validation
|
||||||
server for solving http-01 challenges and thus does not need to be
|
challenge(s) themselves. This either be done manually by the user or
|
||||||
run as a privileged process. Alternatively shows instructions on how
|
through shell scripts provided to Certbot.
|
||||||
to use Python's built-in HTTP server.
|
|
||||||
|
|
||||||
.. todo:: Support for `~.challenges.TLSSNI01`.
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
description = 'Manual configuration or run your own shell scripts'
|
||||||
hidden = True
|
hidden = True
|
||||||
|
long_description = (
|
||||||
description = "Manually configure an HTTP server"
|
'Authenticate through manual configuration or custom shell scripts. '
|
||||||
|
'When using shell scripts, an authenticator script must be provided. '
|
||||||
MESSAGE_TEMPLATE = {
|
'The environment variables available to this script are '
|
||||||
"dns-01": """\
|
'$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
|
Please deploy a DNS TXT record under the name
|
||||||
{domain} with the following value:
|
{domain} with the following value:
|
||||||
|
|
||||||
{validation}
|
{validation}
|
||||||
|
|
||||||
Once this is deployed,
|
Once this is deployed,"""
|
||||||
""",
|
_HTTP_INSTRUCTIONS = """\
|
||||||
"http-01": """\
|
|
||||||
Make sure your web server displays the following content at
|
Make sure your web server displays the following content at
|
||||||
{uri} before continuing:
|
{uri} before continuing:
|
||||||
|
|
||||||
@@ -59,204 +52,114 @@ Make sure your web server displays the following content at
|
|||||||
If you don't have HTTP server configured, you can run the following
|
If you don't have HTTP server configured, you can run the following
|
||||||
command on the target server (as root):
|
command on the target server (as root):
|
||||||
|
|
||||||
{command}
|
mkdir -p /tmp/certbot/public_html/{achall.URI_ROOT_PATH}
|
||||||
"""}
|
cd /tmp/certbot/public_html
|
||||||
|
|
||||||
# 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
|
|
||||||
printf "%s" {validation} > {achall.URI_ROOT_PATH}/{encoded_token}
|
printf "%s" {validation} > {achall.URI_ROOT_PATH}/{encoded_token}
|
||||||
# run only once per server:
|
# run only once per server:
|
||||||
$(command -v python2 || command -v python2.7 || command -v python2.6) -c \\
|
$(command -v python2 || command -v python2.7 || command -v python2.6) -c \\
|
||||||
"import BaseHTTPServer, SimpleHTTPServer; \\
|
"import BaseHTTPServer, SimpleHTTPServer; \\
|
||||||
s = BaseHTTPServer.HTTPServer(('', {port}), SimpleHTTPServer.SimpleHTTPRequestHandler); \\
|
s = BaseHTTPServer.HTTPServer(('', {port}), SimpleHTTPServer.SimpleHTTPRequestHandler); \\
|
||||||
s.serve_forever()" """
|
s.serve_forever()" """
|
||||||
"""Command template."""
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(Authenticator, self).__init__(*args, **kwargs)
|
super(Authenticator, self).__init__(*args, **kwargs)
|
||||||
self._root = (tempfile.mkdtemp() if self.conf("test-mode")
|
self.env = dict()
|
||||||
else "/tmp/certbot")
|
|
||||||
self._httpd = None
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def add_parser_arguments(cls, add):
|
def add_parser_arguments(cls, add):
|
||||||
add("test-mode", action="store_true",
|
add('auth-hook',
|
||||||
help="Test mode. Executes the manual command in subprocess.")
|
help='Path or command to execute for the authentication script')
|
||||||
add("public-ip-logging-ok", action="store_true",
|
add('cleanup-hook',
|
||||||
help="Automatically allows public IP logging. (default: Ask)")
|
help='Path or command to execute for the cleanup script')
|
||||||
|
add('public-ip-logging-ok', action='store_true',
|
||||||
|
help='Automatically allows public IP logging (default: Ask)')
|
||||||
|
|
||||||
def prepare(self): # pylint: disable=missing-docstring,no-self-use
|
def prepare(self): # pylint: disable=missing-docstring
|
||||||
if self.config.noninteractive_mode and not self.conf("test-mode"):
|
if self.config.noninteractive_mode and not self.conf('auth-hook'):
|
||||||
raise errors.PluginError("Running manual mode non-interactively is not supported")
|
raise errors.PluginError(
|
||||||
|
'An authentication script must be provided with --{0} when '
|
||||||
|
'using the manual plugin non-interactively.'.format(
|
||||||
|
self.option_name('auth-hook')))
|
||||||
|
self._validate_hooks()
|
||||||
|
|
||||||
|
def _validate_hooks(self):
|
||||||
|
if self.config.validate_hooks:
|
||||||
|
for name in ('auth-hook', 'cleanup-hook'):
|
||||||
|
hook = self.conf(name)
|
||||||
|
if hook is not None:
|
||||||
|
hook_prefix = self.option_name(name)[:-len('-hook')]
|
||||||
|
hooks.validate_hook(hook, hook_prefix)
|
||||||
|
|
||||||
def more_info(self): # pylint: disable=missing-docstring,no-self-use
|
def more_info(self): # pylint: disable=missing-docstring,no-self-use
|
||||||
return ("This plugin requires user's manual intervention in setting "
|
return (
|
||||||
"up challenges to prove control of a domain and does not need "
|
'This plugin allows the user to customize setup for domain '
|
||||||
"to be run as a privileged process. When solving "
|
'validation challenges either through shell scripts provided by '
|
||||||
"http-01 challenges, the user is responsible for setting up "
|
'the user or by performing the setup manually.')
|
||||||
"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.")
|
|
||||||
|
|
||||||
def get_chall_pref(self, domain):
|
def get_chall_pref(self, domain):
|
||||||
# pylint: disable=missing-docstring,no-self-use,unused-argument
|
# pylint: disable=missing-docstring,no-self-use,unused-argument
|
||||||
return [challenges.HTTP01, challenges.DNS01]
|
return [challenges.HTTP01, challenges.DNS01]
|
||||||
|
|
||||||
def perform(self, achalls):
|
def perform(self, achalls): # pylint: disable=missing-docstring
|
||||||
# pylint: disable=missing-docstring
|
self._verify_ip_logging_ok()
|
||||||
self._get_ip_logging_permission()
|
|
||||||
mapping = {"http-01": self._perform_http01_challenge,
|
if self.conf('auth-hook'):
|
||||||
"dns-01": self._perform_dns01_challenge}
|
perform_achall = self._perform_achall_with_script
|
||||||
|
else:
|
||||||
|
perform_achall = self._perform_achall_manually
|
||||||
|
|
||||||
responses = []
|
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:
|
for achall in achalls:
|
||||||
responses.append(mapping[achall.typ](achall))
|
perform_achall(achall)
|
||||||
|
responses.append(achall.response(achall.account_key))
|
||||||
return responses
|
return responses
|
||||||
|
|
||||||
@classmethod
|
def _verify_ip_logging_ok(self):
|
||||||
def _test_mode_busy_wait(cls, port):
|
if not self.conf('public-ip-logging-ok'):
|
||||||
while True:
|
cli_flag = '--{0}'.format(self.option_name('public-ip-logging-ok'))
|
||||||
time.sleep(1)
|
msg = ('NOTE: The IP of this machine will be publicly logged as '
|
||||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
"having requested this certificate. If you're running "
|
||||||
try:
|
'certbot in manual mode on a machine that is not your '
|
||||||
sock.connect(("localhost", port))
|
"server, please ensure you're okay with that.\n\n"
|
||||||
except socket.error: # pragma: no cover
|
'Are you OK with your IP being logged?')
|
||||||
pass
|
display = zope.component.getUtility(interfaces.IDisplay)
|
||||||
|
if display.yesno(msg, cli_flag=cli_flag, force_interactive=True):
|
||||||
|
setattr(self.config, self.dest('public-ip-logging-ok'), True)
|
||||||
else:
|
else:
|
||||||
break
|
raise errors.PluginError('Must agree to IP logging to proceed')
|
||||||
finally:
|
|
||||||
sock.close()
|
|
||||||
|
|
||||||
def cleanup(self, achalls):
|
def _perform_achall_with_script(self, achall):
|
||||||
# pylint: disable=missing-docstring
|
env = dict(CERTBOT_DOMAIN=achall.domain,
|
||||||
for achall in achalls:
|
CERTBOT_VALIDATION=achall.validation(achall.account_key))
|
||||||
if isinstance(achall.chall, challenges.HTTP01):
|
if isinstance(achall.chall, challenges.HTTP01):
|
||||||
self._cleanup_http01_challenge(achall)
|
env['CERTBOT_TOKEN'] = achall.chall.encode('token')
|
||||||
|
|
||||||
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")
|
|
||||||
else:
|
else:
|
||||||
self._notify_and_wait(
|
os.environ.pop('CERTBOT_TOKEN', None)
|
||||||
self._get_message(achall).format(
|
os.environ.update(env)
|
||||||
validation=validation,
|
_, out = hooks.execute(self.conf('auth-hook'))
|
||||||
response=response,
|
env['CERTBOT_AUTH_OUTPUT'] = out.strip()
|
||||||
uri=achall.chall.uri(achall.domain),
|
self.env[achall.domain] = env
|
||||||
command=command))
|
|
||||||
|
|
||||||
if not response.simple_verify(
|
def _perform_achall_manually(self, achall):
|
||||||
achall.chall, achall.domain,
|
validation = achall.validation(achall.account_key)
|
||||||
achall.account_key.public_key(), self.config.http01_port):
|
if isinstance(achall.chall, challenges.HTTP01):
|
||||||
logger.warning("Self-verify of challenge failed.")
|
msg = self._HTTP_INSTRUCTIONS.format(
|
||||||
|
achall=achall, encoded_token=achall.chall.encode('token'),
|
||||||
return response
|
port=self.config.http01_port,
|
||||||
|
uri=achall.chall.uri(achall.domain), validation=validation)
|
||||||
def _perform_dns01_challenge(self, achall):
|
else:
|
||||||
response, validation = achall.response_and_validation()
|
assert isinstance(achall.chall, challenges.DNS01)
|
||||||
if not self.conf("test-mode"):
|
msg = self._DNS_INSTRUCTIONS.format(
|
||||||
self._notify_and_wait(
|
|
||||||
self._get_message(achall).format(
|
|
||||||
validation=validation,
|
|
||||||
domain=achall.validation_domain_name(achall.domain),
|
domain=achall.validation_domain_name(achall.domain),
|
||||||
response=response))
|
validation=validation)
|
||||||
|
display = zope.component.getUtility(interfaces.IDisplay)
|
||||||
|
display.notification(msg, wrap=False, force_interactive=True)
|
||||||
|
|
||||||
try:
|
def cleanup(self, achalls): # pylint: disable=missing-docstring
|
||||||
verification_status = response.simple_verify(
|
if self.conf('cleanup-hook'):
|
||||||
achall.chall, achall.domain,
|
for achall in achalls:
|
||||||
achall.account_key.public_key())
|
env = self.env.pop(achall.domain)
|
||||||
except acme_errors.DependencyError:
|
if 'CERTBOT_TOKEN' not in env:
|
||||||
logger.warning("Self verification requires optional "
|
os.environ.pop('CERTBOT_TOKEN', None)
|
||||||
"dependency `dnspython` to be installed.")
|
os.environ.update(env)
|
||||||
else:
|
hooks.execute(self.conf('cleanup-hook'))
|
||||||
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",
|
|
||||||
force_interactive=True):
|
|
||||||
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, "")
|
|
||||||
|
|||||||
+78
-100
@@ -1,134 +1,112 @@
|
|||||||
"""Tests for certbot.plugins.manual."""
|
"""Tests for certbot.plugins.manual"""
|
||||||
|
import os
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
|
import six
|
||||||
import mock
|
import mock
|
||||||
|
|
||||||
from acme import challenges
|
from acme import challenges
|
||||||
from acme import errors as acme_errors
|
|
||||||
from acme import jose
|
|
||||||
|
|
||||||
from certbot import achallenges
|
|
||||||
from certbot import errors
|
from certbot import errors
|
||||||
|
|
||||||
from certbot.tests import acme_util
|
from certbot.tests import acme_util
|
||||||
from certbot.tests import util as test_util
|
|
||||||
|
|
||||||
|
|
||||||
KEY = jose.JWKRSA.load(test_util.load_vector("rsa512_key.pem"))
|
|
||||||
|
|
||||||
|
|
||||||
class AuthenticatorTest(unittest.TestCase):
|
class AuthenticatorTest(unittest.TestCase):
|
||||||
"""Tests for certbot.plugins.manual.Authenticator."""
|
"""Tests for certbot.plugins.manual.Authenticator."""
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
from certbot.plugins.manual import Authenticator
|
self.http_achall = acme_util.HTTP01_A
|
||||||
|
self.dns_achall = acme_util.DNS01_A
|
||||||
|
self.achalls = [self.http_achall, self.dns_achall]
|
||||||
self.config = mock.MagicMock(
|
self.config = mock.MagicMock(
|
||||||
http01_port=8080, manual_test_mode=False,
|
http01_port=0, manual_auth_hook=None, manual_cleanup_hook=None,
|
||||||
manual_public_ip_logging_ok=False, noninteractive_mode=True)
|
manual_public_ip_logging_ok=False, noninteractive_mode=False,
|
||||||
self.auth = Authenticator(config=self.config, name="manual")
|
validate_hooks=False)
|
||||||
|
|
||||||
self.http01 = achallenges.KeyAuthorizationAnnotatedChallenge(
|
from certbot.plugins.manual import Authenticator
|
||||||
challb=acme_util.HTTP01_P, domain="foo.com", account_key=KEY)
|
self.auth = Authenticator(self.config, name='manual')
|
||||||
self.dns01 = achallenges.KeyAuthorizationAnnotatedChallenge(
|
|
||||||
challb=acme_util.DNS01_P, domain="foo.com", account_key=KEY)
|
|
||||||
|
|
||||||
self.achalls = [self.http01, self.dns01]
|
def test_prepare_no_hook_noninteractive(self):
|
||||||
|
self.config.noninteractive_mode = True
|
||||||
config_test_mode = mock.MagicMock(
|
|
||||||
http01_port=8080, manual_test_mode=True, noninteractive_mode=True)
|
|
||||||
self.auth_test_mode = Authenticator(
|
|
||||||
config=config_test_mode, name="manual")
|
|
||||||
|
|
||||||
def test_prepare(self):
|
|
||||||
self.assertRaises(errors.PluginError, self.auth.prepare)
|
self.assertRaises(errors.PluginError, self.auth.prepare)
|
||||||
self.auth_test_mode.prepare() # error not raised
|
|
||||||
|
def test_prepare_bad_hook(self):
|
||||||
|
self.config.manual_auth_hook = os.path.abspath(os.sep) # is / on UNIX
|
||||||
|
self.config.validate_hooks = True
|
||||||
|
self.assertRaises(errors.HookCommandNotFound, self.auth.prepare)
|
||||||
|
|
||||||
def test_more_info(self):
|
def test_more_info(self):
|
||||||
self.assertTrue(isinstance(self.auth.more_info(), str))
|
self.assertTrue(isinstance(self.auth.more_info(), six.string_types))
|
||||||
|
|
||||||
def test_get_chall_pref(self):
|
def test_get_chall_pref(self):
|
||||||
self.assertTrue(all(issubclass(pref, challenges.Challenge)
|
self.assertEqual(self.auth.get_chall_pref('example.org'),
|
||||||
for pref in self.auth.get_chall_pref("foo.com")))
|
[challenges.HTTP01, challenges.DNS01])
|
||||||
|
|
||||||
@mock.patch("certbot.plugins.manual.zope.component.getUtility")
|
@mock.patch('certbot.plugins.manual.zope.component.getUtility')
|
||||||
def test_perform_empty(self, mock_interaction):
|
def test_ip_logging_not_ok(self, mock_get_utility):
|
||||||
mock_interaction().yesno.return_value = True
|
mock_get_utility().yesno.return_value = False
|
||||||
self.assertEqual([], self.auth.perform([]))
|
self.assertRaises(errors.PluginError, self.auth.perform, [])
|
||||||
|
|
||||||
@mock.patch("certbot.plugins.manual.zope.component.getUtility")
|
@mock.patch('certbot.plugins.manual.zope.component.getUtility')
|
||||||
@mock.patch("certbot.plugins.manual.sys.stdout")
|
def test_ip_logging_ok(self, mock_get_utility):
|
||||||
@mock.patch("acme.challenges.HTTP01Response.simple_verify")
|
mock_get_utility().yesno.return_value = True
|
||||||
@mock.patch("six.moves.input")
|
self.auth.perform([])
|
||||||
def test_perform(self, mock_raw_input, mock_verify, mock_stdout, mock_interaction):
|
self.assertTrue(self.config.manual_public_ip_logging_ok)
|
||||||
mock_verify.return_value = True
|
|
||||||
mock_interaction().yesno.return_value = True
|
|
||||||
|
|
||||||
resp_http = self.http01.response(KEY)
|
def test_script_perform(self):
|
||||||
resp_dns = self.dns01.response(KEY)
|
self.config.manual_public_ip_logging_ok = True
|
||||||
|
self.config.manual_auth_hook = (
|
||||||
|
'echo $CERTBOT_DOMAIN; echo ${CERTBOT_TOKEN:-notoken}; '
|
||||||
|
'echo $CERTBOT_VALIDATION;')
|
||||||
|
dns_expected = '{0}\n{1}\n{2}'.format(
|
||||||
|
self.dns_achall.domain, 'notoken',
|
||||||
|
self.dns_achall.validation(self.dns_achall.account_key))
|
||||||
|
http_expected = '{0}\n{1}\n{2}'.format(
|
||||||
|
self.http_achall.domain, self.http_achall.chall.encode('token'),
|
||||||
|
self.http_achall.validation(self.http_achall.account_key))
|
||||||
|
|
||||||
self.assertEqual([resp_http, resp_dns], self.auth.perform(self.achalls))
|
self.assertEqual(
|
||||||
self.assertEqual(2, mock_raw_input.call_count)
|
self.auth.perform(self.achalls),
|
||||||
mock_verify.assert_called_with(
|
[achall.response(achall.account_key) for achall in self.achalls])
|
||||||
self.http01.challb.chall, "foo.com", KEY.public_key(), 8080)
|
self.assertEqual(
|
||||||
|
self.auth.env[self.dns_achall.domain]['CERTBOT_AUTH_OUTPUT'],
|
||||||
|
dns_expected)
|
||||||
|
self.assertEqual(
|
||||||
|
self.auth.env[self.http_achall.domain]['CERTBOT_AUTH_OUTPUT'],
|
||||||
|
http_expected)
|
||||||
|
|
||||||
message = mock_stdout.write.mock_calls[0][1][0]
|
@mock.patch('certbot.plugins.manual.zope.component.getUtility')
|
||||||
self.assertTrue(self.http01.chall.encode("token") in message)
|
def test_manual_perform(self, mock_get_utility):
|
||||||
|
self.config.manual_public_ip_logging_ok = True
|
||||||
|
self.assertEqual(
|
||||||
|
self.auth.perform(self.achalls),
|
||||||
|
[achall.response(achall.account_key) for achall in self.achalls])
|
||||||
|
for i, (args, kwargs) in enumerate(mock_get_utility().notification.call_args_list):
|
||||||
|
achall = self.achalls[i]
|
||||||
|
self.assertTrue(achall.validation(achall.account_key) in args[0])
|
||||||
|
self.assertFalse(kwargs['wrap'])
|
||||||
|
|
||||||
mock_verify.return_value = False
|
def test_cleanup(self):
|
||||||
with mock.patch("certbot.plugins.manual.logger") as mock_logger:
|
self.config.manual_public_ip_logging_ok = True
|
||||||
|
self.config.manual_auth_hook = 'echo foo;'
|
||||||
|
self.config.manual_cleanup_hook = '# cleanup'
|
||||||
self.auth.perform(self.achalls)
|
self.auth.perform(self.achalls)
|
||||||
self.assertEqual(2, mock_logger.warning.call_count)
|
|
||||||
|
|
||||||
@mock.patch("certbot.plugins.manual.zope.component.getUtility")
|
for achall in self.achalls:
|
||||||
@mock.patch("acme.challenges.DNS01Response.simple_verify")
|
self.auth.cleanup([achall])
|
||||||
@mock.patch("six.moves.input")
|
self.assertEqual(os.environ['CERTBOT_AUTH_OUTPUT'], 'foo')
|
||||||
def test_perform_missing_dependency(self, mock_raw_input, mock_verify, mock_interaction):
|
self.assertEqual(os.environ['CERTBOT_DOMAIN'], achall.domain)
|
||||||
mock_interaction().yesno.return_value = True
|
self.assertEqual(
|
||||||
mock_verify.side_effect = acme_errors.DependencyError()
|
os.environ['CERTBOT_VALIDATION'],
|
||||||
|
achall.validation(achall.account_key))
|
||||||
|
|
||||||
with mock.patch("certbot.plugins.manual.logger") as mock_logger:
|
if isinstance(achall.chall, challenges.HTTP01):
|
||||||
self.auth.perform([self.dns01])
|
self.assertEqual(
|
||||||
self.assertEqual(1, mock_logger.warning.call_count)
|
os.environ['CERTBOT_TOKEN'],
|
||||||
|
achall.chall.encode('token'))
|
||||||
mock_raw_input.assert_called_once_with("Press ENTER to continue")
|
else:
|
||||||
|
self.assertFalse('CERTBOT_TOKEN' in os.environ)
|
||||||
@mock.patch("certbot.plugins.manual.zope.component.getUtility")
|
|
||||||
@mock.patch("certbot.plugins.manual.Authenticator._notify_and_wait")
|
|
||||||
def test_disagree_with_ip_logging(self, mock_notify, mock_interaction):
|
|
||||||
mock_interaction().yesno.return_value = False
|
|
||||||
mock_notify.side_effect = errors.Error("Exception not raised, \
|
|
||||||
continued execution even after disagreeing with IP logging")
|
|
||||||
|
|
||||||
self.assertRaises(errors.PluginError, self.auth.perform, self.achalls)
|
|
||||||
|
|
||||||
@mock.patch("certbot.plugins.manual.subprocess.Popen", autospec=True)
|
|
||||||
def test_perform_test_command_oserror(self, mock_popen):
|
|
||||||
mock_popen.side_effect = OSError
|
|
||||||
self.assertEqual([False], self.auth_test_mode.perform([self.http01]))
|
|
||||||
|
|
||||||
@mock.patch("certbot.plugins.manual.socket.socket")
|
|
||||||
@mock.patch("certbot.plugins.manual.time.sleep", autospec=True)
|
|
||||||
@mock.patch("certbot.plugins.manual.subprocess.Popen", autospec=True)
|
|
||||||
def test_perform_test_command_run_failure(
|
|
||||||
self, mock_popen, unused_mock_sleep, unused_mock_socket):
|
|
||||||
mock_popen.poll.return_value = 10
|
|
||||||
mock_popen.return_value.pid = 1234
|
|
||||||
self.assertRaises(
|
|
||||||
errors.Error, self.auth_test_mode.perform, self.achalls)
|
|
||||||
|
|
||||||
def test_cleanup_test_mode_already_terminated(self):
|
|
||||||
# pylint: disable=protected-access
|
|
||||||
self.auth_test_mode._httpd = httpd = mock.Mock()
|
|
||||||
httpd.poll.return_value = 0
|
|
||||||
self.auth_test_mode.cleanup(self.achalls)
|
|
||||||
|
|
||||||
def test_cleanup_test_mode_kills_still_running(self):
|
|
||||||
# pylint: disable=protected-access
|
|
||||||
self.auth_test_mode._httpd = httpd = mock.Mock(pid=1234)
|
|
||||||
httpd.poll.return_value = None
|
|
||||||
self.auth_test_mode.cleanup(self.achalls)
|
|
||||||
httpd.terminate.assert_called_once_with()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == '__main__':
|
||||||
unittest.main() # pragma: no cover
|
unittest.main() # pragma: no cover
|
||||||
|
|||||||
@@ -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)
|
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
"""Tests for certbot.plugins.manual."""
|
|
||||||
import os
|
|
||||||
import tempfile
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
import mock
|
|
||||||
|
|
||||||
from acme import challenges
|
|
||||||
from acme import jose
|
|
||||||
|
|
||||||
from certbot import achallenges
|
|
||||||
from certbot import errors
|
|
||||||
|
|
||||||
from certbot.tests import acme_util
|
|
||||||
from certbot.tests import util as test_util
|
|
||||||
|
|
||||||
|
|
||||||
KEY = jose.JWKRSA.load(test_util.load_vector("rsa512_key.pem"))
|
|
||||||
|
|
||||||
|
|
||||||
class AuthenticatorTest(unittest.TestCase):
|
|
||||||
"""Tests for certbot.plugins.script.Authenticator."""
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
from certbot.plugins.script import Authenticator
|
|
||||||
self.auth_return_value = "return from auth\n"
|
|
||||||
self.script_nonexec = create_script(b'# empty')
|
|
||||||
self.script_exec = create_script_exec(b'echo "return from auth\n"')
|
|
||||||
self.config = mock.MagicMock(
|
|
||||||
script_auth=self.script_exec,
|
|
||||||
script_cleanup=self.script_exec,
|
|
||||||
pref_challs=[challenges.Challenge.TYPES["http-01"],
|
|
||||||
challenges.Challenge.TYPES["dns-01"],
|
|
||||||
challenges.Challenge.TYPES["tls-sni-01"]])
|
|
||||||
|
|
||||||
self.tlssni_config = mock.MagicMock(
|
|
||||||
script_auth=self.script_exec,
|
|
||||||
script_cleanup=self.script_exec,
|
|
||||||
pref_challs=[challenges.Challenge.TYPES["tls-sni-01"]])
|
|
||||||
|
|
||||||
self.nochall_config = mock.MagicMock(
|
|
||||||
script_auth=self.script_exec,
|
|
||||||
script_cleanup=self.script_exec,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.default = Authenticator(config=self.config, name="script")
|
|
||||||
self.onlytlssni = Authenticator(config=self.tlssni_config,
|
|
||||||
name="script")
|
|
||||||
self.nochall = Authenticator(config=self.nochall_config,
|
|
||||||
name="script")
|
|
||||||
|
|
||||||
self.http01 = achallenges.KeyAuthorizationAnnotatedChallenge(
|
|
||||||
challb=acme_util.HTTP01_P, domain="foo.com", account_key=KEY)
|
|
||||||
self.dns01 = achallenges.KeyAuthorizationAnnotatedChallenge(
|
|
||||||
challb=acme_util.DNS01_P, domain="foo.com", account_key=KEY)
|
|
||||||
|
|
||||||
self.achalls = [self.http01, self.dns01]
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
os.remove(self.script_exec)
|
|
||||||
os.remove(self.script_nonexec)
|
|
||||||
|
|
||||||
def test_prepare_normal(self):
|
|
||||||
"""Test prepare with typical configuration"""
|
|
||||||
from certbot.plugins.script import Authenticator
|
|
||||||
# Erroring combinations in from of (auth_script, cleanup_script, error)
|
|
||||||
for v in [("/NONEXISTENT/script.sh", "/NONEXISTENT/script.sh",
|
|
||||||
errors.HookCommandNotFound),
|
|
||||||
(self.script_nonexec, "/NONEXISTENT/script.sh",
|
|
||||||
errors.HookCommandNotFound),
|
|
||||||
(self.script_exec, "/NONEXISTENT/script.sh",
|
|
||||||
errors.HookCommandNotFound),
|
|
||||||
("/NONEXISTENT/script.sh", self.script_nonexec,
|
|
||||||
errors.HookCommandNotFound),
|
|
||||||
("/NONEXISTENT/script.sh", self.script_exec,
|
|
||||||
errors.HookCommandNotFound),
|
|
||||||
(None, self.script_exec,
|
|
||||||
errors.PluginError)]:
|
|
||||||
testconf = mock.MagicMock(
|
|
||||||
script_auth=v[0],
|
|
||||||
script_cleanup=v[1],
|
|
||||||
pref_challs=[challenges.Challenge.TYPES["http-01"]])
|
|
||||||
testauth = Authenticator(config=testconf, name="script")
|
|
||||||
self.assertRaises(v[2], testauth.prepare)
|
|
||||||
|
|
||||||
# This should not error
|
|
||||||
self.default.prepare()
|
|
||||||
self.assertEqual(len(self.default.challenges), 2)
|
|
||||||
|
|
||||||
def test_prepare_tlssni(self):
|
|
||||||
"""Test for provided, but unsupported challenge type"""
|
|
||||||
self.assertRaises(errors.PluginError, self.onlytlssni.prepare)
|
|
||||||
|
|
||||||
def test_prepare_nochall(self):
|
|
||||||
"""Test for default challenge"""
|
|
||||||
self.nochall.prepare()
|
|
||||||
self.assertEqual(len(self.nochall.challenges), 1)
|
|
||||||
|
|
||||||
def test_more_info(self):
|
|
||||||
self.assertTrue(isinstance(self.default.more_info(), str))
|
|
||||||
|
|
||||||
def test_get_chall_pref(self):
|
|
||||||
self.default.prepare()
|
|
||||||
self.assertTrue(all(issubclass(pref, challenges.Challenge)
|
|
||||||
for pref in self.default.get_chall_pref(
|
|
||||||
"foo.com")))
|
|
||||||
|
|
||||||
def test_get_supported_challenges(self):
|
|
||||||
self.default.prepare()
|
|
||||||
self.assertTrue(all(issubclass(sup, challenges.Challenge)
|
|
||||||
for sup in self.default.supported_challenges))
|
|
||||||
|
|
||||||
def test_perform(self):
|
|
||||||
resp_http = self.http01.response(KEY)
|
|
||||||
resp_dns = self.dns01.response(KEY)
|
|
||||||
self.default.prepare()
|
|
||||||
# Check for the env vars prior to the run
|
|
||||||
self.assertFalse("CERTBOT_VALIDATION" in os.environ.keys())
|
|
||||||
self.assertFalse("CERTBOT_DOMAIN" in os.environ.keys())
|
|
||||||
self.assertFalse("CERTBOT_AUTH_OUTPUT" in os.environ.keys())
|
|
||||||
|
|
||||||
pref_resp = self.default.perform(self.achalls)
|
|
||||||
self.assertEqual([resp_http, resp_dns], pref_resp)
|
|
||||||
# Check for the env vars post run
|
|
||||||
self.assertTrue("CERTBOT_VALIDATION" in os.environ.keys())
|
|
||||||
self.assertTrue("CERTBOT_DOMAIN" in os.environ.keys())
|
|
||||||
self.assertTrue("CERTBOT_AUTH_OUTPUT" in os.environ.keys())
|
|
||||||
self.assertEqual(os.environ["CERTBOT_AUTH_OUTPUT"],
|
|
||||||
self.auth_return_value.strip())
|
|
||||||
|
|
||||||
@mock.patch('certbot.plugins.script.Authenticator.execute')
|
|
||||||
def test_cleanup(self, mock_exec):
|
|
||||||
mock_exec.return_value = (0, None, None)
|
|
||||||
self.default.prepare()
|
|
||||||
self.default.cleanup(self.achalls)
|
|
||||||
self.assertEqual(mock_exec.call_count, 1)
|
|
||||||
|
|
||||||
@mock.patch('certbot.hooks.Popen')
|
|
||||||
def test_execute(self, mock_popen):
|
|
||||||
proc = mock.Mock()
|
|
||||||
# tuple values: stdout, stderr, errorcode, num_of_logger_calls
|
|
||||||
for t in [("", "", 0, 0),
|
|
||||||
(self.auth_return_value, "", 0, 0),
|
|
||||||
(None, "stderr_output", 0, 1),
|
|
||||||
("whatever", "stderr_output", 1, 2),
|
|
||||||
(b'bytestring outval', "", 0, 0)]:
|
|
||||||
proc = mock.Mock()
|
|
||||||
attrs = {'communicate.return_value': (t[0], t[1]),
|
|
||||||
'returncode': t[2]}
|
|
||||||
proc.configure_mock(**attrs) # pylint: disable=star-args
|
|
||||||
mock_popen.return_value = proc
|
|
||||||
with mock.patch('certbot.hooks.logger.error') as mock_log:
|
|
||||||
output = self.default.execute(self.script_exec)
|
|
||||||
self.assertEqual(mock_log.call_count, t[3])
|
|
||||||
self.assertTrue(isinstance(output, str))
|
|
||||||
|
|
||||||
|
|
||||||
def create_script(contents):
|
|
||||||
""" Helper to create temporary file """
|
|
||||||
f = tempfile.NamedTemporaryFile(delete=False, prefix='.sh')
|
|
||||||
f.write(contents)
|
|
||||||
f.close()
|
|
||||||
return f.name
|
|
||||||
|
|
||||||
|
|
||||||
def create_script_exec(contents):
|
|
||||||
""" Helper to create temporary file with exec permissions"""
|
|
||||||
fname = create_script(contents)
|
|
||||||
os.chmod(fname, 0o700)
|
|
||||||
return fname
|
|
||||||
@@ -133,7 +133,7 @@ def choose_plugin(prepared, question):
|
|||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
noninstaller_plugins = ["webroot", "manual", "standalone", "script"]
|
noninstaller_plugins = ["webroot", "manual", "standalone"]
|
||||||
|
|
||||||
def record_chosen_plugins(config, plugins, auth, inst):
|
def record_chosen_plugins(config, plugins, auth, inst):
|
||||||
"Update the config entries to reflect the plugins we actually selected."
|
"Update the config entries to reflect the plugins we actually selected."
|
||||||
@@ -238,8 +238,6 @@ def cli_plugin_requests(config):
|
|||||||
req_auth = set_configurator(req_auth, "webroot")
|
req_auth = set_configurator(req_auth, "webroot")
|
||||||
if config.manual:
|
if config.manual:
|
||||||
req_auth = set_configurator(req_auth, "manual")
|
req_auth = set_configurator(req_auth, "manual")
|
||||||
if config.script:
|
|
||||||
req_auth = set_configurator(req_auth, "script")
|
|
||||||
logger.debug("Requested authenticator %s and installer %s", req_auth, req_inst)
|
logger.debug("Requested authenticator %s and installer %s", req_auth, req_inst)
|
||||||
return req_auth, req_inst
|
return req_auth, req_inst
|
||||||
|
|
||||||
|
|||||||
@@ -7,9 +7,12 @@ from acme import challenges
|
|||||||
from acme import jose
|
from acme import jose
|
||||||
from acme import messages
|
from acme import messages
|
||||||
|
|
||||||
|
from certbot import auth_handler
|
||||||
|
|
||||||
from certbot.tests import util
|
from certbot.tests import util
|
||||||
|
|
||||||
|
|
||||||
|
JWK = jose.JWK.load(util.load_vector('rsa512_key.pem'))
|
||||||
KEY = util.load_rsa_private_key('rsa512_key.pem')
|
KEY = util.load_rsa_private_key('rsa512_key.pem')
|
||||||
|
|
||||||
# Challenges
|
# Challenges
|
||||||
@@ -50,6 +53,14 @@ DNS01_P = chall_to_challb(DNS01, messages.STATUS_PENDING)
|
|||||||
CHALLENGES_P = [HTTP01_P, TLSSNI01_P, DNS01_P]
|
CHALLENGES_P = [HTTP01_P, TLSSNI01_P, DNS01_P]
|
||||||
|
|
||||||
|
|
||||||
|
# AnnotatedChallenge objects
|
||||||
|
HTTP01_A = auth_handler.challb_to_achall(HTTP01_P, JWK, "example.com")
|
||||||
|
TLSSNI01_A = auth_handler.challb_to_achall(TLSSNI01_P, JWK, "example.net")
|
||||||
|
DNS01_A = auth_handler.challb_to_achall(DNS01_P, JWK, "example.org")
|
||||||
|
|
||||||
|
ACHALLENGES = [HTTP01_A, TLSSNI01_A, DNS01_A]
|
||||||
|
|
||||||
|
|
||||||
def gen_authzr(authz_status, domain, challs, statuses, combos=True):
|
def gen_authzr(authz_status, domain, challs, statuses, combos=True):
|
||||||
"""Generate an authorization resource.
|
"""Generate an authorization resource.
|
||||||
|
|
||||||
|
|||||||
+11
-11
@@ -81,7 +81,7 @@ class ParseTest(unittest.TestCase):
|
|||||||
out = self._help_output(['--help', 'all'])
|
out = self._help_output(['--help', 'all'])
|
||||||
self.assertTrue("--configurator" in out)
|
self.assertTrue("--configurator" in out)
|
||||||
self.assertTrue("how a cert is deployed" in out)
|
self.assertTrue("how a cert is deployed" in out)
|
||||||
self.assertTrue("--manual-test-mode" in out)
|
self.assertTrue("--webroot-path" in out)
|
||||||
self.assertTrue("--text" not in out)
|
self.assertTrue("--text" not in out)
|
||||||
self.assertTrue("--dialog" not in out)
|
self.assertTrue("--dialog" not in out)
|
||||||
self.assertTrue("%s" not in out)
|
self.assertTrue("%s" not in out)
|
||||||
@@ -91,7 +91,7 @@ class ParseTest(unittest.TestCase):
|
|||||||
if "nginx" in self.plugins:
|
if "nginx" in self.plugins:
|
||||||
# may be false while building distributions without plugins
|
# may be false while building distributions without plugins
|
||||||
self.assertTrue("--nginx-ctl" in out)
|
self.assertTrue("--nginx-ctl" in out)
|
||||||
self.assertTrue("--manual-test-mode" not in out)
|
self.assertTrue("--webroot-path" not in out)
|
||||||
self.assertTrue("--checkpoints" not in out)
|
self.assertTrue("--checkpoints" not in out)
|
||||||
|
|
||||||
out = self._help_output(['-h'])
|
out = self._help_output(['-h'])
|
||||||
@@ -102,7 +102,7 @@ class ParseTest(unittest.TestCase):
|
|||||||
self.assertTrue("(the certbot nginx plugin is not" in out)
|
self.assertTrue("(the certbot nginx plugin is not" in out)
|
||||||
|
|
||||||
out = self._help_output(['--help', 'plugins'])
|
out = self._help_output(['--help', 'plugins'])
|
||||||
self.assertTrue("--manual-test-mode" not in out)
|
self.assertTrue("--webroot-path" not in out)
|
||||||
self.assertTrue("--prepare" in out)
|
self.assertTrue("--prepare" in out)
|
||||||
self.assertTrue('"plugins" subcommand' in out)
|
self.assertTrue('"plugins" subcommand' in out)
|
||||||
|
|
||||||
@@ -305,22 +305,22 @@ class SetByCliTest(unittest.TestCase):
|
|||||||
|
|
||||||
def test_report_config_interaction_str(self):
|
def test_report_config_interaction_str(self):
|
||||||
cli.report_config_interaction('manual_public_ip_logging_ok',
|
cli.report_config_interaction('manual_public_ip_logging_ok',
|
||||||
'manual_test_mode')
|
'manual_auth_hook')
|
||||||
cli.report_config_interaction('manual_test_mode', 'manual')
|
cli.report_config_interaction('manual_auth_hook', 'manual')
|
||||||
|
|
||||||
self._test_report_config_interaction_common()
|
self._test_report_config_interaction_common()
|
||||||
|
|
||||||
def test_report_config_interaction_iterable(self):
|
def test_report_config_interaction_iterable(self):
|
||||||
cli.report_config_interaction(('manual_public_ip_logging_ok',),
|
cli.report_config_interaction(('manual_public_ip_logging_ok',),
|
||||||
('manual_test_mode',))
|
('manual_auth_hook',))
|
||||||
cli.report_config_interaction(('manual_test_mode',), ('manual',))
|
cli.report_config_interaction(('manual_auth_hook',), ('manual',))
|
||||||
|
|
||||||
self._test_report_config_interaction_common()
|
self._test_report_config_interaction_common()
|
||||||
|
|
||||||
def _test_report_config_interaction_common(self):
|
def _test_report_config_interaction_common(self):
|
||||||
"""Tests implied interaction between manual flags.
|
"""Tests implied interaction between manual flags.
|
||||||
|
|
||||||
--manual implies --manual-test-mode which implies
|
--manual implies --manual-auth-hook which implies
|
||||||
--manual-public-ip-logging-ok. These interactions don't actually
|
--manual-public-ip-logging-ok. These interactions don't actually
|
||||||
exist in the client, but are used here for testing purposes.
|
exist in the client, but are used here for testing purposes.
|
||||||
|
|
||||||
@@ -328,13 +328,13 @@ class SetByCliTest(unittest.TestCase):
|
|||||||
|
|
||||||
args = ['--manual']
|
args = ['--manual']
|
||||||
verb = 'renew'
|
verb = 'renew'
|
||||||
for v in ('manual', 'manual_test_mode', 'manual_public_ip_logging_ok'):
|
for v in ('manual', 'manual_auth_hook', 'manual_public_ip_logging_ok'):
|
||||||
self.assertTrue(_call_set_by_cli(v, args, verb))
|
self.assertTrue(_call_set_by_cli(v, args, verb))
|
||||||
|
|
||||||
cli.set_by_cli.detector = None
|
cli.set_by_cli.detector = None
|
||||||
|
|
||||||
args = ['--manual-test-mode']
|
args = ['--manual-auth-hook', 'command']
|
||||||
for v in ('manual_test_mode', 'manual_public_ip_logging_ok'):
|
for v in ('manual_auth_hook', 'manual_public_ip_logging_ok'):
|
||||||
self.assertTrue(_call_set_by_cli(v, args, verb))
|
self.assertTrue(_call_set_by_cli(v, args, verb))
|
||||||
|
|
||||||
self.assertFalse(_call_set_by_cli('manual', args, verb))
|
self.assertFalse(_call_set_by_cli('manual', args, verb))
|
||||||
|
|||||||
@@ -131,7 +131,6 @@ setup(
|
|||||||
'null = certbot.plugins.null:Installer',
|
'null = certbot.plugins.null:Installer',
|
||||||
'standalone = certbot.plugins.standalone:Authenticator',
|
'standalone = certbot.plugins.standalone:Authenticator',
|
||||||
'webroot = certbot.plugins.webroot:Authenticator',
|
'webroot = certbot.plugins.webroot:Authenticator',
|
||||||
'script = certbot.plugins.script:Authenticator',
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -44,7 +44,12 @@ python_server_pid=$!
|
|||||||
common --domains le2.wtf --preferred-challenges http-01 run
|
common --domains le2.wtf --preferred-challenges http-01 run
|
||||||
kill $python_server_pid
|
kill $python_server_pid
|
||||||
|
|
||||||
common -a manual -d le.wtf auth --rsa-key-size 4096
|
common certonly -a manual -d le.wtf --rsa-key-size 4096 \
|
||||||
|
--manual-auth-hook ./tests/manual-http-auth.sh \
|
||||||
|
--manual-cleanup-hook ./tests/manual-http-cleanup.sh
|
||||||
|
|
||||||
|
common certonly -a manual -d dns.le.wtf --preferred-challenges dns-01 \
|
||||||
|
--manual-auth-hook ./tests/manual-dns-auth.sh
|
||||||
|
|
||||||
export CSR_PATH="${root}/csr.der" KEY_PATH="${root}/key.pem" \
|
export CSR_PATH="${root}/csr.der" KEY_PATH="${root}/key.pem" \
|
||||||
OPENSSL_CNF=examples/openssl.cnf
|
OPENSSL_CNF=examples/openssl.cnf
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ certbot_test_no_force_renew () {
|
|||||||
--no-verify-ssl \
|
--no-verify-ssl \
|
||||||
--tls-sni-01-port $tls_sni_01_port \
|
--tls-sni-01-port $tls_sni_01_port \
|
||||||
--http-01-port $http_01_port \
|
--http-01-port $http_01_port \
|
||||||
--manual-test-mode \
|
--manual-public-ip-logging-ok \
|
||||||
$store_flags \
|
$store_flags \
|
||||||
--non-interactive \
|
--non-interactive \
|
||||||
--no-redirect \
|
--no-redirect \
|
||||||
|
|||||||
Executable
+4
@@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
curl -X POST 'http://localhost:8055/set-txt' -d \
|
||||||
|
"{\"host\": \"_acme-challenge.$CERTBOT_DOMAIN.\", \
|
||||||
|
\"value\": \"$CERTBOT_VALIDATION\"}"
|
||||||
Executable
+12
@@ -0,0 +1,12 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
uri_path=".well-known/acme-challenge/$CERTBOT_TOKEN"
|
||||||
|
|
||||||
|
cd $(mktemp -d)
|
||||||
|
mkdir -p $(dirname $uri_path)
|
||||||
|
echo $CERTBOT_VALIDATION > $uri_path
|
||||||
|
python -m SimpleHTTPServer $http_01_port >/dev/null 2>&1 &
|
||||||
|
server_pid=$!
|
||||||
|
while ! curl "http://localhost:$http_01_port/$uri_path" >/dev/null 2>&1; do
|
||||||
|
sleep 1s
|
||||||
|
done
|
||||||
|
echo $server_pid
|
||||||
Executable
+2
@@ -0,0 +1,2 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
kill $CERTBOT_AUTH_OUTPUT
|
||||||
Reference in New Issue
Block a user