mirror of
https://github.com/certbot/certbot.git
synced 2026-01-26 07:41:33 +03:00
Streamline and reorganize Certbot's CLI output.
This change is a substantial command-line UX overhaul,
based on previous user research. The main goal was to streamline
and clarify output. To see more verbose output, use the -v or -vv flags.
---
* nginx,apache: CLI logging changes
- Add "Successfully deployed ..." message using display_util
- Remove IReporter usage and replace with display_util
- Standardize "... could not find a VirtualHost ..." error
This changes also bumps the version of certbot required by certbot-nginx
and certbot-apache to take use of the new display_util function.
* fix certbot_compatibility_test
since the http plugins now require IDisplay, we need to inject it
* fix dependency version on certbot
* use better asserts
* try fix oldest deps
because certbot 1.10.0 depends on acme>=1.8.0, we need to use
acme==1.8.0 in the -oldest tests
* cli: redesign output of new certificate reporting
Changes the output of run, certonly and certonly --csr. No longer uses
IReporter.
* cli: redesign output of failed authz reporting
* fix problem sorting to be stable between py2 & 3
* add some catch-all error text
* cli: dont use IReporter for EFF donation prompt
* add per-authenticator hints
* pass achalls to auth_hint, write some tests
* exclude static auth hints from coverage
* dont call auth_hint unless derived from .Plugin
* dns fallback hint: dont assume --dns-blah works
--dns-blah won't work for third-party plugins, they need to be specified
using --authenticator dns-blah.
* add code comments about the auth_hint interface
* renew: don't restart the installer for dry-runs
Prevents Certbot from superfluously invoking the installer restart
during dry-run renewals. (This does not affect authenticator restarts).
Additionally removes some CLI output that was reporting the fullchain
path of the renewed certificate.
* update CHANGELOG.md
* cli: redesign output when cert installation failed
- Display a message when certificate installation begins.
- Don't use IReporter, just log errors immediately if restart/rollback
fails.
- Prompt the user with a command to retry the installation process once
they have fixed any underlying problems.
* vary by preconfigured_renewal
and move expiry date to be above the renewal advice
* update code comment
Co-authored-by: ohemorange <ebportnoy@gmail.com>
* update code comment
Co-authored-by: ohemorange <ebportnoy@gmail.com>
* fix lint
* derve cert name from cert_path, if possible
* fix type annotation
* text change in nginx hint
Co-authored-by: ohemorange <ebportnoy@gmail.com>
* print message when restarting server after renewal
* log: print "advice" when exiting with an error
When running in non-quiet mode.
* try fix -oldest lock_test.py
* fix docstring
* s/Restarting/Reloading/ when notifying the user
* fix test name
Co-authored-by: ohemorange <ebportnoy@gmail.com>
* type annotations
* s/using the {} plugin/installer: {}/
* copy: avoid "plugin" where possible
* link to user guide#automated-renewals
when not running with --preconfigured-renewal
* cli: reduce default logging verbosity
* fix lock_test: -vv is needed to see logger.debug
* Change comment in log.py to match the change to default verbosity
* Audit and adjust logging levels in apache module
* Audit and adjust logging levels in nginx module
* Audit, adjust logging levels, and improve logging calls in certbot module
* Fix tests to mock correct methods and classes
* typo in non-preconfigured-renewal message
Co-authored-by: ohemorange <ebportnoy@gmail.com>
* fix test
* revert acme version bump
* catch up to python3 changes
* Revert "revert acme version bump"
This reverts commit fa83d6a51c.
* Change ocsp check error to warning since it's non-fatal
* Update storage_test in parallel with last change
* get rid of leading newline on "Deploying [...]"
* shrink renewal and installation success messages
* print logfile rather than logdir in exit handler
* Decrease logging level to info for idempotent operation where enhancement is already set
* Display cert not yet due for renewal message when renewing and no other action will be taken, and change cert to certificate
* also write to logger so it goes in the log file
* Don't double write to log file; fix main test
* cli: remove trailing newline on new cert reporting
* ignore type error
* revert accidental changes to dependencies
* Pass tests in any timezone by using utcfromtimestamp
* Add changelog entry
* fix nits
* Improve wording of try again message
* minor wording change to changelog
* hooks: send hook stdout to CLI stdout
includes both --manual and --{pre,post,renew} hooks
* update docstrings and remove TODO
* add a pending deprecation on execute_command
* add test coverage for both
* update deprecation text
Co-authored-by: ohemorange <ebportnoy@gmail.com>
Co-authored-by: Alex Zorin <alex@zorin.id.au>
Co-authored-by: alexzorin <alex@zor.io>
191 lines
9.0 KiB
Python
191 lines
9.0 KiB
Python
# pylint: disable=too-many-lines
|
|
"""Test for certbot_apache._internal.configurator AutoHSTS functionality"""
|
|
import re
|
|
import unittest
|
|
|
|
try:
|
|
import mock
|
|
except ImportError: # pragma: no cover
|
|
from unittest import mock # type: ignore
|
|
|
|
from certbot import errors
|
|
from certbot_apache._internal import constants
|
|
import util
|
|
|
|
|
|
class AutoHSTSTest(util.ApacheTest):
|
|
"""Tests for AutoHSTS feature"""
|
|
# pylint: disable=protected-access
|
|
|
|
def setUp(self): # pylint: disable=arguments-differ
|
|
super().setUp()
|
|
|
|
self.config = util.get_apache_configurator(
|
|
self.config_path, self.vhost_path, self.config_dir, self.work_dir)
|
|
self.config.parser.modules["headers_module"] = None
|
|
self.config.parser.modules["mod_headers.c"] = None
|
|
self.config.parser.modules["ssl_module"] = None
|
|
self.config.parser.modules["mod_ssl.c"] = None
|
|
|
|
self.vh_truth = util.get_vh_truth(
|
|
self.temp_dir, "debian_apache_2_4/multiple_vhosts")
|
|
|
|
def get_autohsts_value(self, vh_path):
|
|
""" Get value from Strict-Transport-Security header """
|
|
header_path = self.config.parser.find_dir("Header", None, vh_path)
|
|
if header_path:
|
|
pat = '(?:[ "]|^)(strict-transport-security)(?:[ "]|$)'
|
|
for head in header_path:
|
|
if re.search(pat, self.config.parser.aug.get(head).lower()):
|
|
return self.config.parser.aug.get(
|
|
head.replace("arg[3]", "arg[4]"))
|
|
return None # pragma: no cover
|
|
|
|
@mock.patch("certbot_apache._internal.configurator.ApacheConfigurator.restart")
|
|
@mock.patch("certbot_apache._internal.configurator.ApacheConfigurator.enable_mod")
|
|
def test_autohsts_enable_headers_mod(self, mock_enable, _restart):
|
|
self.config.parser.modules.pop("headers_module", None)
|
|
self.config.parser.modules.pop("mod_header.c", None)
|
|
self.config.enable_autohsts(mock.MagicMock(), ["ocspvhost.com"])
|
|
self.assertTrue(mock_enable.called)
|
|
|
|
@mock.patch("certbot_apache._internal.configurator.ApacheConfigurator.restart")
|
|
def test_autohsts_deploy_already_exists(self, _restart):
|
|
self.config.enable_autohsts(mock.MagicMock(), ["ocspvhost.com"])
|
|
self.assertRaises(errors.PluginEnhancementAlreadyPresent,
|
|
self.config.enable_autohsts,
|
|
mock.MagicMock(), ["ocspvhost.com"])
|
|
|
|
@mock.patch("certbot_apache._internal.constants.AUTOHSTS_FREQ", 0)
|
|
@mock.patch("certbot_apache._internal.configurator.ApacheConfigurator.restart")
|
|
@mock.patch("certbot_apache._internal.configurator.ApacheConfigurator.prepare")
|
|
def test_autohsts_increase(self, mock_prepare, _mock_restart):
|
|
self.config._prepared = False
|
|
maxage = "\"max-age={0}\""
|
|
initial_val = maxage.format(constants.AUTOHSTS_STEPS[0])
|
|
inc_val = maxage.format(constants.AUTOHSTS_STEPS[1])
|
|
|
|
self.config.enable_autohsts(mock.MagicMock(), ["ocspvhost.com"])
|
|
# Verify initial value
|
|
self.assertEqual(self.get_autohsts_value(self.vh_truth[7].path),
|
|
initial_val)
|
|
# Increase
|
|
self.config.update_autohsts(mock.MagicMock())
|
|
# Verify increased value
|
|
self.assertEqual(self.get_autohsts_value(self.vh_truth[7].path),
|
|
inc_val)
|
|
self.assertTrue(mock_prepare.called)
|
|
|
|
@mock.patch("certbot_apache._internal.configurator.ApacheConfigurator.restart")
|
|
@mock.patch("certbot_apache._internal.configurator.ApacheConfigurator._autohsts_increase")
|
|
def test_autohsts_increase_noop(self, mock_increase, _restart):
|
|
maxage = "\"max-age={0}\""
|
|
initial_val = maxage.format(constants.AUTOHSTS_STEPS[0])
|
|
self.config.enable_autohsts(mock.MagicMock(), ["ocspvhost.com"])
|
|
# Verify initial value
|
|
self.assertEqual(self.get_autohsts_value(self.vh_truth[7].path),
|
|
initial_val)
|
|
|
|
self.config.update_autohsts(mock.MagicMock())
|
|
# Freq not patched, so value shouldn't increase
|
|
self.assertFalse(mock_increase.called)
|
|
|
|
|
|
@mock.patch("certbot_apache._internal.configurator.ApacheConfigurator.restart")
|
|
@mock.patch("certbot_apache._internal.constants.AUTOHSTS_FREQ", 0)
|
|
def test_autohsts_increase_no_header(self, _restart):
|
|
self.config.enable_autohsts(mock.MagicMock(), ["ocspvhost.com"])
|
|
# Remove the header
|
|
dir_locs = self.config.parser.find_dir("Header", None,
|
|
self.vh_truth[7].path)
|
|
dir_loc = "/".join(dir_locs[0].split("/")[:-1])
|
|
self.config.parser.aug.remove(dir_loc)
|
|
self.assertRaises(errors.PluginError,
|
|
self.config.update_autohsts,
|
|
mock.MagicMock())
|
|
|
|
@mock.patch("certbot_apache._internal.constants.AUTOHSTS_FREQ", 0)
|
|
@mock.patch("certbot_apache._internal.configurator.ApacheConfigurator.restart")
|
|
def test_autohsts_increase_and_make_permanent(self, _mock_restart):
|
|
maxage = "\"max-age={0}\""
|
|
max_val = maxage.format(constants.AUTOHSTS_PERMANENT)
|
|
mock_lineage = mock.MagicMock()
|
|
mock_lineage.key_path = "/etc/apache2/ssl/key-certbot_15.pem"
|
|
self.config.enable_autohsts(mock.MagicMock(), ["ocspvhost.com"])
|
|
for i in range(len(constants.AUTOHSTS_STEPS)-1):
|
|
# Ensure that value is not made permanent prematurely
|
|
self.config.deploy_autohsts(mock_lineage)
|
|
self.assertNotEqual(self.get_autohsts_value(self.vh_truth[7].path),
|
|
max_val)
|
|
self.config.update_autohsts(mock.MagicMock())
|
|
# Value should match pre-permanent increment step
|
|
cur_val = maxage.format(constants.AUTOHSTS_STEPS[i+1])
|
|
self.assertEqual(self.get_autohsts_value(self.vh_truth[7].path),
|
|
cur_val)
|
|
# Ensure that the value is raised to max
|
|
self.assertEqual(self.get_autohsts_value(self.vh_truth[7].path),
|
|
maxage.format(constants.AUTOHSTS_STEPS[-1]))
|
|
# Make permanent
|
|
self.config.deploy_autohsts(mock_lineage)
|
|
self.assertEqual(self.get_autohsts_value(self.vh_truth[7].path),
|
|
max_val)
|
|
|
|
def test_autohsts_update_noop(self):
|
|
with mock.patch("time.time") as mock_time:
|
|
# Time mock is used to make sure that the execution does not
|
|
# continue when no autohsts entries exist in pluginstorage
|
|
self.config.update_autohsts(mock.MagicMock())
|
|
self.assertFalse(mock_time.called)
|
|
|
|
def test_autohsts_make_permanent_noop(self):
|
|
self.config.storage.put = mock.MagicMock()
|
|
self.config.deploy_autohsts(mock.MagicMock())
|
|
# Make sure that the execution does not continue when no entries in store
|
|
self.assertFalse(self.config.storage.put.called)
|
|
|
|
@mock.patch("certbot_apache._internal.display_ops.select_vhost")
|
|
def test_autohsts_no_ssl_vhost(self, mock_select):
|
|
mock_select.return_value = self.vh_truth[0]
|
|
with mock.patch("certbot_apache._internal.configurator.logger.error") as mock_log:
|
|
self.assertRaises(errors.PluginError,
|
|
self.config.enable_autohsts,
|
|
mock.MagicMock(), "invalid.example.com")
|
|
self.assertTrue(
|
|
"Certbot was not able to find SSL" in mock_log.call_args[0][0])
|
|
|
|
@mock.patch("certbot_apache._internal.configurator.ApacheConfigurator.restart")
|
|
@mock.patch("certbot_apache._internal.configurator.ApacheConfigurator.add_vhost_id")
|
|
def test_autohsts_dont_enhance_twice(self, mock_id, _restart):
|
|
mock_id.return_value = "1234567"
|
|
self.config.enable_autohsts(mock.MagicMock(),
|
|
["ocspvhost.com", "ocspvhost.com"])
|
|
self.assertEqual(mock_id.call_count, 1)
|
|
|
|
def test_autohsts_remove_orphaned(self):
|
|
# pylint: disable=protected-access
|
|
self.config._autohsts_fetch_state()
|
|
self.config._autohsts["orphan_id"] = {"laststep": 0, "timestamp": 0}
|
|
|
|
self.config._autohsts_save_state()
|
|
self.config.update_autohsts(mock.MagicMock())
|
|
self.assertFalse("orphan_id" in self.config._autohsts)
|
|
# Make sure it's removed from the pluginstorage file as well
|
|
self.config._autohsts = None
|
|
self.config._autohsts_fetch_state()
|
|
self.assertFalse(self.config._autohsts)
|
|
|
|
def test_autohsts_make_permanent_vhost_not_found(self):
|
|
# pylint: disable=protected-access
|
|
self.config._autohsts_fetch_state()
|
|
self.config._autohsts["orphan_id"] = {"laststep": 999, "timestamp": 0}
|
|
self.config._autohsts_save_state()
|
|
with mock.patch("certbot_apache._internal.configurator.logger.error") as mock_log:
|
|
self.config.deploy_autohsts(mock.MagicMock())
|
|
self.assertTrue(mock_log.called)
|
|
self.assertTrue(
|
|
"VirtualHost with id orphan_id was not" in mock_log.call_args[0][0])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main() # pragma: no cover
|