mirror of
https://github.com/certbot/certbot.git
synced 2026-08-03 03:52:02 +02:00
Merge pull request #5417 from certbot/apache-http
HTTP01 support in Apache
This commit is contained in:
@@ -24,9 +24,10 @@ from certbot_apache import apache_util
|
|||||||
from certbot_apache import augeas_configurator
|
from certbot_apache import augeas_configurator
|
||||||
from certbot_apache import constants
|
from certbot_apache import constants
|
||||||
from certbot_apache import display_ops
|
from certbot_apache import display_ops
|
||||||
from certbot_apache import tls_sni_01
|
from certbot_apache import http_01
|
||||||
from certbot_apache import obj
|
from certbot_apache import obj
|
||||||
from certbot_apache import parser
|
from certbot_apache import parser
|
||||||
|
from certbot_apache import tls_sni_01
|
||||||
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
@@ -163,6 +164,9 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
|||||||
"ensure-http-header": self._set_http_header,
|
"ensure-http-header": self._set_http_header,
|
||||||
"staple-ocsp": self._enable_ocsp_stapling}
|
"staple-ocsp": self._enable_ocsp_stapling}
|
||||||
|
|
||||||
|
# This will be set during the perform function
|
||||||
|
self.http_doer = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def mod_ssl_conf(self):
|
def mod_ssl_conf(self):
|
||||||
"""Full absolute path to SSL configuration file."""
|
"""Full absolute path to SSL configuration file."""
|
||||||
@@ -736,31 +740,43 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# If nonstandard port, add service definition for matching
|
self.prepare_https_modules(temp)
|
||||||
if port != "443":
|
self.ensure_listen(port, https=True)
|
||||||
|
|
||||||
|
def ensure_listen(self, port, https=False):
|
||||||
|
"""Make sure that Apache is listening on the port. Checks if the
|
||||||
|
Listen statement for the port already exists, and adds it to the
|
||||||
|
configuration if necessary.
|
||||||
|
|
||||||
|
:param str port: Port number to check and add Listen for if not in
|
||||||
|
place already
|
||||||
|
:param bool https: If the port will be used for HTTPS
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# If HTTPS requested for nonstandard port, add service definition
|
||||||
|
if https and port != "443":
|
||||||
port_service = "%s %s" % (port, "https")
|
port_service = "%s %s" % (port, "https")
|
||||||
else:
|
else:
|
||||||
port_service = port
|
port_service = port
|
||||||
|
|
||||||
self.prepare_https_modules(temp)
|
|
||||||
# Check for Listen <port>
|
# Check for Listen <port>
|
||||||
# Note: This could be made to also look for ip:443 combo
|
# Note: This could be made to also look for ip:443 combo
|
||||||
listens = [self.parser.get_arg(x).split()[0] for
|
listens = [self.parser.get_arg(x).split()[0] for
|
||||||
x in self.parser.find_dir("Listen")]
|
x in self.parser.find_dir("Listen")]
|
||||||
|
|
||||||
# In case no Listens are set (which really is a broken apache config)
|
|
||||||
if not listens:
|
|
||||||
listens = ["80"]
|
|
||||||
|
|
||||||
# Listen already in place
|
# Listen already in place
|
||||||
if self._has_port_already(listens, port):
|
if self._has_port_already(listens, port):
|
||||||
return
|
return
|
||||||
|
|
||||||
listen_dirs = set(listens)
|
listen_dirs = set(listens)
|
||||||
|
|
||||||
|
if not listens:
|
||||||
|
listen_dirs.add(port_service)
|
||||||
|
|
||||||
for listen in listens:
|
for listen in listens:
|
||||||
# For any listen statement, check if the machine also listens on
|
# For any listen statement, check if the machine also listens on
|
||||||
# Port 443. If not, add such a listen statement.
|
# the given port. If not, add such a listen statement.
|
||||||
if len(listen.split(":")) == 1:
|
if len(listen.split(":")) == 1:
|
||||||
# Its listening to all interfaces
|
# Its listening to all interfaces
|
||||||
if port not in listen_dirs and port_service not in listen_dirs:
|
if port not in listen_dirs and port_service not in listen_dirs:
|
||||||
@@ -772,11 +788,39 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
|||||||
if "%s:%s" % (ip, port_service) not in listen_dirs and (
|
if "%s:%s" % (ip, port_service) not in listen_dirs and (
|
||||||
"%s:%s" % (ip, port_service) not in listen_dirs):
|
"%s:%s" % (ip, port_service) not in listen_dirs):
|
||||||
listen_dirs.add("%s:%s" % (ip, port_service))
|
listen_dirs.add("%s:%s" % (ip, port_service))
|
||||||
self._add_listens(listen_dirs, listens, port)
|
if https:
|
||||||
|
self._add_listens_https(listen_dirs, listens, port)
|
||||||
|
else:
|
||||||
|
self._add_listens_http(listen_dirs, listens, port)
|
||||||
|
|
||||||
def _add_listens(self, listens, listens_orig, port):
|
def _add_listens_http(self, listens, listens_orig, port):
|
||||||
"""Helper method for prepare_server_https to figure out which new
|
"""Helper method for ensure_listen to figure out which new
|
||||||
listen statements need adding
|
listen statements need adding for listening HTTP on port
|
||||||
|
|
||||||
|
:param set listens: Set of all needed Listen statements
|
||||||
|
:param list listens_orig: List of existing listen statements
|
||||||
|
:param string port: Port number we're adding
|
||||||
|
"""
|
||||||
|
|
||||||
|
new_listens = listens.difference(listens_orig)
|
||||||
|
|
||||||
|
if port in new_listens:
|
||||||
|
# We have wildcard, skip the rest
|
||||||
|
self.parser.add_dir(parser.get_aug_path(self.parser.loc["listen"]),
|
||||||
|
"Listen", port)
|
||||||
|
self.save_notes += "Added Listen %s directive to %s\n" % (
|
||||||
|
port, self.parser.loc["listen"])
|
||||||
|
else:
|
||||||
|
for listen in new_listens:
|
||||||
|
self.parser.add_dir(parser.get_aug_path(
|
||||||
|
self.parser.loc["listen"]), "Listen", listen.split(" "))
|
||||||
|
self.save_notes += ("Added Listen %s directive to "
|
||||||
|
"%s\n") % (listen,
|
||||||
|
self.parser.loc["listen"])
|
||||||
|
|
||||||
|
def _add_listens_https(self, listens, listens_orig, port):
|
||||||
|
"""Helper method for ensure_listen to figure out which new
|
||||||
|
listen statements need adding for listening HTTPS on port
|
||||||
|
|
||||||
:param set listens: Set of all needed Listen statements
|
:param set listens: Set of all needed Listen statements
|
||||||
:param list listens_orig: List of existing listen statements
|
:param list listens_orig: List of existing listen statements
|
||||||
@@ -1855,7 +1899,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
|||||||
###########################################################################
|
###########################################################################
|
||||||
def get_chall_pref(self, unused_domain): # pylint: disable=no-self-use
|
def get_chall_pref(self, unused_domain): # pylint: disable=no-self-use
|
||||||
"""Return list of challenge preferences."""
|
"""Return list of challenge preferences."""
|
||||||
return [challenges.TLSSNI01]
|
return [challenges.TLSSNI01, challenges.HTTP01]
|
||||||
|
|
||||||
def perform(self, achalls):
|
def perform(self, achalls):
|
||||||
"""Perform the configuration related challenge.
|
"""Perform the configuration related challenge.
|
||||||
@@ -1867,16 +1911,21 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
|||||||
"""
|
"""
|
||||||
self._chall_out.update(achalls)
|
self._chall_out.update(achalls)
|
||||||
responses = [None] * len(achalls)
|
responses = [None] * len(achalls)
|
||||||
chall_doer = tls_sni_01.ApacheTlsSni01(self)
|
self.http_doer = http_01.ApacheHttp01(self)
|
||||||
|
sni_doer = tls_sni_01.ApacheTlsSni01(self)
|
||||||
|
|
||||||
for i, achall in enumerate(achalls):
|
for i, achall in enumerate(achalls):
|
||||||
# Currently also have chall_doer hold associated index of the
|
# Currently also have chall_doer hold associated index of the
|
||||||
# challenge. This helps to put all of the responses back together
|
# challenge. This helps to put all of the responses back together
|
||||||
# when they are all complete.
|
# when they are all complete.
|
||||||
chall_doer.add_chall(achall, i)
|
if isinstance(achall.chall, challenges.HTTP01):
|
||||||
|
self.http_doer.add_chall(achall, i)
|
||||||
|
else: # tls-sni-01
|
||||||
|
sni_doer.add_chall(achall, i)
|
||||||
|
|
||||||
sni_response = chall_doer.perform()
|
http_response = self.http_doer.perform()
|
||||||
if sni_response:
|
sni_response = sni_doer.perform()
|
||||||
|
if http_response or sni_response:
|
||||||
# Must reload in order to activate the challenges.
|
# Must reload in order to activate the challenges.
|
||||||
# Handled here because we may be able to load up other challenge
|
# Handled here because we may be able to load up other challenge
|
||||||
# types
|
# types
|
||||||
@@ -1886,14 +1935,18 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
|||||||
# of identifying when the new configuration is being used.
|
# of identifying when the new configuration is being used.
|
||||||
time.sleep(3)
|
time.sleep(3)
|
||||||
|
|
||||||
|
self._update_responses(responses, http_response, self.http_doer)
|
||||||
|
self._update_responses(responses, sni_response, sni_doer)
|
||||||
|
|
||||||
|
return responses
|
||||||
|
|
||||||
|
def _update_responses(self, responses, chall_response, chall_doer):
|
||||||
# Go through all of the challenges and assign them to the proper
|
# Go through all of the challenges and assign them to the proper
|
||||||
# place in the responses return value. All responses must be in the
|
# place in the responses return value. All responses must be in the
|
||||||
# same order as the original challenges.
|
# same order as the original challenges.
|
||||||
for i, resp in enumerate(sni_response):
|
for i, resp in enumerate(chall_response):
|
||||||
responses[chall_doer.indices[i]] = resp
|
responses[chall_doer.indices[i]] = resp
|
||||||
|
|
||||||
return responses
|
|
||||||
|
|
||||||
def cleanup(self, achalls):
|
def cleanup(self, achalls):
|
||||||
"""Revert all challenges."""
|
"""Revert all challenges."""
|
||||||
self._chall_out.difference_update(achalls)
|
self._chall_out.difference_update(achalls)
|
||||||
@@ -1903,6 +1956,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
|||||||
self.revert_challenge_config()
|
self.revert_challenge_config()
|
||||||
self.restart()
|
self.restart()
|
||||||
self.parser.reset_modules()
|
self.parser.reset_modules()
|
||||||
|
self.http_doer.cleanup()
|
||||||
|
|
||||||
def install_ssl_options_conf(self, options_ssl, options_ssl_digest):
|
def install_ssl_options_conf(self, options_ssl, options_ssl_digest):
|
||||||
"""Copy Certbot's SSL options file into the system's config dir if required."""
|
"""Copy Certbot's SSL options file into the system's config dir if required."""
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
"""A class that performs HTTP-01 challenges for Apache"""
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
from certbot.plugins import common
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class ApacheHttp01(common.TLSSNI01):
|
||||||
|
"""Class that performs HTPP-01 challenges within the Apache configurator."""
|
||||||
|
|
||||||
|
CONFIG_TEMPLATE24 = """\
|
||||||
|
Alias /.well-known/acme-challenge {0}
|
||||||
|
|
||||||
|
<Directory {0} >
|
||||||
|
Require all granted
|
||||||
|
</Directory>
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
CONFIG_TEMPLATE22 = """\
|
||||||
|
Alias /.well-known/acme-challenge {0}
|
||||||
|
|
||||||
|
<Directory {0} >
|
||||||
|
Order allow,deny
|
||||||
|
Allow from all
|
||||||
|
</Directory>
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(ApacheHttp01, self).__init__(*args, **kwargs)
|
||||||
|
self.challenge_conf = os.path.join(
|
||||||
|
self.configurator.conf("challenge-location"),
|
||||||
|
"le_http_01_challenge.conf")
|
||||||
|
self.challenge_dir = None
|
||||||
|
|
||||||
|
def perform(self):
|
||||||
|
"""Perform all HTTP-01 challenges."""
|
||||||
|
if not self.achalls:
|
||||||
|
return []
|
||||||
|
# Save any changes to the configuration as a precaution
|
||||||
|
# About to make temporary changes to the config
|
||||||
|
self.configurator.save("Changes before challenge setup", True)
|
||||||
|
|
||||||
|
self.configurator.ensure_listen(str(
|
||||||
|
self.configurator.config.http01_port))
|
||||||
|
self.prepare_http01_modules()
|
||||||
|
|
||||||
|
responses = self._set_up_challenges()
|
||||||
|
self._mod_config()
|
||||||
|
# Save reversible changes
|
||||||
|
self.configurator.save("HTTP Challenge", True)
|
||||||
|
|
||||||
|
return responses
|
||||||
|
|
||||||
|
def cleanup(self):
|
||||||
|
"""Cleanup the challenge directory."""
|
||||||
|
if self.challenge_dir:
|
||||||
|
shutil.rmtree(self.challenge_dir, ignore_errors=True)
|
||||||
|
self.challenge_dir = None
|
||||||
|
|
||||||
|
def prepare_http01_modules(self):
|
||||||
|
"""Make sure that we have the needed modules available for http01"""
|
||||||
|
|
||||||
|
if self.configurator.conf("handle-modules"):
|
||||||
|
needed_modules = ["alias"]
|
||||||
|
if self.configurator.version < (2, 4):
|
||||||
|
needed_modules.append("authz_host")
|
||||||
|
else:
|
||||||
|
needed_modules.append("authz_core")
|
||||||
|
for mod in needed_modules:
|
||||||
|
if mod + "_module" not in self.configurator.parser.modules:
|
||||||
|
self.configurator.enable_mod(mod, temp=True)
|
||||||
|
|
||||||
|
def _mod_config(self):
|
||||||
|
self.configurator.parser.add_include(
|
||||||
|
self.configurator.parser.loc["default"], self.challenge_conf)
|
||||||
|
self.configurator.reverter.register_file_creation(
|
||||||
|
True, self.challenge_conf)
|
||||||
|
|
||||||
|
if self.configurator.version < (2, 4):
|
||||||
|
config_template = self.CONFIG_TEMPLATE22
|
||||||
|
else:
|
||||||
|
config_template = self.CONFIG_TEMPLATE24
|
||||||
|
config_text = config_template.format(self.challenge_dir)
|
||||||
|
|
||||||
|
logger.debug("writing a config file with text:\n %s", config_text)
|
||||||
|
with open(self.challenge_conf, "w") as new_conf:
|
||||||
|
new_conf.write(config_text)
|
||||||
|
|
||||||
|
def _set_up_challenges(self):
|
||||||
|
self.challenge_dir = tempfile.mkdtemp()
|
||||||
|
os.chmod(self.challenge_dir, 0o755)
|
||||||
|
|
||||||
|
responses = []
|
||||||
|
for achall in self.achalls:
|
||||||
|
responses.append(self._set_up_challenge(achall))
|
||||||
|
|
||||||
|
return responses
|
||||||
|
|
||||||
|
def _set_up_challenge(self, achall):
|
||||||
|
response, validation = achall.response_and_validation()
|
||||||
|
|
||||||
|
name = os.path.join(self.challenge_dir, achall.chall.encode("token"))
|
||||||
|
with open(name, 'wb') as f:
|
||||||
|
f.write(validation.encode())
|
||||||
|
os.chmod(name, 0o644)
|
||||||
|
|
||||||
|
return response
|
||||||
@@ -140,5 +140,5 @@ class DebianConfigurator(configurator.ApacheConfigurator):
|
|||||||
"a2dismod are configured correctly for certbot.")
|
"a2dismod are configured correctly for certbot.")
|
||||||
|
|
||||||
self.reverter.register_undo_command(
|
self.reverter.register_undo_command(
|
||||||
temp, [self.conf("dismod"), mod_name])
|
temp, [self.conf("dismod"), "-f", mod_name])
|
||||||
util.run_script([self.conf("enmod"), mod_name])
|
util.run_script([self.conf("enmod"), mod_name])
|
||||||
|
|||||||
@@ -424,6 +424,43 @@ class MultipleVhostsTest(util.ApacheTest):
|
|||||||
self.assertTrue(self.config.parser.find_dir(
|
self.assertTrue(self.config.parser.find_dir(
|
||||||
"NameVirtualHost", "*:80"))
|
"NameVirtualHost", "*:80"))
|
||||||
|
|
||||||
|
def test_add_listen_80(self):
|
||||||
|
mock_find = mock.Mock()
|
||||||
|
mock_add_dir = mock.Mock()
|
||||||
|
mock_find.return_value = []
|
||||||
|
self.config.parser.find_dir = mock_find
|
||||||
|
self.config.parser.add_dir = mock_add_dir
|
||||||
|
self.config.ensure_listen("80")
|
||||||
|
self.assertTrue(mock_add_dir.called)
|
||||||
|
self.assertTrue(mock_find.called)
|
||||||
|
self.assertEqual(mock_add_dir.call_args[0][1], "Listen")
|
||||||
|
self.assertEqual(mock_add_dir.call_args[0][2], "80")
|
||||||
|
|
||||||
|
def test_add_listen_80_named(self):
|
||||||
|
mock_find = mock.Mock()
|
||||||
|
mock_find.return_value = ["test1", "test2", "test3"]
|
||||||
|
mock_get = mock.Mock()
|
||||||
|
mock_get.side_effect = ["1.2.3.4:80", "[::1]:80", "1.1.1.1:443"]
|
||||||
|
mock_add_dir = mock.Mock()
|
||||||
|
|
||||||
|
self.config.parser.find_dir = mock_find
|
||||||
|
self.config.parser.get_arg = mock_get
|
||||||
|
self.config.parser.add_dir = mock_add_dir
|
||||||
|
|
||||||
|
self.config.ensure_listen("80")
|
||||||
|
self.assertEqual(mock_add_dir.call_count, 0)
|
||||||
|
|
||||||
|
# Reset return lists and inputs
|
||||||
|
mock_add_dir.reset_mock()
|
||||||
|
mock_get.side_effect = ["1.2.3.4:80", "[::1]:80", "1.1.1.1:443"]
|
||||||
|
|
||||||
|
# Test
|
||||||
|
self.config.ensure_listen("8080")
|
||||||
|
self.assertEqual(mock_add_dir.call_count, 3)
|
||||||
|
self.assertTrue(mock_add_dir.called)
|
||||||
|
self.assertEqual(mock_add_dir.call_args[0][1], "Listen")
|
||||||
|
self.assertEqual(mock_add_dir.call_args[0][2], ['1.2.3.4:8080'])
|
||||||
|
|
||||||
def test_prepare_server_https(self):
|
def test_prepare_server_https(self):
|
||||||
mock_enable = mock.Mock()
|
mock_enable = mock.Mock()
|
||||||
self.config.enable_mod = mock_enable
|
self.config.enable_mod = mock_enable
|
||||||
@@ -435,7 +472,6 @@ class MultipleVhostsTest(util.ApacheTest):
|
|||||||
# This will test the Add listen
|
# This will test the Add listen
|
||||||
self.config.parser.find_dir = mock_find
|
self.config.parser.find_dir = mock_find
|
||||||
self.config.parser.add_dir_to_ifmodssl = mock_add_dir
|
self.config.parser.add_dir_to_ifmodssl = mock_add_dir
|
||||||
|
|
||||||
self.config.prepare_server_https("443")
|
self.config.prepare_server_https("443")
|
||||||
# Changing the order these modules are enabled breaks the reverter
|
# Changing the order these modules are enabled breaks the reverter
|
||||||
self.assertEqual(mock_enable.call_args_list[0][0][0], "socache_shmcb")
|
self.assertEqual(mock_enable.call_args_list[0][0][0], "socache_shmcb")
|
||||||
@@ -676,23 +712,33 @@ class MultipleVhostsTest(util.ApacheTest):
|
|||||||
self.config._add_name_vhost_if_necessary(self.vh_truth[0])
|
self.config._add_name_vhost_if_necessary(self.vh_truth[0])
|
||||||
self.assertEqual(self.config.add_name_vhost.call_count, 2)
|
self.assertEqual(self.config.add_name_vhost.call_count, 2)
|
||||||
|
|
||||||
|
@mock.patch("certbot_apache.configurator.http_01.ApacheHttp01.perform")
|
||||||
@mock.patch("certbot_apache.configurator.tls_sni_01.ApacheTlsSni01.perform")
|
@mock.patch("certbot_apache.configurator.tls_sni_01.ApacheTlsSni01.perform")
|
||||||
@mock.patch("certbot_apache.configurator.ApacheConfigurator.restart")
|
@mock.patch("certbot_apache.configurator.ApacheConfigurator.restart")
|
||||||
def test_perform(self, mock_restart, mock_perform):
|
def test_perform(self, mock_restart, mock_tls_perform, mock_http_perform):
|
||||||
# Only tests functionality specific to configurator.perform
|
# Only tests functionality specific to configurator.perform
|
||||||
# Note: As more challenges are offered this will have to be expanded
|
# Note: As more challenges are offered this will have to be expanded
|
||||||
account_key, achall1, achall2 = self.get_achalls()
|
account_key, achalls = self.get_key_and_achalls()
|
||||||
|
|
||||||
expected = [
|
all_expected = []
|
||||||
achall1.response(account_key),
|
http_expected = []
|
||||||
achall2.response(account_key),
|
tls_expected = []
|
||||||
]
|
for achall in achalls:
|
||||||
|
response = achall.response(account_key)
|
||||||
|
if isinstance(achall.chall, challenges.HTTP01):
|
||||||
|
http_expected.append(response)
|
||||||
|
else:
|
||||||
|
tls_expected.append(response)
|
||||||
|
all_expected.append(response)
|
||||||
|
|
||||||
mock_perform.return_value = expected
|
mock_http_perform.return_value = http_expected
|
||||||
responses = self.config.perform([achall1, achall2])
|
mock_tls_perform.return_value = tls_expected
|
||||||
|
|
||||||
self.assertEqual(mock_perform.call_count, 1)
|
responses = self.config.perform(achalls)
|
||||||
self.assertEqual(responses, expected)
|
|
||||||
|
self.assertEqual(mock_http_perform.call_count, 1)
|
||||||
|
self.assertEqual(mock_tls_perform.call_count, 1)
|
||||||
|
self.assertEqual(responses, all_expected)
|
||||||
|
|
||||||
self.assertEqual(mock_restart.call_count, 1)
|
self.assertEqual(mock_restart.call_count, 1)
|
||||||
|
|
||||||
@@ -700,30 +746,38 @@ class MultipleVhostsTest(util.ApacheTest):
|
|||||||
@mock.patch("certbot_apache.parser.ApacheParser._get_runtime_cfg")
|
@mock.patch("certbot_apache.parser.ApacheParser._get_runtime_cfg")
|
||||||
def test_cleanup(self, mock_cfg, mock_restart):
|
def test_cleanup(self, mock_cfg, mock_restart):
|
||||||
mock_cfg.return_value = ""
|
mock_cfg.return_value = ""
|
||||||
_, achall1, achall2 = self.get_achalls()
|
_, achalls = self.get_key_and_achalls()
|
||||||
|
self.config.http_doer = mock.MagicMock()
|
||||||
|
|
||||||
self.config._chall_out.add(achall1) # pylint: disable=protected-access
|
for achall in achalls:
|
||||||
self.config._chall_out.add(achall2) # pylint: disable=protected-access
|
self.config._chall_out.add(achall) # pylint: disable=protected-access
|
||||||
|
|
||||||
self.config.cleanup([achall1])
|
for i, achall in enumerate(achalls):
|
||||||
self.assertFalse(mock_restart.called)
|
self.config.cleanup([achall])
|
||||||
|
if i == len(achalls) - 1:
|
||||||
self.config.cleanup([achall2])
|
|
||||||
self.assertTrue(mock_restart.called)
|
self.assertTrue(mock_restart.called)
|
||||||
|
self.assertTrue(self.config.http_doer.cleanup.called)
|
||||||
|
else:
|
||||||
|
self.assertFalse(mock_restart.called)
|
||||||
|
self.assertFalse(self.config.http_doer.cleanup.called)
|
||||||
|
|
||||||
@mock.patch("certbot_apache.configurator.ApacheConfigurator.restart")
|
@mock.patch("certbot_apache.configurator.ApacheConfigurator.restart")
|
||||||
@mock.patch("certbot_apache.parser.ApacheParser._get_runtime_cfg")
|
@mock.patch("certbot_apache.parser.ApacheParser._get_runtime_cfg")
|
||||||
def test_cleanup_no_errors(self, mock_cfg, mock_restart):
|
def test_cleanup_no_errors(self, mock_cfg, mock_restart):
|
||||||
mock_cfg.return_value = ""
|
mock_cfg.return_value = ""
|
||||||
_, achall1, achall2 = self.get_achalls()
|
_, achalls = self.get_key_and_achalls()
|
||||||
|
self.config.http_doer = mock.MagicMock()
|
||||||
|
|
||||||
self.config._chall_out.add(achall1) # pylint: disable=protected-access
|
for achall in achalls:
|
||||||
|
self.config._chall_out.add(achall) # pylint: disable=protected-access
|
||||||
|
|
||||||
self.config.cleanup([achall2])
|
self.config.cleanup([achalls[-1]])
|
||||||
self.assertFalse(mock_restart.called)
|
self.assertFalse(mock_restart.called)
|
||||||
|
self.assertFalse(self.config.http_doer.cleanup.called)
|
||||||
|
|
||||||
self.config.cleanup([achall1, achall2])
|
self.config.cleanup(achalls)
|
||||||
self.assertTrue(mock_restart.called)
|
self.assertTrue(mock_restart.called)
|
||||||
|
self.assertTrue(self.config.http_doer.cleanup.called)
|
||||||
|
|
||||||
@mock.patch("certbot.util.run_script")
|
@mock.patch("certbot.util.run_script")
|
||||||
def test_get_version(self, mock_script):
|
def test_get_version(self, mock_script):
|
||||||
@@ -1151,7 +1205,7 @@ class MultipleVhostsTest(util.ApacheTest):
|
|||||||
not_rewriterule = "NotRewriteRule ^ ..."
|
not_rewriterule = "NotRewriteRule ^ ..."
|
||||||
self.assertFalse(self.config._sift_rewrite_rule(not_rewriterule))
|
self.assertFalse(self.config._sift_rewrite_rule(not_rewriterule))
|
||||||
|
|
||||||
def get_achalls(self):
|
def get_key_and_achalls(self):
|
||||||
"""Return testing achallenges."""
|
"""Return testing achallenges."""
|
||||||
account_key = self.rsa512jwk
|
account_key = self.rsa512jwk
|
||||||
achall1 = achallenges.KeyAuthorizationAnnotatedChallenge(
|
achall1 = achallenges.KeyAuthorizationAnnotatedChallenge(
|
||||||
@@ -1166,8 +1220,12 @@ class MultipleVhostsTest(util.ApacheTest):
|
|||||||
token=b"uqnaPzxtrndteOqtrXb0Asl5gOJfWAnnx6QJyvcmlDU"),
|
token=b"uqnaPzxtrndteOqtrXb0Asl5gOJfWAnnx6QJyvcmlDU"),
|
||||||
"pending"),
|
"pending"),
|
||||||
domain="certbot.demo", account_key=account_key)
|
domain="certbot.demo", account_key=account_key)
|
||||||
|
achall3 = achallenges.KeyAuthorizationAnnotatedChallenge(
|
||||||
|
challb=acme_util.chall_to_challb(
|
||||||
|
challenges.HTTP01(token=(b'x' * 16)), "pending"),
|
||||||
|
domain="example.org", account_key=account_key)
|
||||||
|
|
||||||
return account_key, achall1, achall2
|
return account_key, (achall1, achall2, achall3)
|
||||||
|
|
||||||
def test_make_addrs_sni_ready(self):
|
def test_make_addrs_sni_ready(self):
|
||||||
self.config.version = (2, 2)
|
self.config.version = (2, 2)
|
||||||
|
|||||||
@@ -0,0 +1,152 @@
|
|||||||
|
"""Test for certbot_apache.http_01."""
|
||||||
|
import mock
|
||||||
|
import os
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from acme import challenges
|
||||||
|
|
||||||
|
from certbot import achallenges
|
||||||
|
|
||||||
|
from certbot.tests import acme_util
|
||||||
|
|
||||||
|
from certbot_apache.tests import util
|
||||||
|
|
||||||
|
|
||||||
|
NUM_ACHALLS = 3
|
||||||
|
|
||||||
|
|
||||||
|
class ApacheHttp01TestMeta(type):
|
||||||
|
"""Generates parmeterized tests for testing perform."""
|
||||||
|
def __new__(mcs, name, bases, class_dict):
|
||||||
|
|
||||||
|
def _gen_test(num_achalls, minor_version):
|
||||||
|
def _test(self):
|
||||||
|
achalls = self.achalls[:num_achalls]
|
||||||
|
self.config.version = (2, minor_version)
|
||||||
|
self.common_perform_test(achalls)
|
||||||
|
return _test
|
||||||
|
|
||||||
|
for i in range(1, NUM_ACHALLS + 1):
|
||||||
|
for j in (2, 4):
|
||||||
|
test_name = "test_perform_{0}_{1}".format(i, j)
|
||||||
|
class_dict[test_name] = _gen_test(i, j)
|
||||||
|
return type.__new__(mcs, name, bases, class_dict)
|
||||||
|
|
||||||
|
|
||||||
|
class ApacheHttp01Test(util.ApacheTest):
|
||||||
|
"""Test for certbot_apache.http_01.ApacheHttp01."""
|
||||||
|
|
||||||
|
__metaclass__ = ApacheHttp01TestMeta
|
||||||
|
|
||||||
|
def setUp(self, *args, **kwargs):
|
||||||
|
super(ApacheHttp01Test, self).setUp(*args, **kwargs)
|
||||||
|
|
||||||
|
self.account_key = self.rsa512jwk
|
||||||
|
self.achalls = []
|
||||||
|
for i in range(NUM_ACHALLS):
|
||||||
|
self.achalls.append(
|
||||||
|
achallenges.KeyAuthorizationAnnotatedChallenge(
|
||||||
|
challb=acme_util.chall_to_challb(
|
||||||
|
challenges.HTTP01(token=((chr(ord('a') + i) * 16))),
|
||||||
|
"pending"),
|
||||||
|
domain="example{0}.com".format(i),
|
||||||
|
account_key=self.account_key))
|
||||||
|
|
||||||
|
modules = ["alias", "authz_core", "authz_host"]
|
||||||
|
for mod in modules:
|
||||||
|
self.config.parser.modules.add("mod_{0}.c".format(mod))
|
||||||
|
self.config.parser.modules.add(mod + "_module")
|
||||||
|
|
||||||
|
from certbot_apache.http_01 import ApacheHttp01
|
||||||
|
self.http = ApacheHttp01(self.config)
|
||||||
|
|
||||||
|
def test_empty_perform(self):
|
||||||
|
self.assertFalse(self.http.perform())
|
||||||
|
|
||||||
|
@mock.patch("certbot_apache.configurator.ApacheConfigurator.enable_mod")
|
||||||
|
def test_enable_modules_22(self, mock_enmod):
|
||||||
|
self.config.version = (2, 2)
|
||||||
|
self.config.parser.modules.remove("authz_host_module")
|
||||||
|
self.config.parser.modules.remove("mod_authz_host.c")
|
||||||
|
|
||||||
|
enmod_calls = self.common_enable_modules_test(mock_enmod)
|
||||||
|
self.assertEqual(enmod_calls[0][0][0], "authz_host")
|
||||||
|
|
||||||
|
@mock.patch("certbot_apache.configurator.ApacheConfigurator.enable_mod")
|
||||||
|
def test_enable_modules_24(self, mock_enmod):
|
||||||
|
self.config.parser.modules.remove("authz_core_module")
|
||||||
|
self.config.parser.modules.remove("mod_authz_core.c")
|
||||||
|
|
||||||
|
enmod_calls = self.common_enable_modules_test(mock_enmod)
|
||||||
|
self.assertEqual(enmod_calls[0][0][0], "authz_core")
|
||||||
|
|
||||||
|
def common_enable_modules_test(self, mock_enmod):
|
||||||
|
"""Tests enabling mod_alias and other modules."""
|
||||||
|
self.config.parser.modules.remove("alias_module")
|
||||||
|
self.config.parser.modules.remove("mod_alias.c")
|
||||||
|
|
||||||
|
self.http.prepare_http01_modules()
|
||||||
|
|
||||||
|
self.assertTrue(mock_enmod.called)
|
||||||
|
calls = mock_enmod.call_args_list
|
||||||
|
other_calls = []
|
||||||
|
for call in calls:
|
||||||
|
if "alias" != call[0][0]:
|
||||||
|
other_calls.append(call)
|
||||||
|
|
||||||
|
# If these lists are equal, we never enabled mod_alias
|
||||||
|
self.assertNotEqual(calls, other_calls)
|
||||||
|
return other_calls
|
||||||
|
|
||||||
|
def common_perform_test(self, achalls):
|
||||||
|
"""Tests perform with the given achalls."""
|
||||||
|
for achall in achalls:
|
||||||
|
self.http.add_chall(achall)
|
||||||
|
|
||||||
|
expected_response = [
|
||||||
|
achall.response(self.account_key) for achall in achalls]
|
||||||
|
self.assertEqual(self.http.perform(), expected_response)
|
||||||
|
|
||||||
|
self.assertTrue(os.path.isdir(self.http.challenge_dir))
|
||||||
|
self._has_min_permissions(self.http.challenge_dir, 0o755)
|
||||||
|
self._test_challenge_conf()
|
||||||
|
|
||||||
|
for achall in achalls:
|
||||||
|
self._test_challenge_file(achall)
|
||||||
|
|
||||||
|
challenge_dir = self.http.challenge_dir
|
||||||
|
self.http.cleanup()
|
||||||
|
self.assertFalse(os.path.exists(challenge_dir))
|
||||||
|
|
||||||
|
def _test_challenge_conf(self):
|
||||||
|
self.assertEqual(
|
||||||
|
len(self.config.parser.find_dir(
|
||||||
|
"Include", self.http.challenge_conf)), 1)
|
||||||
|
|
||||||
|
with open(self.http.challenge_conf) as f:
|
||||||
|
conf_contents = f.read()
|
||||||
|
|
||||||
|
alias_fmt = "Alias /.well-known/acme-challenge {0}"
|
||||||
|
alias = alias_fmt.format(self.http.challenge_dir)
|
||||||
|
self.assertTrue(alias in conf_contents)
|
||||||
|
if self.config.version < (2, 4):
|
||||||
|
self.assertTrue("Allow from all" in conf_contents)
|
||||||
|
else:
|
||||||
|
self.assertTrue("Require all granted" in conf_contents)
|
||||||
|
|
||||||
|
def _test_challenge_file(self, achall):
|
||||||
|
name = os.path.join(self.http.challenge_dir, achall.chall.encode("token"))
|
||||||
|
validation = achall.validation(self.account_key)
|
||||||
|
|
||||||
|
self._has_min_permissions(name, 0o644)
|
||||||
|
with open(name, 'rb') as f:
|
||||||
|
self.assertEqual(f.read(), validation.encode())
|
||||||
|
|
||||||
|
def _has_min_permissions(self, path, min_mode):
|
||||||
|
"""Tests the given file has at least the permissions in mode."""
|
||||||
|
st_mode = os.stat(path).st_mode
|
||||||
|
self.assertEqual(st_mode, st_mode | min_mode)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main() # pragma: no cover
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
"""Test for certbot_apache.tls_sni_01."""
|
"""Test for certbot_apache.tls_sni_01."""
|
||||||
import unittest
|
|
||||||
import shutil
|
import shutil
|
||||||
|
import unittest
|
||||||
|
|
||||||
import mock
|
import mock
|
||||||
|
|
||||||
|
|||||||
@@ -103,6 +103,7 @@ def get_apache_configurator( # pylint: disable=too-many-arguments, too-many-loc
|
|||||||
apache_challenge_location=config_path,
|
apache_challenge_location=config_path,
|
||||||
backup_dir=backups,
|
backup_dir=backups,
|
||||||
config_dir=config_dir,
|
config_dir=config_dir,
|
||||||
|
http01_port=80,
|
||||||
temp_checkpoint_dir=os.path.join(work_dir, "temp_checkpoints"),
|
temp_checkpoint_dir=os.path.join(work_dir, "temp_checkpoints"),
|
||||||
in_progress_dir=os.path.join(backups, "IN_PROGRESS"),
|
in_progress_dir=os.path.join(backups, "IN_PROGRESS"),
|
||||||
work_dir=work_dir)
|
work_dir=work_dir)
|
||||||
|
|||||||
Reference in New Issue
Block a user