Compare commits

...

35 Commits

Author SHA1 Message Date
Erica Portnoy
e8079a815a Use verbose=3 in sample dev cli.ini now that count is allowed in config files 2021-06-09 17:47:37 -07:00
alexzorin
d7b26c1bb2 cli: dont use argv[0] in user-facing messages (#8857) 2021-06-09 14:31:15 -07:00
Michel Le Bihan
78261dbae2 Fix typo of fulfill in dns_rfc2136 plugin (#8886) 2021-06-06 09:55:24 +10:00
Jonathan Griffin
2ed4e0a17e Fixed typo in common.py (#8881)
Fixed typo:

exterally -> externally
2021-06-03 13:42:56 -07:00
Brad Warren
c372dd8aee Remove local-oldest-requirements files (#8863)
This is part of https://github.com/certbot/certbot/issues/8787. I got a +1 from our packagers at major distros in https://github.com/certbot/certbot/issues/8761.

* remove local-oldest-requirements files

* fix tests

* fix some oldest tests

* list packages on one line in tox.ini

* add changelog entry
2021-06-01 14:46:06 -07:00
Brad Warren
01772280c0 Merge pull request #8879 from certbot/candidate-1.16.0
Release 1.16.0
2021-06-01 14:13:44 -07:00
Erica Portnoy
814d8d1aba Bump version to 1.17.0 2021-06-01 10:52:31 -07:00
Erica Portnoy
a190480517 Add contents to certbot/CHANGELOG.md for next version 2021-06-01 10:52:31 -07:00
Erica Portnoy
7e8f22e136 Release 1.16.0 2021-06-01 10:52:23 -07:00
Erica Portnoy
965a403699 Update changelog for 1.16.0 release 2021-06-01 10:49:17 -07:00
Brad Warren
968cc5801b delete eggs before running poetry (#8865) 2021-05-31 09:03:25 +02:00
Brad Warren
492b578662 Update coverage and pytest (#8875)
* unpin pytest and update pinnings

* ignore external mock warnings

* fix assertion

* fix test_revoke_mutual_exclusive_flags

* fix output count

* capture stdout and stderr separately

* undouble counts

* rename variable

* don't use capture_output

* fix leaky test

* update coverage
2021-05-31 09:01:01 +02:00
ohemorange
e946479b9f Use shortlink for renewal setup instructions (#8874) 2021-05-28 14:50:59 -07:00
Adrien Ferrand
f88105a952 Deprecate usage of IConfig as a singleton in Certbot (#8869)
* Deprecate usage of IConfig as a singleton in Certbot

* Fix local oldest requirements

* Add changelog

* Add tests for certbot.crypto_util.init_save_* functions

Co-authored-by: Brad Warren <bmw@users.noreply.github.com>
2021-05-28 12:17:56 -07:00
alexzorin
3380694fa8 windows: fix colors and bold text not rendering (#8872)
Fixes #8848.
2021-05-28 10:36:51 -07:00
ohemorange
18631b99ef Add instructions for setting up a cronjob in the docs (#8870)
* Add instructions for setting up a cronjob in the docs

* Be more specific about where the cron entry will be created

Co-authored-by: alexzorin <alex@zorin.id.au>

* Correct &amp;s to &s

Co-authored-by: alexzorin <alex@zorin.id.au>

* Correct other &amp; to &

Co-authored-by: alexzorin <alex@zorin.id.au>

* De-weasel the double-scheduled-task comment

Co-authored-by: alexzorin <alex@zorin.id.au>

* Have users create directory hooks instead of command line hooks

* Use sudo in command

Co-authored-by: alexzorin <alex@zorin.id.au>

* tell windows users to ignore these instructions instead of telling them they won't work

* Use the same commands that we have in the general instructions

Co-authored-by: alexzorin <alex@zorin.id.au>
2021-05-28 16:27:56 +10:00
Brad Warren
55d461392a Remove unused tools (#8862)
* remove unused tools

* remove deactivate.py
2021-05-28 06:47:44 +10:00
Arthur Lutz
a7a9a8480b [docs/using] Add mention of CentOS as supported by apache plugin (#8871) 2021-05-27 10:45:11 -07:00
Brad Warren
3640b8546e remove ancient comment (#8861) 2021-05-27 22:05:26 +10:00
Brad Warren
1f94c7db20 remove ancient .gitignore (#8864) 2021-05-27 21:52:46 +10:00
alexzorin
a02223a97f cli: later printing of renewal and install retry advice (#8860)
* later printing of renewal and install retry advice

Move printing of advice for automated renewal, and retrying installation
in case of failure, towards the end of `run` and `certonly`.

Also adds some renewal advice for the --csr case (no autorenewal).

* update renewal advice for preconfigured-renewal

* rewrite in terms of "NEXT STEPS" for run/certonly

* fix lint

* re-add "Could not install certificate"

* update --csr renewal advice

* rewrite non-preconfigured-renewal renewal advice
2021-05-26 15:16:12 -07:00
ohemorange
2e31b1ca41 Remove no names found in configuration files because it sounds like an error but actually it is fine (#8866)
* Remove no names found in configuration files because it sounds like an error but actually it is fine

* fix test

* Pose question more grammatically and specifically, and remove extra space

* fix lint

Co-authored-by: Alex Zorin <alex@zorin.id.au>
2021-05-27 07:54:04 +10:00
alexzorin
7ce86f588b windows: always run with --preconfigured-renewal (#8867)
Adds a Pynsist extra_preamble in the Certbot entry_point for the
Windows installer, ensuring the flag is always set.
2021-05-26 15:45:40 +02:00
alexzorin
39b396763a apache/nginx: certbot>=1.10.0 -> 1.10.1 (#8859)
1.10.0 was a bad release and this breaks our oldest Boulder tests.


I bumped the version to 1.10.0 in #8852 to get access to a new public display_util API, but that was the release with the broken deprecation of `--manual-public-ip-logging-ok`. So let's bump it to 1.10.1.
2021-05-25 08:40:18 -07:00
ohemorange
6f27c32db1 Command-line UX overhaul (#8852)
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>
2021-05-25 10:47:39 +10:00
Brad Warren
099c6c8b24 remove references to certbot-constraints.txt (#8858) 2021-05-24 14:17:11 -07:00
Brad Warren
315ddb247f Upgrade pylint (#8855)
This is part of https://github.com/certbot/certbot/issues/8782. I took it on now because the currently pinned version of `pylint` doesn't work with newer versions of `poetry` which I wanted to upgrade as part of https://github.com/certbot/certbot/issues/8787.

To say a bit more about the specific changes in this PR:

* Newer versions of `pylint` complain if `Popen` isn't used as a context manager. Instead of making this change, I switched to using `subprocess.run` which is simpler and [recommended in the Python docs](https://docs.python.org/3/library/subprocess.html#using-the-subprocess-module). I also disabled this check in a few places where no longer using `Popen` would require significant refactoring.
* The deleted code in `certbot/certbot/_internal/renewal.py` is cruft since https://github.com/certbot/certbot/pull/8685.
* The unused argument to `enable_mod` in the Apache plugin is used in some over the override classes that subclass that class.

* unpin pylint and repin dependencies

* disable raise-missing-from

* disable wrong-input-order

* remove unused code

* misc lint fixes

* remove unused import

* various lint fixes
2021-05-24 10:02:55 -07:00
alexzorin
2df279bc5b cli: dont double-print choosing plugins error (#8850) 2021-05-17 16:39:04 -07:00
osirisinferi
9e6b406218 Move 5040495 CHANGELOG.md entry to correct version (#8851)
The merge of #8789 left the CHANGELOG.md entry at a previous certbot release. This PR puts the entry at the correct certbot version.
2021-05-17 11:04:05 -07:00
Adrien Ferrand
352ee258b7 [Windows] Cleanup Certbot pkg dir before installing to avoid dependencies conflicts (#8836)
Fixes #8824

This PR makes the installer first delete (if exist) the previous `pkg` directory in the Certbot installation in order to avoid dependencies conflicts when a new version of Certbot (with new versions of dependencies) is intaller other an existing one.

I took the simplest approach here, which is to delete specifically the directories known to create conflicts, instead of more complex approaches that involve to factor in some way the complete uninstaller logic. This is because the complexity added without a clear improvement does not worth it in my opinion. More specifically:
* factorizing in some way the uninstaller section in the NSIS template make the installer use any potential new logic of a new installation of Certbot instead of the one applying for the current installation, and may create unexpected errors during installation or at runtime
* calling the existing `uninstaller.exe` would be better, but I could not find a proper way to let NSIS wait for the actual end of the uninstall logic, and again may create unexpected errors during installation or at runtime

* Cleanup Certbot pkg dir before installing to avoid dependencies conflicts

* Add a changelog

Co-authored-by: Brad Warren <bmw@users.noreply.github.com>
2021-05-17 10:22:47 -07:00
osirisinferi
5040495741 Use UTF-8 for renewal configuration file encoding (#8789) 2021-05-16 15:17:41 +02:00
Thomas G
bc23e07ee5 Fix incompatibility with lexicon >= v3.6.0 (#8819) 2021-05-16 15:03:53 +02:00
Mads Jensen
466e437a20 Use new GitHub templates. Add funding link (#8845) 2021-05-14 11:43:58 -07:00
Brad Warren
ee3b3656ea Remove old apache tests (#8843)
Apache test farm tests started failing last night due to a change in pyenv. See https://dev.azure.com/certbot/certbot/_build/results?buildId=3948&view=logs&j=f67c2a39-2c4f-5190-915f-6f32a7a4306f&t=96f0f394-f513-5158-f5e7-a26e55aeadbf&l=26943.

I managed to fix that in d94f20f8b7, however, the OSes the tests were failing on were Debian 9 and Ubuntu 16.04. [Debian 9 reached its end-of-life in July 2020](https://wiki.debian.org/DebianReleases) and [Ubuntu 16.04 reached its end of standard support in April 2021](https://wiki.ubuntu.com/Releases). As shown at the same links, Debian 9 still has support from the LTS team and Ubuntu 16.04 has ESM support. Do we still want to support either of these OSes?

If so, we can use the commit I linked in the first sentence of the last paragraph, but I think supporting the OSes through their standard support is good enough. The Certbot team has enough on their plate and especially when the OSes are so old that we can't even use their packaged version of Python anymore which complicates our tests, I think we can just drop support and move on.

I don't have a strong opinion here though so if someone else does, let me know what you'd like to see or make the PR yourself based on the changes in my linked commit and I'll merge it.

You can see the tests passing with this change at https://dev.azure.com/certbot/certbot/_build/results?buildId=3955&view=results.

* Remove apache tests on old OSes

* remove unused pyenv code
2021-05-14 11:27:47 -07:00
miigotu
db40974788 Add 3rd party certbot-dns-godaddy to the docs (#8844)
* Add 3rd party certbot-dns-godaddy to the docs

* fix up rst syntax for godaddy link

Co-authored-by: alexzorin <alex@zor.io>
2021-05-13 09:22:31 +10:00
132 changed files with 1858 additions and 1352 deletions

View File

@@ -2,14 +2,12 @@ steps:
- bash: |
FINAL_STATUS=0
declare -a FAILED_BUILDS
python3 -m venv .venv
source .venv/bin/activate
python tools/pipstrap.py
tools/venv.py
source venv/bin/activate
for doc_path in */docs
do
echo ""
echo "##[group]Building $doc_path"
tools/pip_install_editable.py $doc_path/..[docs]
if ! sphinx-build -W --keep-going -b html $doc_path $doc_path/_build/html; then
FINAL_STATUS=1
FAILED_BUILDS[${#FAILED_BUILDS[@]}]="${doc_path%/docs}"

1
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1 @@
custom: https://supporters.eff.org/donate/support-work-on-certbot

View File

@@ -56,7 +56,18 @@ extension-pkg-whitelist=pywintypes,win32api,win32file,win32security
# See https://github.com/PyCQA/pylint/issues/1498.
# 3) Same as point 2 for no-value-for-parameter.
# See https://github.com/PyCQA/pylint/issues/2820.
disable=fixme,locally-disabled,locally-enabled,bad-continuation,no-self-use,invalid-name,cyclic-import,duplicate-code,design,import-outside-toplevel,useless-object-inheritance,unsubscriptable-object,no-value-for-parameter,no-else-return,no-else-raise,no-else-break,no-else-continue
# 4) raise-missing-from makes it an error to raise an exception from except
# block without using explicit exception chaining. While explicit exception
# chaining results in a slightly more informative traceback, I don't think
# it's beneficial enough for us to change all of our current instances and
# give Certbot developers errors about this when they're working on new code
# in the future. You can read more about exception chaining and this pylint
# check at
# https://blog.ram.rachum.com/post/621791438475296768/improving-python-exception-chaining-with.
# 5) wrong-import-order generates false positives and a pylint developer
# suggests that people using isort should disable this check at
# https://github.com/PyCQA/pylint/issues/3817#issuecomment-687892090.
disable=fixme,locally-disabled,locally-enabled,bad-continuation,no-self-use,invalid-name,cyclic-import,duplicate-code,design,import-outside-toplevel,useless-object-inheritance,unsubscriptable-object,no-value-for-parameter,no-else-return,no-else-raise,no-else-break,no-else-continue,raise-missing-from,wrong-import-order
[REPORTS]

View File

@@ -3,9 +3,8 @@ import sys
from setuptools import find_packages
from setuptools import setup
version = '1.16.0.dev0'
version = '1.17.0.dev0'
# Please update tox.ini when modifying dependency version requirements
install_requires = [
'cryptography>=2.1.4',
# formerly known as acme.jose:

View File

@@ -220,13 +220,14 @@ def _get_runtime_cfg(command):
"""
try:
proc = subprocess.Popen(
proc = subprocess.run(
command,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True,
check=False,
env=util.env_no_snap_for_external_calls())
stdout, stderr = proc.communicate()
stdout, stderr = proc.stdout, proc.stderr
except (OSError, ValueError):
logger.error(

View File

@@ -136,6 +136,6 @@ def assertEqualPathsList(first, second): # pragma: no cover
if any(isPass(path) for path in second):
return
for fpath in first:
assert any([fnmatch.fnmatch(fpath, spath) for spath in second])
assert any(fnmatch.fnmatch(fpath, spath) for spath in second)
for spath in second:
assert any([fnmatch.fnmatch(fpath, spath) for fpath in first])
assert any(fnmatch.fnmatch(fpath, spath) for fpath in first)

View File

@@ -25,6 +25,7 @@ from certbot import util
from certbot.achallenges import KeyAuthorizationAnnotatedChallenge # pylint: disable=unused-import
from certbot.compat import filesystem
from certbot.compat import os
from certbot.display import util as display_util
from certbot.plugins import common
from certbot.plugins.enhancements import AutoHSTSEnhancement
from certbot.plugins.util import path_surgery
@@ -515,6 +516,8 @@ class ApacheConfigurator(common.Installer):
vhosts = self.choose_vhosts(domain)
for vhost in vhosts:
self._deploy_cert(vhost, cert_path, key_path, chain_path, fullchain_path)
display_util.notify("Successfully deployed certificate for {} to {}"
.format(domain, vhost.filep))
def choose_vhosts(self, domain, create_if_no_ssl=True):
"""
@@ -553,6 +556,19 @@ class ApacheConfigurator(common.Installer):
return list(matched)
def _raise_no_suitable_vhost_error(self, target_name: str):
"""
Notifies the user that Certbot could not find a vhost to secure
and raises an error.
:param str target_name: The server name that could not be mapped
:raises errors.PluginError: Raised unconditionally
"""
raise errors.PluginError(
"Certbot could not find a VirtualHost for {0} in the Apache "
"configuration. Please create a VirtualHost with a ServerName "
"matching {0} and try again.".format(target_name)
)
def _in_wildcard_scope(self, name, domain):
"""
Helper method for _vhosts_for_wildcard() that makes sure that the domain
@@ -590,12 +606,7 @@ class ApacheConfigurator(common.Installer):
dialog_output = display_ops.select_vhost_multiple(list(dialog_input))
if not dialog_output:
logger.error(
"No vhost exists with servername or alias for domain %s. "
"No vhost was selected. Please specify ServerName or ServerAlias "
"in the Apache config.",
domain)
raise errors.PluginError("No vhost selected")
self._raise_no_suitable_vhost_error(domain)
# Make sure we create SSL vhosts for the ones that are HTTP only
# if requested.
@@ -719,12 +730,7 @@ class ApacheConfigurator(common.Installer):
# Select a vhost from a list
vhost = display_ops.select_vhost(target_name, self.vhosts)
if vhost is None:
logger.error(
"No vhost exists with servername or alias of %s. "
"No vhost was selected. Please specify ServerName or ServerAlias "
"in the Apache config.",
target_name)
raise errors.PluginError("No vhost selected")
self._raise_no_suitable_vhost_error(target_name)
if temp:
return vhost
if not vhost.ssl:
@@ -1532,12 +1538,11 @@ class ApacheConfigurator(common.Installer):
raise errors.PluginError("Unable to write/read in make_vhost_ssl")
if sift:
reporter = zope.component.getUtility(interfaces.IReporter)
reporter.add_message(
"Some rewrite rules copied from {0} were disabled in the "
"vhost for your HTTPS site located at {1} because they have "
"the potential to create redirection loops.".format(
vhost.filep, ssl_fp), reporter.MEDIUM_PRIORITY)
display_util.notify(
f"Some rewrite rules copied from {vhost.filep} were disabled in the "
f"vhost for your HTTPS site located at {ssl_fp} because they have "
"the potential to create redirection loops."
)
self.parser.aug.set("/augeas/files%s/mtime" % (self._escape(ssl_fp)), "0")
self.parser.aug.set("/augeas/files%s/mtime" % (self._escape(vhost.filep)), "0")
@@ -1866,13 +1871,13 @@ class ApacheConfigurator(common.Installer):
if options:
msg_enhancement += ": " + options
msg = msg_tmpl.format(domain, msg_enhancement)
logger.warning(msg)
logger.error(msg)
raise errors.PluginError(msg)
try:
for vhost in vhosts:
func(vhost, options)
except errors.PluginError:
logger.warning("Failed %s for %s", enhancement, domain)
logger.error("Failed %s for %s", enhancement, domain)
raise
def _autohsts_increase(self, vhost, id_str, nextstep):
@@ -2396,7 +2401,7 @@ class ApacheConfigurator(common.Installer):
vhost.enabled = True
return
def enable_mod(self, mod_name, temp=False):
def enable_mod(self, mod_name, temp=False): # pylint: disable=unused-argument
"""Enables module in Apache.
Both enables and reloads Apache so module is active.
@@ -2436,7 +2441,7 @@ class ApacheConfigurator(common.Installer):
try:
util.run_script(self.options.restart_cmd)
except errors.SubprocessError as err:
logger.info("Unable to restart apache using %s",
logger.warning("Unable to restart apache using %s",
self.options.restart_cmd)
alt_restart = self.options.restart_cmd_alt
if alt_restart:
@@ -2500,6 +2505,11 @@ class ApacheConfigurator(common.Installer):
version=".".join(str(i) for i in self.version))
)
def auth_hint(self, failed_achalls): # pragma: no cover
return ("The Certificate Authority failed to verify the temporary Apache configuration "
"changes made by Certbot. Ensure that the listed domains point to this Apache "
"server and that it is accessible from the internet.")
###########################################################################
# Challenges Section
###########################################################################
@@ -2593,7 +2603,7 @@ class ApacheConfigurator(common.Installer):
msg_tmpl = ("Certbot was not able to find SSL VirtualHost for a "
"domain {0} for enabling AutoHSTS enhancement.")
msg = msg_tmpl.format(d)
logger.warning(msg)
logger.error(msg)
raise errors.PluginError(msg)
for vh in vhosts:
try:
@@ -2679,7 +2689,7 @@ class ApacheConfigurator(common.Installer):
except errors.PluginError:
msg = ("Could not find VirtualHost with ID {0}, disabling "
"AutoHSTS for this VirtualHost").format(id_str)
logger.warning(msg)
logger.error(msg)
# Remove the orphaned AutoHSTS entry from pluginstorage
self._autohsts.pop(id_str)
continue
@@ -2719,7 +2729,7 @@ class ApacheConfigurator(common.Installer):
except errors.PluginError:
msg = ("VirtualHost with id {} was not found, unable to "
"make HSTS max-age permanent.").format(id_str)
logger.warning(msg)
logger.error(msg)
self._autohsts.pop(id_str)
continue
if self._autohsts_vhost_in_lineage(vhost, lineage):

View File

@@ -119,7 +119,7 @@ def _vhost_menu(domain, vhosts):
"guidance in non-interactive mode. Certbot may need "
"vhosts to be explicitly labelled with ServerName or "
"ServerAlias directives.".format(domain))
logger.warning(msg)
logger.error(msg)
raise errors.MissingCommandlineFlag(msg)
return code, tag

View File

@@ -58,7 +58,7 @@ class DebianConfigurator(configurator.ApacheConfigurator):
# Already in shape
vhost.enabled = True
return None
logger.warning(
logger.error(
"Could not symlink %s to %s, got error: %s", enabled_path,
vhost.filep, err.strerror)
errstring = ("Encountered error while trying to enable a " +

View File

@@ -1,3 +0,0 @@
# Remember to update setup.py to match the package versions below.
acme[dev]==0.29.0
certbot[dev]==1.6.0

View File

@@ -1,13 +1,14 @@
from setuptools import find_packages
from setuptools import setup
version = '1.16.0.dev0'
version = '1.17.0.dev0'
# Remember to update local-oldest-requirements.txt when changing the minimum
# acme/certbot version.
install_requires = [
'acme>=0.29.0',
'certbot>=1.6.0',
# We specify the minimum acme and certbot version as the current plugin
# version for simplicity. See
# https://github.com/certbot/certbot/issues/8761 for more info.
f'acme>={version}',
f'certbot>={version}',
'python-augeas',
'setuptools>=39.0.1',
'zope.component',

View File

@@ -146,7 +146,7 @@ class AutoHSTSTest(util.ApacheTest):
@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.warning") as mock_log:
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")
@@ -179,7 +179,7 @@ class AutoHSTSTest(util.ApacheTest):
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.warning") as mock_log:
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(

View File

@@ -1,5 +1,6 @@
"""Test for certbot_apache._internal.configurator for CentOS 6 overrides"""
import unittest
from unittest import mock
from certbot.compat import os
from certbot.errors import MisconfigurationError
@@ -65,7 +66,8 @@ class CentOS6Tests(util.ApacheTest):
raise Exception("Missed: %s" % vhost) # pragma: no cover
self.assertEqual(found, 2)
def test_loadmod_default(self):
@mock.patch("certbot_apache._internal.configurator.display_util.notify")
def test_loadmod_default(self, unused_mock_notify):
ssl_loadmods = self.config.parser.find_dir(
"LoadModule", "ssl_module", exclude=False)
self.assertEqual(len(ssl_loadmods), 1)
@@ -95,7 +97,8 @@ class CentOS6Tests(util.ApacheTest):
ifmod_args = self.config.parser.get_all_args(lm[:-17])
self.assertTrue("!mod_ssl.c" in ifmod_args)
def test_loadmod_multiple(self):
@mock.patch("certbot_apache._internal.configurator.display_util.notify")
def test_loadmod_multiple(self, unused_mock_notify):
sslmod_args = ["ssl_module", "modules/mod_ssl.so"]
# Adds another LoadModule to main httpd.conf in addtition to ssl.conf
self.config.parser.add_dir(self.config.parser.loc["default"], "LoadModule",
@@ -115,7 +118,8 @@ class CentOS6Tests(util.ApacheTest):
for mod in post_loadmods:
self.assertTrue(self.config.parser.not_modssl_ifmodule(mod)) #pylint: disable=no-member
def test_loadmod_rootconf_exists(self):
@mock.patch("certbot_apache._internal.configurator.display_util.notify")
def test_loadmod_rootconf_exists(self, unused_mock_notify):
sslmod_args = ["ssl_module", "modules/mod_ssl.so"]
rootconf_ifmod = self.config.parser.get_ifmod(
parser.get_aug_path(self.config.parser.loc["default"]),
@@ -142,7 +146,8 @@ class CentOS6Tests(util.ApacheTest):
self.config.parser.get_all_args(mods[0][:-7]),
sslmod_args)
def test_neg_loadmod_already_on_path(self):
@mock.patch("certbot_apache._internal.configurator.display_util.notify")
def test_neg_loadmod_already_on_path(self, unused_mock_notify):
loadmod_args = ["ssl_module", "modules/mod_ssl.so"]
ifmod = self.config.parser.get_ifmod(
self.vh_truth[1].path, "!mod_ssl.c", beginning=True)
@@ -185,7 +190,8 @@ class CentOS6Tests(util.ApacheTest):
# Make sure that none was changed
self.assertEqual(pre_matches, post_matches)
def test_loadmod_not_found(self):
@mock.patch("certbot_apache._internal.configurator.display_util.notify")
def test_loadmod_not_found(self, unused_mock_notify):
# Remove all existing LoadModule ssl_module... directives
orig_loadmods = self.config.parser.find_dir("LoadModule",
"ssl_module",

View File

@@ -337,7 +337,8 @@ class MultipleVhostsTest(util.ApacheTest):
vhosts = self.config._non_default_vhosts(self.config.vhosts)
self.assertEqual(len(vhosts), 10)
def test_deploy_cert_enable_new_vhost(self):
@mock.patch('certbot_apache._internal.configurator.display_util.notify')
def test_deploy_cert_enable_new_vhost(self, unused_mock_notify):
# Create
ssl_vhost = self.config.make_vhost_ssl(self.vh_truth[0])
self.config.parser.modules["ssl_module"] = None
@@ -375,7 +376,8 @@ class MultipleVhostsTest(util.ApacheTest):
self.fail("Include shouldn't be added, as patched find_dir 'finds' existing one") \
# pragma: no cover
def test_deploy_cert(self):
@mock.patch('certbot_apache._internal.configurator.display_util.notify')
def test_deploy_cert(self, unused_mock_notify):
self.config.parser.modules["ssl_module"] = None
self.config.parser.modules["mod_ssl.c"] = None
self.config.parser.modules["socache_shmcb_module"] = None
@@ -891,7 +893,7 @@ class MultipleVhostsTest(util.ApacheTest):
self.config.enhance, "certbot.demo", "unknown_enhancement")
def test_enhance_no_ssl_vhost(self):
with mock.patch("certbot_apache._internal.configurator.logger.warning") as mock_log:
with mock.patch("certbot_apache._internal.configurator.logger.error") as mock_log:
self.assertRaises(errors.PluginError, self.config.enhance,
"certbot.demo", "redirect")
# Check that correct logger.warning was printed
@@ -1290,7 +1292,8 @@ class MultipleVhostsTest(util.ApacheTest):
os.path.basename(inc_path) in self.config.parser.existing_paths[
os.path.dirname(inc_path)])
def test_deploy_cert_not_parsed_path(self):
@mock.patch('certbot_apache._internal.configurator.display_util.notify')
def test_deploy_cert_not_parsed_path(self, unused_mock_notify):
# Make sure that we add include to root config for vhosts when
# handle-sites is false
self.config.parser.modules["ssl_module"] = None
@@ -1386,7 +1389,8 @@ class MultipleVhostsTest(util.ApacheTest):
self.assertEqual(vhs[0], self.vh_truth[7])
def test_deploy_cert_wildcard(self):
@mock.patch('certbot_apache._internal.configurator.display_util.notify')
def test_deploy_cert_wildcard(self, unused_mock_notify):
# pylint: disable=protected-access
mock_choose_vhosts = mock.MagicMock()
mock_choose_vhosts.return_value = [self.vh_truth[7]]
@@ -1606,8 +1610,8 @@ class MultiVhostsTest(util.ApacheTest):
self.assertEqual(self.config._get_new_vh_path(without_index, both),
with_index_2[0])
@certbot_util.patch_get_utility()
def test_make_vhost_ssl_with_existing_rewrite_rule(self, mock_get_utility):
@mock.patch("certbot_apache._internal.configurator.display_util.notify")
def test_make_vhost_ssl_with_existing_rewrite_rule(self, mock_notify):
self.config.parser.modules["rewrite_module"] = None
ssl_vhost = self.config.make_vhost_ssl(self.vh_truth[4])
@@ -1623,11 +1627,11 @@ class MultiVhostsTest(util.ApacheTest):
"\"http://new.example.com/docs/$1\" [R,L]")
self.assertTrue(commented_rewrite_rule in conf_text)
self.assertTrue(uncommented_rewrite_rule in conf_text)
mock_get_utility().add_message.assert_called_once_with(mock.ANY,
mock.ANY)
self.assertEqual(mock_notify.call_count, 1)
self.assertIn("Some rewrite rules", mock_notify.call_args[0][0])
@certbot_util.patch_get_utility()
def test_make_vhost_ssl_with_existing_rewrite_conds(self, mock_get_utility):
@mock.patch("certbot_apache._internal.configurator.display_util.notify")
def test_make_vhost_ssl_with_existing_rewrite_conds(self, mock_notify):
self.config.parser.modules["rewrite_module"] = None
ssl_vhost = self.config.make_vhost_ssl(self.vh_truth[3])
@@ -1652,8 +1656,8 @@ class MultiVhostsTest(util.ApacheTest):
self.assertTrue(commented_cond1 in conf_line_set)
self.assertTrue(commented_cond2 in conf_line_set)
self.assertTrue(commented_rewrite_rule in conf_line_set)
mock_get_utility().add_message.assert_called_once_with(mock.ANY,
mock.ANY)
self.assertEqual(mock_notify.call_count, 1)
self.assertIn("Some rewrite rules", mock_notify.call_args[0][0])
class InstallSslOptionsConfTest(util.ApacheTest):

View File

@@ -49,10 +49,11 @@ class MultipleVhostsTestDebian(util.ApacheTest):
@mock.patch("certbot.util.run_script")
@mock.patch("certbot.util.exe_exists")
@mock.patch("certbot_apache._internal.apache_util.subprocess.Popen")
def test_enable_mod(self, mock_popen, mock_exe_exists, mock_run_script):
mock_popen().communicate.return_value = ("Define: DUMP_RUN_CFG", "")
mock_popen().returncode = 0
@mock.patch("certbot_apache._internal.apache_util.subprocess.run")
def test_enable_mod(self, mock_run, mock_exe_exists, mock_run_script):
mock_run.return_value.stdout = "Define: DUMP_RUN_CFG"
mock_run.return_value.stderr = ""
mock_run.return_value.returncode = 0
mock_exe_exists.return_value = True
self.config.enable_mod("ssl")

View File

@@ -305,17 +305,19 @@ class BasicParserTest(util.ParserTest):
self.assertRaises(
errors.PluginError, self.parser.update_runtime_variables)
@mock.patch("certbot_apache._internal.apache_util.subprocess.Popen")
def test_update_runtime_vars_bad_ctl(self, mock_popen):
mock_popen.side_effect = OSError
@mock.patch("certbot_apache._internal.apache_util.subprocess.run")
def test_update_runtime_vars_bad_ctl(self, mock_run):
mock_run.side_effect = OSError
self.assertRaises(
errors.MisconfigurationError,
self.parser.update_runtime_variables)
@mock.patch("certbot_apache._internal.apache_util.subprocess.Popen")
def test_update_runtime_vars_bad_exit(self, mock_popen):
mock_popen().communicate.return_value = ("", "")
mock_popen.returncode = -1
@mock.patch("certbot_apache._internal.apache_util.subprocess.run")
def test_update_runtime_vars_bad_exit(self, mock_run):
mock_proc = mock_run.return_value
mock_proc.stdout = ""
mock_proc.stderr = ""
mock_proc.returncode = -1
self.assertRaises(
errors.MisconfigurationError,
self.parser.update_runtime_variables)

View File

@@ -61,7 +61,7 @@ class IntegrationTestsContext:
Execute certbot with given args, not renewing certificates by default.
:param args: args to pass to certbot
:param force_renew: set to False to not renew by default
:return: output of certbot execution
:return: stdout and stderr from certbot execution
"""
command = ['--authenticator', 'standalone', '--installer', 'null']
command.extend(args)

View File

@@ -78,9 +78,9 @@ def test_registration_override(context):
def test_prepare_plugins(context):
"""Test that plugins are correctly instantiated and displayed."""
output = context.certbot(['plugins', '--init', '--prepare'])
stdout, _ = context.certbot(['plugins', '--init', '--prepare'])
assert 'webroot' in output
assert 'webroot' in stdout
def test_http_01(context):
@@ -407,9 +407,9 @@ def test_invalid_domain_with_dns_challenge(context):
'--manual-cleanup-hook', context.manual_dns_cleanup_hook
])
output = context.certbot(['certificates'])
stdout, _ = context.certbot(['certificates'])
assert context.get_domain('fail-dns1') not in output
assert context.get_domain('fail-dns1') not in stdout
def test_reuse_key(context):
@@ -614,11 +614,11 @@ def test_revoke_and_unregister(context):
context.certbot(['unregister'])
output = context.certbot(['certificates'])
stdout, _ = context.certbot(['certificates'])
assert cert1 not in output
assert cert2 not in output
assert cert3 in output
assert cert1 not in stdout
assert cert2 not in stdout
assert cert3 in stdout
def test_revoke_mutual_exclusive_flags(context):
@@ -630,7 +630,7 @@ def test_revoke_mutual_exclusive_flags(context):
'revoke', '--cert-name', cert,
'--cert-path', join(context.config_dir, 'live', cert, 'fullchain.pem')
])
assert 'Exactly one of --cert-path or --cert-name must be specified' in error.out
assert 'Exactly one of --cert-path or --cert-name must be specified' in error.value.stderr
def test_revoke_multiple_lineages(context):
@@ -685,12 +685,12 @@ def test_wildcard_certificates(context):
def test_ocsp_status_stale(context):
"""Test retrieval of OCSP statuses for staled config"""
sample_data_path = misc.load_sample_data_path(context.workspace)
output = context.certbot(['certificates', '--config-dir', sample_data_path])
stdout, _ = context.certbot(['certificates', '--config-dir', sample_data_path])
assert output.count('TEST_CERT') == 2, ('Did not find two test certs as expected ({0})'
.format(output.count('TEST_CERT')))
assert output.count('EXPIRED') == 2, ('Did not find two expired certs as expected ({0})'
.format(output.count('EXPIRED')))
assert stdout.count('TEST_CERT') == 2, ('Did not find two test certs as expected ({0})'
.format(stdout.count('TEST_CERT')))
assert stdout.count('EXPIRED') == 2, ('Did not find two expired certs as expected ({0})'
.format(stdout.count('EXPIRED')))
def test_ocsp_status_live(context):
@@ -699,20 +699,20 @@ def test_ocsp_status_live(context):
# OSCP 1: Check live certificate OCSP status (VALID)
context.certbot(['--domains', cert])
output = context.certbot(['certificates'])
stdout, _ = context.certbot(['certificates'])
assert output.count('VALID') == 1, 'Expected {0} to be VALID'.format(cert)
assert output.count('EXPIRED') == 0, 'Did not expect {0} to be EXPIRED'.format(cert)
assert stdout.count('VALID') == 1, 'Expected {0} to be VALID'.format(cert)
assert stdout.count('EXPIRED') == 0, 'Did not expect {0} to be EXPIRED'.format(cert)
# OSCP 2: Check live certificate OCSP status (REVOKED)
context.certbot(['revoke', '--cert-name', cert, '--no-delete-after-revoke'])
# Sometimes in oldest tests (using openssl binary and not cryptography), the OCSP status is
# not seen immediately by Certbot as invalid. Waiting few seconds solves this transient issue.
time.sleep(5)
output = context.certbot(['certificates'])
stdout, _ = context.certbot(['certificates'])
assert output.count('INVALID') == 1, 'Expected {0} to be INVALID'.format(cert)
assert output.count('REVOKED') == 1, 'Expected {0} to be REVOKED'.format(cert)
assert stdout.count('INVALID') == 1, 'Expected {0} to be INVALID'.format(cert)
assert stdout.count('REVOKED') == 1, 'Expected {0} to be REVOKED'.format(cert)
def test_ocsp_renew(context):

View File

@@ -51,6 +51,7 @@ class IntegrationTestsContext(certbot_context.IntegrationTestsContext):
with open(self.nginx_config_path, 'w') as file:
file.write(self.nginx_config)
# pylint: disable=consider-using-with
process = subprocess.Popen(['nginx', '-c', self.nginx_config_path, '-g', 'daemon off;'])
assert process.poll() is None

View File

@@ -240,6 +240,7 @@ class ACMEServer:
if not env:
env = os.environ
stdout = sys.stderr if force_stderr else self._stdout
# pylint: disable=consider-using-with
process = subprocess.Popen(
command, stdout=stdout, stderr=subprocess.STDOUT, cwd=cwd, env=env
)

View File

@@ -17,7 +17,7 @@ def certbot_test(certbot_args, directory_url, http_01_port, tls_alpn_01_port,
Invoke the certbot executable available in PATH in a test context for the given args.
The test context consists in running certbot in debug mode, with various flags suitable
for tests (eg. no ssl check, customizable ACME challenge ports and config directory ...).
This command captures stdout and returns it to the caller.
This command captures both stdout and stderr and returns it to the caller.
:param list certbot_args: the arguments to pass to the certbot executable
:param str directory_url: URL of the ACME directory server to use
:param int http_01_port: port for the HTTP-01 challenges
@@ -25,13 +25,19 @@ def certbot_test(certbot_args, directory_url, http_01_port, tls_alpn_01_port,
:param str config_dir: certbot configuration directory to use
:param str workspace: certbot current directory to use
:param bool force_renew: set False to not force renew existing certificates (default: True)
:return: stdout as string
:rtype: str
:return: stdout and stderr as strings
:rtype: `tuple` of `str`
"""
command, env = _prepare_args_env(certbot_args, directory_url, http_01_port, tls_alpn_01_port,
config_dir, workspace, force_renew)
return subprocess.check_output(command, universal_newlines=True, cwd=workspace, env=env)
proc = subprocess.run(command, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, check=False, universal_newlines=True,
cwd=workspace, env=env)
print('--> Certbot log output was:')
print(proc.stderr)
proc.check_returncode()
return proc.stdout, proc.stderr
def _prepare_environ(workspace):

View File

@@ -83,6 +83,7 @@ class DNSServer:
def _start_bind(self):
"""Launch the BIND9 server as a Docker container"""
addr_str = "{}:{}".format(BIND_BIND_ADDRESS[0], BIND_BIND_ADDRESS[1])
# pylint: disable=consider-using-with
self.process = subprocess.Popen(
[
"docker",

View File

@@ -48,7 +48,6 @@ class Proxy(configurators_common.Proxy):
setattr(self.le_config, "nginx_" + k, constants.os_constant(k))
conf = configuration.NamespaceConfig(self.le_config)
zope.component.provideUtility(conf)
self._configurator = configurator.NginxConfigurator(
config=conf, name="nginx")
self._configurator.prepare()

View File

@@ -10,6 +10,7 @@ import tempfile
import time
from typing import List
from typing import Tuple
import zope.component
import OpenSSL
from urllib3.util import connection
@@ -19,6 +20,7 @@ from acme import crypto_util
from acme import messages
from certbot import achallenges
from certbot import errors as le_errors
from certbot.display import util as display_util
from certbot.tests import acme_util
from certbot_compatibility_test import errors
from certbot_compatibility_test import util
@@ -327,10 +329,17 @@ def setup_logging(args):
root_logger.addHandler(handler)
def setup_display():
""""Prepares IDisplay for the Certbot plugins """
displayer = display_util.NoninteractiveDisplay(sys.stdout)
zope.component.provideUtility(displayer)
def main():
"""Main test script execution."""
args = get_args()
setup_logging(args)
setup_display()
if args.plugin not in PLUGINS:
raise errors.Error("Unknown plugin {0}".format(args.plugin))

View File

@@ -3,7 +3,7 @@ import sys
from setuptools import find_packages
from setuptools import setup
version = '1.16.0.dev0'
version = '1.17.0.dev0'
install_requires = [
'certbot',

View File

@@ -1,3 +0,0 @@
# Remember to update setup.py to match the package versions below.
acme[dev]==0.29.0
certbot[dev]==1.1.0

View File

@@ -4,10 +4,8 @@ import sys
from setuptools import find_packages
from setuptools import setup
version = '1.16.0.dev0'
version = '1.17.0.dev0'
# Remember to update local-oldest-requirements.txt when changing the minimum
# acme/certbot version.
install_requires = [
'cloudflare>=1.5.1',
'setuptools>=39.0.1',
@@ -16,8 +14,11 @@ install_requires = [
if not os.environ.get('SNAP_BUILD'):
install_requires.extend([
'acme>=0.29.0',
'certbot>=1.1.0',
# We specify the minimum acme and certbot version as the current plugin
# version for simplicity. See
# https://github.com/certbot/certbot/issues/8761 for more info.
f'acme>={version}',
f'certbot>={version}',
])
elif 'bdist_wheel' in sys.argv[1:]:
raise RuntimeError('Unset SNAP_BUILD when building wheels '

View File

@@ -41,7 +41,8 @@ class AuthenticatorTest(test_util.TempDirTestCase, dns_test_common.BaseAuthentic
# _get_cloudflare_client | pylint: disable=protected-access
self.auth._get_cloudflare_client = mock.MagicMock(return_value=self.mock_client)
def test_perform(self):
@test_util.patch_get_utility()
def test_perform(self, unused_mock_get_utility):
self.auth.perform([self.achall])
expected = [mock.call.add_txt_record(DOMAIN, '_acme-challenge.'+DOMAIN, mock.ANY, mock.ANY)]
@@ -55,7 +56,8 @@ class AuthenticatorTest(test_util.TempDirTestCase, dns_test_common.BaseAuthentic
expected = [mock.call.del_txt_record(DOMAIN, '_acme-challenge.'+DOMAIN, mock.ANY)]
self.assertEqual(expected, self.mock_client.mock_calls)
def test_api_token(self):
@test_util.patch_get_utility()
def test_api_token(self, unused_mock_get_utility):
dns_test_common.write({"cloudflare_api_token": API_TOKEN},
self.config.cloudflare_credentials)
self.auth.perform([self.achall])

View File

@@ -1,3 +0,0 @@
# Remember to update setup.py to match the package versions below.
acme[dev]==0.31.0
certbot[dev]==1.1.0

View File

@@ -4,20 +4,21 @@ import sys
from setuptools import find_packages
from setuptools import setup
version = '1.16.0.dev0'
version = '1.17.0.dev0'
# Remember to update local-oldest-requirements.txt when changing the minimum
# acme/certbot version.
install_requires = [
'dns-lexicon>=2.2.1', # Support for >1 TXT record per name
'dns-lexicon>=3.1.0', # Changed `rtype` parameter name
'setuptools>=39.0.1',
'zope.interface',
]
if not os.environ.get('SNAP_BUILD'):
install_requires.extend([
'acme>=0.31.0',
'certbot>=1.1.0',
# We specify the minimum acme and certbot version as the current plugin
# version for simplicity. See
# https://github.com/certbot/certbot/issues/8761 for more info.
f'acme>={version}',
f'certbot>={version}',
])
elif 'bdist_wheel' in sys.argv[1:]:
raise RuntimeError('Unset SNAP_BUILD when building wheels '

View File

@@ -1,3 +0,0 @@
# Remember to update setup.py to match the package versions below.
acme[dev]==0.29.0
certbot[dev]==1.1.0

View File

@@ -4,10 +4,8 @@ import sys
from setuptools import find_packages
from setuptools import setup
version = '1.16.0.dev0'
version = '1.17.0.dev0'
# Remember to update local-oldest-requirements.txt when changing the minimum
# acme/certbot version.
install_requires = [
'python-digitalocean>=1.11', # 1.15.0 or newer is recommended for TTL support
'setuptools>=39.0.1',
@@ -16,8 +14,11 @@ install_requires = [
if not os.environ.get('SNAP_BUILD'):
install_requires.extend([
'acme>=0.29.0',
'certbot>=1.1.0',
# We specify the minimum acme and certbot version as the current plugin
# version for simplicity. See
# https://github.com/certbot/certbot/issues/8761 for more info.
f'acme>={version}',
f'certbot>={version}',
])
elif 'bdist_wheel' in sys.argv[1:]:
raise RuntimeError('Unset SNAP_BUILD when building wheels '

View File

@@ -37,7 +37,8 @@ class AuthenticatorTest(test_util.TempDirTestCase, dns_test_common.BaseAuthentic
# _get_digitalocean_client | pylint: disable=protected-access
self.auth._get_digitalocean_client = mock.MagicMock(return_value=self.mock_client)
def test_perform(self):
@test_util.patch_get_utility()
def test_perform(self, unused_mock_get_utility):
self.auth.perform([self.achall])
expected = [mock.call.add_txt_record(DOMAIN, '_acme-challenge.'+DOMAIN, mock.ANY, 30)]

View File

@@ -1,3 +0,0 @@
# Remember to update setup.py to match the package versions below.
acme[dev]==0.31.0
certbot[dev]==1.1.0

View File

@@ -4,10 +4,8 @@ import sys
from setuptools import find_packages
from setuptools import setup
version = '1.16.0.dev0'
version = '1.17.0.dev0'
# Remember to update local-oldest-requirements.txt when changing the minimum
# acme/certbot version.
install_requires = [
'setuptools>=39.0.1',
'zope.interface',
@@ -15,8 +13,11 @@ install_requires = [
if not os.environ.get('SNAP_BUILD'):
install_requires.extend([
'acme>=0.31.0',
'certbot>=1.1.0',
# We specify the minimum acme and certbot version as the current plugin
# version for simplicity. See
# https://github.com/certbot/certbot/issues/8761 for more info.
f'acme>={version}',
f'certbot>={version}',
])
elif 'bdist_wheel' in sys.argv[1:]:
raise RuntimeError('Unset SNAP_BUILD when building wheels '
@@ -32,7 +33,7 @@ if os.environ.get('SNAP_BUILD'):
# which allows us to potentially upgrade our packages in these distros
# as necessary.
if os.environ.get('CERTBOT_OLDEST') == '1':
install_requires.append('dns-lexicon>=2.2.1')
install_requires.append('dns-lexicon>=3.1.0') # Changed parameter name
else:
install_requires.append('dns-lexicon>=3.2.1')

View File

@@ -1,3 +0,0 @@
# Remember to update setup.py to match the package versions below.
acme[dev]==0.31.0
certbot[dev]==1.1.0

View File

@@ -4,20 +4,21 @@ import sys
from setuptools import find_packages
from setuptools import setup
version = '1.16.0.dev0'
version = '1.17.0.dev0'
# Remember to update local-oldest-requirements.txt when changing the minimum
# acme/certbot version.
install_requires = [
'dns-lexicon>=2.2.1', # Support for >1 TXT record per name
'dns-lexicon>=3.1.0', # Changed `rtype` parameter name
'setuptools>=39.0.1',
'zope.interface',
]
if not os.environ.get('SNAP_BUILD'):
install_requires.extend([
'acme>=0.31.0',
'certbot>=1.1.0',
# We specify the minimum acme and certbot version as the current plugin
# version for simplicity. See
# https://github.com/certbot/certbot/issues/8761 for more info.
f'acme>={version}',
f'certbot>={version}',
])
elif 'bdist_wheel' in sys.argv[1:]:
raise RuntimeError('Unset SNAP_BUILD when building wheels '

View File

@@ -1,3 +0,0 @@
# Remember to update setup.py to match the package versions below.
acme[dev]==0.31.0
certbot[dev]==1.1.0

View File

@@ -4,19 +4,21 @@ import sys
from setuptools import find_packages
from setuptools import setup
version = '1.16.0.dev0'
version = '1.17.0.dev0'
# Please update tox.ini when modifying dependency version requirements
install_requires = [
'dns-lexicon>=2.1.22',
'dns-lexicon>=3.1.0', # Changed `rtype` parameter name
'setuptools>=39.0.1',
'zope.interface',
]
if not os.environ.get('SNAP_BUILD'):
install_requires.extend([
'acme>=0.31.0',
'certbot>=1.1.0',
# We specify the minimum acme and certbot version as the current plugin
# version for simplicity. See
# https://github.com/certbot/certbot/issues/8761 for more info.
f'acme>={version}',
f'certbot>={version}',
])
elif 'bdist_wheel' in sys.argv[1:]:
raise RuntimeError('Unset SNAP_BUILD when building wheels '

View File

@@ -1,3 +0,0 @@
# Remember to update setup.py to match the package versions below.
acme[dev]==0.29.0
certbot[dev]==1.1.0

View File

@@ -4,10 +4,8 @@ import sys
from setuptools import find_packages
from setuptools import setup
version = '1.16.0.dev0'
version = '1.17.0.dev0'
# Remember to update local-oldest-requirements.txt when changing the minimum
# acme/certbot version.
install_requires = [
'google-api-python-client>=1.5.5',
'oauth2client>=4.0',
@@ -19,8 +17,11 @@ install_requires = [
if not os.environ.get('SNAP_BUILD'):
install_requires.extend([
'acme>=0.29.0',
'certbot>=1.1.0',
# We specify the minimum acme and certbot version as the current plugin
# version for simplicity. See
# https://github.com/certbot/certbot/issues/8761 for more info.
f'acme>={version}',
f'certbot>={version}',
])
elif 'bdist_wheel' in sys.argv[1:]:
raise RuntimeError('Unset SNAP_BUILD when building wheels '

View File

@@ -43,7 +43,8 @@ class AuthenticatorTest(test_util.TempDirTestCase, dns_test_common.BaseAuthentic
# _get_google_client | pylint: disable=protected-access
self.auth._get_google_client = mock.MagicMock(return_value=self.mock_client)
def test_perform(self):
@test_util.patch_get_utility()
def test_perform(self, unused_mock_get_utility):
self.auth.perform([self.achall])
expected = [mock.call.add_txt_record(DOMAIN, '_acme-challenge.'+DOMAIN, mock.ANY, mock.ANY)]
@@ -58,7 +59,8 @@ class AuthenticatorTest(test_util.TempDirTestCase, dns_test_common.BaseAuthentic
self.assertEqual(expected, self.mock_client.mock_calls)
@mock.patch('httplib2.Http.request', side_effect=ServerNotFoundError)
def test_without_auth(self, unused_mock):
@test_util.patch_get_utility()
def test_without_auth(self, unused_mock_get_utility, unused_mock):
self.config.google_credentials = None
self.assertRaises(PluginError, self.auth.perform, [self.achall])

View File

@@ -1,4 +0,0 @@
# Remember to update setup.py to match the package versions below.
acme[dev]==0.31.0
certbot[dev]==1.1.0
dns-lexicon==2.2.3

View File

@@ -4,19 +4,21 @@ import sys
from setuptools import find_packages
from setuptools import setup
version = '1.16.0.dev0'
version = '1.17.0.dev0'
# Please update tox.ini when modifying dependency version requirements
install_requires = [
'dns-lexicon>=2.2.3',
'dns-lexicon>=3.1.0', # Changed `rtype` parameter name
'setuptools>=39.0.1',
'zope.interface',
]
if not os.environ.get('SNAP_BUILD'):
install_requires.extend([
'acme>=0.31.0',
'certbot>=1.1.0',
# We specify the minimum acme and certbot version as the current plugin
# version for simplicity. See
# https://github.com/certbot/certbot/issues/8761 for more info.
f'acme>={version}',
f'certbot>={version}',
])
elif 'bdist_wheel' in sys.argv[1:]:
raise RuntimeError('Unset SNAP_BUILD when building wheels '

View File

@@ -1,3 +0,0 @@
# Remember to update setup.py to match the package versions below.
acme[dev]==0.31.0
certbot[dev]==1.1.0

View File

@@ -4,20 +4,21 @@ import sys
from setuptools import find_packages
from setuptools import setup
version = '1.16.0.dev0'
version = '1.17.0.dev0'
# Remember to update local-oldest-requirements.txt when changing the minimum
# acme/certbot version.
install_requires = [
'dns-lexicon>=2.2.1', # Support for >1 TXT record per name
'dns-lexicon>=3.1.0', # Changed `rtype` parameter name
'setuptools>=39.0.1',
'zope.interface',
]
if not os.environ.get('SNAP_BUILD'):
install_requires.extend([
'acme>=0.31.0',
'certbot>=1.1.0',
# We specify the minimum acme and certbot version as the current plugin
# version for simplicity. See
# https://github.com/certbot/certbot/issues/8761 for more info.
f'acme>={version}',
f'certbot>={version}',
])
elif 'bdist_wheel' in sys.argv[1:]:
raise RuntimeError('Unset SNAP_BUILD when building wheels '

View File

@@ -1,3 +0,0 @@
# Remember to update setup.py to match the package versions below.
acme[dev]==0.31.0
certbot[dev]==1.1.0

View File

@@ -4,20 +4,21 @@ import sys
from setuptools import find_packages
from setuptools import setup
version = '1.16.0.dev0'
version = '1.17.0.dev0'
# Remember to update local-oldest-requirements.txt when changing the minimum
# acme/certbot version.
install_requires = [
'dns-lexicon>=2.2.1', # Support for >1 TXT record per name
'dns-lexicon>=3.1.0', # Changed `rtype` parameter name
'setuptools>=39.0.1',
'zope.interface',
]
if not os.environ.get('SNAP_BUILD'):
install_requires.extend([
'acme>=0.31.0',
'certbot>=1.1.0',
# We specify the minimum acme and certbot version as the current plugin
# version for simplicity. See
# https://github.com/certbot/certbot/issues/8761 for more info.
f'acme>={version}',
f'certbot>={version}',
])
elif 'bdist_wheel' in sys.argv[1:]:
raise RuntimeError('Unset SNAP_BUILD when building wheels '

View File

@@ -1,4 +0,0 @@
# Remember to update setup.py to match the package versions below.
acme[dev]==0.31.0
certbot[dev]==1.1.0
dns-lexicon==2.7.14

View File

@@ -4,20 +4,21 @@ import sys
from setuptools import find_packages
from setuptools import setup
version = '1.16.0.dev0'
version = '1.17.0.dev0'
# Remember to update local-oldest-requirements.txt when changing the minimum
# acme/certbot version.
install_requires = [
'dns-lexicon>=2.7.14', # Correct proxy use on OVH provider
'dns-lexicon>=3.1.0', # Changed `rtype` parameter name
'setuptools>=39.0.1',
'zope.interface',
]
if not os.environ.get('SNAP_BUILD'):
install_requires.extend([
'acme>=0.31.0',
'certbot>=1.1.0',
# We specify the minimum acme and certbot version as the current plugin
# version for simplicity. See
# https://github.com/certbot/certbot/issues/8761 for more info.
f'acme>={version}',
f'certbot>={version}',
])
elif 'bdist_wheel' in sys.argv[1:]:
raise RuntimeError('Unset SNAP_BUILD when building wheels '

View File

@@ -27,7 +27,7 @@ DEFAULT_NETWORK_TIMEOUT = 45
class Authenticator(dns_common.DNSAuthenticator):
"""DNS Authenticator using RFC 2136 Dynamic Updates
This Authenticator uses RFC 2136 Dynamic Updates to fulfull a dns-01 challenge.
This Authenticator uses RFC 2136 Dynamic Updates to fulfill a dns-01 challenge.
"""
ALGORITHMS = {

View File

@@ -1,3 +0,0 @@
# Remember to update setup.py to match the package versions below.
acme[dev]==0.29.0
certbot[dev]==1.1.0

View File

@@ -4,10 +4,8 @@ import sys
from setuptools import find_packages
from setuptools import setup
version = '1.16.0.dev0'
version = '1.17.0.dev0'
# Remember to update local-oldest-requirements.txt when changing the minimum
# acme/certbot version.
install_requires = [
'dnspython',
'setuptools>=39.0.1',
@@ -16,8 +14,11 @@ install_requires = [
if not os.environ.get('SNAP_BUILD'):
install_requires.extend([
'acme>=0.29.0',
'certbot>=1.1.0',
# We specify the minimum acme and certbot version as the current plugin
# version for simplicity. See
# https://github.com/certbot/certbot/issues/8761 for more info.
f'acme>={version}',
f'certbot>={version}',
])
elif 'bdist_wheel' in sys.argv[1:]:
raise RuntimeError('Unset SNAP_BUILD when building wheels '

View File

@@ -42,7 +42,8 @@ class AuthenticatorTest(test_util.TempDirTestCase, dns_test_common.BaseAuthentic
# _get_rfc2136_client | pylint: disable=protected-access
self.auth._get_rfc2136_client = mock.MagicMock(return_value=self.mock_client)
def test_perform(self):
@test_util.patch_get_utility()
def test_perform(self, unused_mock_get_utility):
self.auth.perform([self.achall])
expected = [mock.call.add_txt_record('_acme-challenge.'+DOMAIN, mock.ANY, mock.ANY)]
@@ -65,7 +66,8 @@ class AuthenticatorTest(test_util.TempDirTestCase, dns_test_common.BaseAuthentic
self.auth.perform,
[self.achall])
def test_valid_algorithm_passes(self):
@test_util.patch_get_utility()
def test_valid_algorithm_passes(self, unused_mock_get_utility):
config = VALID_CONFIG.copy()
config["rfc2136_algorithm"] = "HMAC-sha512"
dns_test_common.write(config, self.config.rfc2136_credentials)

View File

@@ -1,62 +0,0 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*,cover
.hypothesis/
# Translations
*.mo
*.pot
# Django stuff:
*.log
# Sphinx documentation
docs/_build/
# PyBuilder
target/
#Ipython Notebook
.ipynb_checkpoints

View File

@@ -1,3 +0,0 @@
# Remember to update setup.py to match the package versions below.
acme[dev]==0.29.0
certbot[dev]==1.1.0

View File

@@ -4,10 +4,8 @@ import sys
from setuptools import find_packages
from setuptools import setup
version = '1.16.0.dev0'
version = '1.17.0.dev0'
# Remember to update local-oldest-requirements.txt when changing the minimum
# acme/certbot version.
install_requires = [
'boto3',
'setuptools>=39.0.1',
@@ -16,8 +14,11 @@ install_requires = [
if not os.environ.get('SNAP_BUILD'):
install_requires.extend([
'acme>=0.29.0',
'certbot>=1.1.0',
# We specify the minimum acme and certbot version as the current plugin
# version for simplicity. See
# https://github.com/certbot/certbot/issues/8761 for more info.
f'acme>={version}',
f'certbot>={version}',
])
elif 'bdist_wheel' in sys.argv[1:]:
raise RuntimeError('Unset SNAP_BUILD when building wheels '

View File

@@ -1,3 +0,0 @@
# Remember to update setup.py to match the package versions below.
acme[dev]==0.31.0
certbot[dev]==1.1.0

View File

@@ -4,19 +4,21 @@ import sys
from setuptools import find_packages
from setuptools import setup
version = '1.16.0.dev0'
version = '1.17.0.dev0'
# Please update tox.ini when modifying dependency version requirements
install_requires = [
'dns-lexicon>=2.1.23',
'dns-lexicon>=3.1.0', # Changed `rtype` parameter name
'setuptools>=39.0.1',
'zope.interface',
]
if not os.environ.get('SNAP_BUILD'):
install_requires.extend([
'acme>=0.31.0',
'certbot>=1.1.0',
# We specify the minimum acme and certbot version as the current plugin
# version for simplicity. See
# https://github.com/certbot/certbot/issues/8761 for more info.
f'acme>={version}',
f'certbot>={version}',
])
elif 'bdist_wheel' in sys.argv[1:]:
raise RuntimeError('Unset SNAP_BUILD when building wheels '

View File

@@ -24,6 +24,7 @@ from certbot import crypto_util
from certbot import errors
from certbot import interfaces
from certbot import util
from certbot.display import util as display_util
from certbot.compat import os
from certbot.plugins import common
from certbot_nginx._internal import constants
@@ -234,6 +235,8 @@ class NginxConfigurator(common.Installer):
vhosts = self.choose_vhosts(domain, create_if_no_match=True)
for vhost in vhosts:
self._deploy_cert(vhost, cert_path, key_path, chain_path, fullchain_path)
display_util.notify("Successfully deployed certificate for {} to {}"
.format(domain, vhost.filep))
def _deploy_cert(self, vhost, cert_path, key_path, chain_path, fullchain_path): # pylint: disable=unused-argument
"""
@@ -675,8 +678,9 @@ class NginxConfigurator(common.Installer):
"""Generate invalid certs that let us create ssl directives for Nginx"""
# TODO: generate only once
tmp_dir = os.path.join(self.config.work_dir, "snakeoil")
le_key = crypto_util.init_save_key(
key_size=1024, key_dir=tmp_dir, keyname="key.pem")
le_key = crypto_util.generate_key(
key_size=1024, key_dir=tmp_dir, keyname="key.pem",
strict_permissions=self.config.strict_permissions)
key = OpenSSL.crypto.load_privatekey(
OpenSSL.crypto.FILETYPE_PEM, le_key.pem)
cert = acme_crypto_util.gen_ss_cert(key, domains=[socket.gethostname()])
@@ -766,7 +770,7 @@ class NginxConfigurator(common.Installer):
raise errors.PluginError(
"Unsupported enhancement: {0}".format(enhancement))
except errors.PluginError:
logger.warning("Failed %s for %s", enhancement, domain)
logger.error("Failed %s for %s", enhancement, domain)
raise
def _has_certbot_redirect(self, vhost, domain):
@@ -985,13 +989,14 @@ class NginxConfigurator(common.Installer):
Unable to run Nginx version command
"""
try:
proc = subprocess.Popen(
proc = subprocess.run(
[self.conf('ctl'), "-c", self.nginx_conf, "-V"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True,
check=False,
env=util.env_no_snap_for_external_calls())
text = proc.communicate()[1] # nginx prints output to stderr
text = proc.stderr # nginx prints output to stderr
except (OSError, ValueError) as error:
logger.debug(str(error), exc_info=True)
raise errors.PluginError(
@@ -1075,6 +1080,13 @@ class NginxConfigurator(common.Installer):
version=".".join(str(i) for i in self.version))
)
def auth_hint(self, failed_achalls): # pragma: no cover
return (
"The Certificate Authority failed to verify the temporary nginx configuration changes "
"made by Certbot. Ensure the listed domains point to this nginx server and that it is "
"accessible from the internet."
)
###################################################
# Wrapper functions for Reverter class (IInstaller)
###################################################
@@ -1224,10 +1236,9 @@ def nginx_restart(nginx_ctl, nginx_conf, sleep_duration):
try:
reload_output: Text = u""
with tempfile.TemporaryFile() as out:
proc = subprocess.Popen([nginx_ctl, "-c", nginx_conf, "-s", "reload"],
env=util.env_no_snap_for_external_calls(),
stdout=out, stderr=out)
proc.communicate()
proc = subprocess.run([nginx_ctl, "-c", nginx_conf, "-s", "reload"],
env=util.env_no_snap_for_external_calls(),
stdout=out, stderr=out, check=False)
out.seek(0)
reload_output = out.read().decode("utf-8")
@@ -1237,9 +1248,8 @@ def nginx_restart(nginx_ctl, nginx_conf, sleep_duration):
# Write to temporary files instead of piping because of communication issues on Arch
# https://github.com/certbot/certbot/issues/4324
with tempfile.TemporaryFile() as out:
nginx_proc = subprocess.Popen([nginx_ctl, "-c", nginx_conf],
stdout=out, stderr=out, env=util.env_no_snap_for_external_calls())
nginx_proc.communicate()
nginx_proc = subprocess.run([nginx_ctl, "-c", nginx_conf],
stdout=out, stderr=out, env=util.env_no_snap_for_external_calls(), check=False)
if nginx_proc.returncode != 0:
out.seek(0)
# Enter recovery routine...

View File

@@ -128,12 +128,12 @@ class NginxHttp01(common.ChallengePerformer):
ipv6_addr = ipv6_addr + " ipv6only=on"
addresses = [obj.Addr.fromstring(default_addr),
obj.Addr.fromstring(ipv6_addr)]
logger.info(("Using default addresses %s and %s for authentication."),
logger.debug(("Using default addresses %s and %s for authentication."),
default_addr,
ipv6_addr)
else:
addresses = [obj.Addr.fromstring(default_addr)]
logger.info("Using default address %s for authentication.",
logger.debug("Using default address %s for authentication.",
default_addr)
return addresses

View File

@@ -217,7 +217,7 @@ class NginxParser:
"character. Only UTF-8 encoding is "
"supported.", item)
except pyparsing.ParseException as err:
logger.debug("Could not parse file: %s due to %s", item, err)
logger.warning("Could not parse file: %s due to %s", item, err)
return trees
def _find_config_root(self):
@@ -430,7 +430,7 @@ def _parse_ssl_options(ssl_options):
logger.warning("Could not read file: %s due to invalid character. "
"Only UTF-8 encoding is supported.", ssl_options)
except pyparsing.ParseBaseException as err:
logger.debug("Could not parse file: %s due to %s", ssl_options, err)
logger.warning("Could not parse file: %s due to %s", ssl_options, err)
return []
def _do_for_subarray(entry, condition, func, path=None):

View File

@@ -1,3 +0,0 @@
# Remember to update setup.py to match the package versions below.
acme[dev]==1.6.0
certbot[dev]==1.6.0

View File

@@ -1,13 +1,14 @@
from setuptools import find_packages
from setuptools import setup
version = '1.16.0.dev0'
version = '1.17.0.dev0'
# Remember to update local-oldest-requirements.txt when changing the minimum
# acme/certbot version.
install_requires = [
'acme>=1.4.0',
'certbot>=1.6.0',
# We specify the minimum acme and certbot version as the current plugin
# version for simplicity. See
# https://github.com/certbot/certbot/issues/8761 for more info.
f'acme>={version}',
f'certbot>={version}',
'PyOpenSSL>=17.3.0',
'pyparsing>=2.2.0',
'setuptools>=39.0.1',

View File

@@ -31,6 +31,10 @@ class NginxConfiguratorTest(util.NginxTest):
self.config = self.get_nginx_configurator(
self.config_path, self.config_dir, self.work_dir, self.logs_dir)
patch = mock.patch('certbot_nginx._internal.configurator.display_util.notify')
self.mock_notify = patch.start()
self.addCleanup(patch.stop)
@mock.patch("certbot_nginx._internal.configurator.util.exe_exists")
def test_prepare_no_install(self, mock_exe_exists):
mock_exe_exists.return_value = False
@@ -42,15 +46,16 @@ class NginxConfiguratorTest(util.NginxTest):
self.assertEqual(13, len(self.config.parser.parsed))
@mock.patch("certbot_nginx._internal.configurator.util.exe_exists")
@mock.patch("certbot_nginx._internal.configurator.subprocess.Popen")
def test_prepare_initializes_version(self, mock_popen, mock_exe_exists):
mock_popen().communicate.return_value = (
"", "\n".join(["nginx version: nginx/1.6.2",
@mock.patch("certbot_nginx._internal.configurator.subprocess.run")
def test_prepare_initializes_version(self, mock_run, mock_exe_exists):
mock_run.return_value.stdout = ""
mock_run.return_value.stderr = "\n".join(
["nginx version: nginx/1.6.2",
"built by clang 6.0 (clang-600.0.56)"
" (based on LLVM 3.5svn)",
"TLS SNI support enabled",
"configure arguments: --prefix=/usr/local/Cellar/"
"nginx/1.6.2 --with-http_ssl_module"]))
"nginx/1.6.2 --with-http_ssl_module"])
mock_exe_exists.return_value = True
@@ -347,141 +352,149 @@ class NginxConfiguratorTest(util.NginxTest):
self.assertEqual(mock_revert.call_count, 1)
self.assertEqual(mock_restart.call_count, 2)
@mock.patch("certbot_nginx._internal.configurator.subprocess.Popen")
def test_get_version(self, mock_popen):
mock_popen().communicate.return_value = (
"", "\n".join(["nginx version: nginx/1.4.2",
@mock.patch("certbot_nginx._internal.configurator.subprocess.run")
def test_get_version(self, mock_run):
mock_run.return_value.stdout = ""
mock_run.return_value.stderr = "\n".join(
["nginx version: nginx/1.4.2",
"built by clang 6.0 (clang-600.0.56)"
" (based on LLVM 3.5svn)",
"TLS SNI support enabled",
"configure arguments: --prefix=/usr/local/Cellar/"
"nginx/1.6.2 --with-http_ssl_module"]))
"nginx/1.6.2 --with-http_ssl_module"])
self.assertEqual(self.config.get_version(), (1, 4, 2))
mock_popen().communicate.return_value = (
"", "\n".join(["nginx version: nginx/0.9",
mock_run.return_value.stdout = ""
mock_run.return_value.stderr = "\n".join(
["nginx version: nginx/0.9",
"built by clang 6.0 (clang-600.0.56)"
" (based on LLVM 3.5svn)",
"TLS SNI support enabled",
"configure arguments: --with-http_ssl_module"]))
"configure arguments: --with-http_ssl_module"])
self.assertEqual(self.config.get_version(), (0, 9))
mock_popen().communicate.return_value = (
"", "\n".join(["blah 0.0.1",
mock_run.return_value.stdout = ""
mock_run.return_value.stderr = "\n".join(
["blah 0.0.1",
"built by clang 6.0 (clang-600.0.56)"
" (based on LLVM 3.5svn)",
"TLS SNI support enabled",
"configure arguments: --with-http_ssl_module"]))
"configure arguments: --with-http_ssl_module"])
self.assertRaises(errors.PluginError, self.config.get_version)
mock_popen().communicate.return_value = (
"", "\n".join(["nginx version: nginx/1.4.2",
"TLS SNI support enabled"]))
mock_run.return_value.stdout = ""
mock_run.return_value.stderr = "\n".join(
["nginx version: nginx/1.4.2",
"TLS SNI support enabled"])
self.assertRaises(errors.PluginError, self.config.get_version)
mock_popen().communicate.return_value = (
"", "\n".join(["nginx version: nginx/1.4.2",
mock_run.return_value.stdout = ""
mock_run.return_value.stderr = "\n".join(
["nginx version: nginx/1.4.2",
"built by clang 6.0 (clang-600.0.56)"
" (based on LLVM 3.5svn)",
"configure arguments: --with-http_ssl_module"]))
"configure arguments: --with-http_ssl_module"])
self.assertRaises(errors.PluginError, self.config.get_version)
mock_popen().communicate.return_value = (
"", "\n".join(["nginx version: nginx/0.8.1",
mock_run.return_value.stdout = ""
mock_run.return_value.stderr = "\n".join(
["nginx version: nginx/0.8.1",
"built by clang 6.0 (clang-600.0.56)"
" (based on LLVM 3.5svn)",
"TLS SNI support enabled",
"configure arguments: --with-http_ssl_module"]))
"configure arguments: --with-http_ssl_module"])
self.assertRaises(errors.NotSupportedError, self.config.get_version)
mock_popen.side_effect = OSError("Can't find program")
mock_run.side_effect = OSError("Can't find program")
self.assertRaises(errors.PluginError, self.config.get_version)
@mock.patch("certbot_nginx._internal.configurator.subprocess.Popen")
def test_get_openssl_version(self, mock_popen):
@mock.patch("certbot_nginx._internal.configurator.subprocess.run")
def test_get_openssl_version(self, mock_run):
# pylint: disable=protected-access
mock_popen().communicate.return_value = (
"", """
mock_run.return_value.stdout = ""
mock_run.return_value.stderr = """
nginx version: nginx/1.15.5
built by gcc 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.9)
built with OpenSSL 1.0.2g 1 Mar 2016
TLS SNI support enabled
configure arguments:
""")
"""
self.assertEqual(self.config._get_openssl_version(), "1.0.2g")
mock_popen().communicate.return_value = (
"", """
mock_run.return_value.stdout = ""
mock_run.return_value.stderr = """
nginx version: nginx/1.15.5
built by gcc 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.9)
built with OpenSSL 1.0.2-beta1 1 Mar 2016
TLS SNI support enabled
configure arguments:
""")
"""
self.assertEqual(self.config._get_openssl_version(), "1.0.2-beta1")
mock_popen().communicate.return_value = (
"", """
mock_run.return_value.stdout = ""
mock_run.return_value.stderr = """
nginx version: nginx/1.15.5
built by gcc 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.9)
built with OpenSSL 1.0.2 1 Mar 2016
TLS SNI support enabled
configure arguments:
""")
"""
self.assertEqual(self.config._get_openssl_version(), "1.0.2")
mock_popen().communicate.return_value = (
"", """
mock_run.return_value.stdout = ""
mock_run.return_value.stderr = """
nginx version: nginx/1.15.5
built by gcc 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.9)
built with OpenSSL 1.0.2g 1 Mar 2016 (running with OpenSSL 1.0.2a 1 Mar 2016)
TLS SNI support enabled
configure arguments:
""")
"""
self.assertEqual(self.config._get_openssl_version(), "1.0.2a")
mock_popen().communicate.return_value = (
"", """
mock_run.return_value.stdout = ""
mock_run.return_value.stderr = """
nginx version: nginx/1.15.5
built by gcc 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.9)
built with LibreSSL 2.2.2
TLS SNI support enabled
configure arguments:
""")
"""
self.assertEqual(self.config._get_openssl_version(), "")
mock_popen().communicate.return_value = (
"", """
mock_run.return_value.stdout = ""
mock_run.return_value.stderr = """
nginx version: nginx/1.15.5
built by gcc 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.9)
TLS SNI support enabled
configure arguments:
""")
"""
self.assertEqual(self.config._get_openssl_version(), "")
@mock.patch("certbot_nginx._internal.configurator.subprocess.Popen")
@mock.patch("certbot_nginx._internal.configurator.subprocess.run")
@mock.patch("certbot_nginx._internal.configurator.time")
def test_nginx_restart(self, mock_time, mock_popen):
mocked = mock_popen()
mocked.communicate.return_value = ('', '')
def test_nginx_restart(self, mock_time, mock_run):
mocked = mock_run.return_value
mocked.stdout = ''
mocked.stderr = ''
mocked.returncode = 0
self.config.restart()
self.assertEqual(mocked.communicate.call_count, 1)
self.assertEqual(mock_run.call_count, 1)
mock_time.sleep.assert_called_once_with(0.1234)
@mock.patch("certbot_nginx._internal.configurator.subprocess.Popen")
@mock.patch("certbot_nginx._internal.configurator.subprocess.run")
@mock.patch("certbot_nginx._internal.configurator.logger.debug")
def test_nginx_restart_fail(self, mock_log_debug, mock_popen):
mocked = mock_popen()
mocked.communicate.return_value = ('', '')
def test_nginx_restart_fail(self, mock_log_debug, mock_run):
mocked = mock_run.return_value
mocked.stdout = ''
mocked.stderr = ''
mocked.returncode = 1
self.assertRaises(errors.MisconfigurationError, self.config.restart)
self.assertEqual(mocked.communicate.call_count, 2)
self.assertEqual(mock_run.call_count, 2)
mock_log_debug.assert_called_once_with("nginx reload failed:\n%s", "")
@mock.patch("certbot_nginx._internal.configurator.subprocess.Popen")
def test_no_nginx_start(self, mock_popen):
mock_popen.side_effect = OSError("Can't find program")
@mock.patch("certbot_nginx._internal.configurator.subprocess.run")
def test_no_nginx_start(self, mock_run):
mock_run.side_effect = OSError("Can't find program")
self.assertRaises(errors.MisconfigurationError, self.config.restart)
@mock.patch("certbot.util.run_script")

View File

@@ -9,7 +9,6 @@ try:
except ImportError: # pragma: no cover
from unittest import mock # type: ignore
import pkg_resources
import zope.component
from certbot import util
from certbot.compat import os
@@ -79,9 +78,6 @@ class NginxTest(test_util.ConfigTestCase):
openssl_version=openssl_version)
config.prepare()
# Provide general config utility.
zope.component.provideUtility(self.configuration)
return config

1
certbot/.gitignore vendored
View File

@@ -1 +0,0 @@
*.crt

View File

@@ -2,7 +2,7 @@
Certbot adheres to [Semantic Versioning](https://semver.org/).
## 1.16.0 - master
## 1.17.0 - master
### Added
@@ -10,7 +10,12 @@ Certbot adheres to [Semantic Versioning](https://semver.org/).
### Changed
*
* We changed how dependencies are specified between Certbot packages. For this
and future releases, higher level Certbot components will require that lower
level components are the same version or newer. More specifically, version X
of the Certbot package will now always require acme>=X and version Y of a
plugin package will always require acme>=Y and certbot=>Y. Specifying
dependencies in this way simplifies testing and development.
### Fixed
@@ -18,6 +23,36 @@ Certbot adheres to [Semantic Versioning](https://semver.org/).
More details about these changes can be found on our GitHub repo.
## 1.16.0 - 2021-06-01
### Added
*
### Changed
* DNS plugins based on lexicon now require dns-lexicon >= v3.1.0
* Use UTF-8 encoding for renewal configuration files
* Windows installer now cleans up old Certbot dependency packages
before installing the new ones to avoid version conflicts.
* This release contains a substantial command-line UX overhaul,
based on previous user research. The main goal was to streamline
and clarify output. If you would like to see more verbose output, use
the -v or -vv flags. UX improvements are an iterative process and
the Certbot team welcomes constructive feedback.
* Functions `certbot.crypto_util.init_save_key` and `certbot.crypto_util.init_save_csr`,
whose behaviors rely on the global Certbot `config` singleton, are deprecated and will
be removed in a future release. Please use `certbot.crypto_util.generate_key` and
`certbot.crypto_util.generate_csr` instead.
### Fixed
* Fix TypeError due to incompatibility with lexicon >= v3.6.0
* Installers (e.g. nginx, Apache) were being restarted unnecessarily after dry-run renewals.
* Colors and bold text should properly render in all supported versions of Windows.
More details about these changes can be found on our GitHub repo.
## 1.15.0 - 2021-05-04
### Added

View File

@@ -1,3 +1,3 @@
"""Certbot client."""
# version number like 1.2.3a0, must have at least 2 parts, like 1.2
__version__ = '1.16.0.dev0'
__version__ = '1.17.0.dev0'

View File

@@ -15,6 +15,8 @@ from certbot import achallenges
from certbot import errors
from certbot import interfaces
from certbot._internal import error_handler
from certbot.display import util as display_util
from certbot.plugins import common as plugin_common
logger = logging.getLogger(__name__)
@@ -42,11 +44,12 @@ class AuthHandler:
self.account = account
self.pref_challs = pref_challs
def handle_authorizations(self, orderr, best_effort=False, max_retries=30):
def handle_authorizations(self, orderr, config, best_effort=False, max_retries=30):
"""
Retrieve all authorizations, perform all challenges required to validate
these authorizations, then poll and wait for the authorization to be checked.
:param acme.messages.OrderResource orderr: must have authorizations filled in
:param interfaces.IConfig config: current Certbot configuration
:param bool best_effort: if True, not all authorizations need to be validated (eg. renew)
:param int max_retries: maximum number of retries to poll authorizations
:returns: list of all validated authorizations
@@ -70,8 +73,6 @@ class AuthHandler:
resps = self.auth.perform(achalls)
# If debug is on, wait for user input before starting the verification process.
logger.info('Waiting for verification...')
config = zope.component.getUtility(interfaces.IConfig)
if config.debug_challenges:
notify = zope.component.getUtility(interfaces.IDisplay).notification
notify('Challenges loaded. Press continue to submit to CA. '
@@ -88,6 +89,7 @@ class AuthHandler:
self.acme.answer_challenge(achall.challb, resp)
# Wait for authorizations to be checked.
logger.info('Waiting for verification...')
self._poll_authorizations(authzrs, max_retries, best_effort)
# Keep validated authorizations only. If there is none, no certificate can be issued.
@@ -148,7 +150,7 @@ class AuthHandler:
authzrs_failed = [authzr for authzr, _ in authzrs_to_check.values()
if authzr.body.status == messages.STATUS_INVALID]
for authzr_failed in authzrs_failed:
logger.warning('Challenge failed for domain %s',
logger.info('Challenge failed for domain %s',
authzr_failed.body.identifier.value)
# Accumulating all failed authzrs to build a consolidated report
# on them at the end of the polling.
@@ -173,7 +175,7 @@ class AuthHandler:
# In case of failed authzrs, create a report to the user.
if authzrs_failed_to_report:
_report_failed_authzrs(authzrs_failed_to_report, self.account.key)
self._report_failed_authzrs(authzrs_failed_to_report)
if not best_effort:
# Without best effort, having failed authzrs is critical and fail the process.
raise errors.AuthorizationError('Some challenges have failed.')
@@ -264,6 +266,29 @@ class AuthHandler:
return achalls
def _report_failed_authzrs(self, failed_authzrs: List[messages.AuthorizationResource]) -> None:
"""Notifies the user about failed authorizations."""
problems: Dict[str, List[achallenges.AnnotatedChallenge]] = {}
failed_achalls = [challb_to_achall(challb, self.account.key, authzr.body.identifier.value)
for authzr in failed_authzrs for challb in authzr.body.challenges
if challb.error]
for achall in failed_achalls:
problems.setdefault(achall.error.typ, []).append(achall)
msg = [f"\nCertbot failed to authenticate some domains (authenticator: {self.auth.name})."
" The Certificate Authority reported these problems:"]
for _, achalls in sorted(problems.items(), key=lambda item: item[0]):
msg.append(_generate_failed_chall_msg(achalls))
# auth_hint will only be called on authenticators that subclass
# plugin_common.Plugin. Refer to comment on that function.
if failed_achalls and isinstance(self.auth, plugin_common.Plugin):
msg.append(f"\nHint: {self.auth.auth_hint(failed_achalls)}\n")
display_util.notify("".join(msg))
def challb_to_achall(challb, account_key, domain):
"""Converts a ChallengeBody object to an AnnotatedChallenge.
@@ -393,60 +418,13 @@ def _report_no_chall_path(challbs):
raise errors.AuthorizationError(msg)
_ERROR_HELP_COMMON = (
"To fix these errors, please make sure that your domain name was entered "
"correctly and the DNS A/AAAA record(s) for that domain contain(s) the "
"right IP address.")
_ERROR_HELP = {
"connection":
_ERROR_HELP_COMMON + " Additionally, please check that your computer "
"has a publicly routable IP address and that no firewalls are preventing "
"the server from communicating with the client. If you're using the "
"webroot plugin, you should also verify that you are serving files "
"from the webroot path you provided.",
"dnssec":
_ERROR_HELP_COMMON + " Additionally, if you have DNSSEC enabled for "
"your domain, please ensure that the signature is valid.",
"malformed":
"To fix these errors, please make sure that you did not provide any "
"invalid information to the client, and try running Certbot "
"again.",
"serverInternal":
"Unfortunately, an error on the ACME server prevented you from completing "
"authorization. Please try again later.",
"tls":
_ERROR_HELP_COMMON + " Additionally, please check that you have an "
"up-to-date TLS configuration that allows the server to communicate "
"with the Certbot client.",
"unauthorized": _ERROR_HELP_COMMON,
"unknownHost": _ERROR_HELP_COMMON,
}
def _report_failed_authzrs(failed_authzrs, account_key):
"""Notifies the user about failed authorizations."""
problems: Dict[str, List[achallenges.AnnotatedChallenge]] = {}
failed_achalls = [challb_to_achall(challb, account_key, authzr.body.identifier.value)
for authzr in failed_authzrs for challb in authzr.body.challenges
if challb.error]
for achall in failed_achalls:
problems.setdefault(achall.error.typ, []).append(achall)
reporter = zope.component.getUtility(interfaces.IReporter)
for achalls in problems.values():
reporter.add_message(_generate_failed_chall_msg(achalls), reporter.MEDIUM_PRIORITY)
def _generate_failed_chall_msg(failed_achalls):
# type: (List[achallenges.AnnotatedChallenge]) -> str
"""Creates a user friendly error message about failed challenges.
:param list failed_achalls: A list of failed
:class:`certbot.achallenges.AnnotatedChallenge` with the same error
type.
:returns: A formatted error message for the client.
:rtype: str
@@ -455,14 +433,10 @@ def _generate_failed_chall_msg(failed_achalls):
typ = error.typ
if messages.is_acme_error(error):
typ = error.code
msg = ["The following errors were reported by the server:"]
msg = []
for achall in failed_achalls:
msg.append("\n\nDomain: %s\nType: %s\nDetail: %s" % (
msg.append("\n Domain: %s\n Type: %s\n Detail: %s\n" % (
achall.domain, typ, achall.error.detail))
if typ in _ERROR_HELP:
msg.append("\n\n")
msg.append(_ERROR_HELP[typ])
return "".join(msg)

View File

@@ -9,7 +9,6 @@ from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric.rsa import generate_private_key # type: ignore
import josepy as jose
import OpenSSL
import zope.component
from acme import client as acme_client
from acme import crypto_util as acme_crypto_util
@@ -18,7 +17,6 @@ from acme import messages
import certbot
from certbot import crypto_util
from certbot import errors
from certbot import interfaces
from certbot import util
from certbot._internal import account
from certbot._internal import auth_handler
@@ -30,6 +28,7 @@ from certbot._internal import storage
from certbot._internal.plugins import selection as plugin_selection
from certbot.compat import os
from certbot.display import ops as display_ops
from certbot.display import util as display_util
logger = logging.getLogger(__name__)
@@ -154,7 +153,7 @@ def register(config, account_storage, tos_cb=None):
if not config.register_unsafely_without_email:
msg = ("No email was provided and "
"--register-unsafely-without-email was not present.")
logger.warning(msg)
logger.error(msg)
raise errors.Error(msg)
if not config.dry_run:
logger.debug("Registering without email!")
@@ -276,7 +275,7 @@ class Client:
if self.auth_handler is None:
msg = ("Unable to obtain certificate because authenticator is "
"not set.")
logger.warning(msg)
logger.error(msg)
raise errors.Error(msg)
if self.account.regr is None:
raise errors.Error("Please register with the ACME server first.")
@@ -335,7 +334,7 @@ class Client:
key = None
key_size = self.config.rsa_key_size
elliptic_curve = None
elliptic_curve = "secp256r1"
# key-type defaults to a list, but we are only handling 1 currently
if isinstance(self.config.key_type, list):
@@ -363,13 +362,15 @@ class Client:
data=acme_crypto_util.make_csr(
key.pem, domains, self.config.must_staple))
else:
key = key or crypto_util.init_save_key(
key = key or crypto_util.generate_key(
key_size=key_size,
key_dir=self.config.key_dir,
key_type=self.config.key_type,
elliptic_curve=elliptic_curve,
strict_permissions=self.config.strict_permissions,
)
csr = crypto_util.init_save_csr(key, domains, self.config.csr_dir)
csr = crypto_util.generate_csr(key, domains, self.config.csr_dir,
self.config.must_staple, self.config.strict_permissions)
orderr = self._get_order_and_authorizations(csr.data, self.config.allow_subset_of_names)
authzr = orderr.authorizations
@@ -421,7 +422,7 @@ class Client:
logger.warning("Certbot was unable to obtain fresh authorizations for every domain"
". The dry run will continue, but results may not be accurate.")
authzr = self.auth_handler.handle_authorizations(orderr, best_effort)
authzr = self.auth_handler.handle_authorizations(orderr, self.config, best_effort)
return orderr.update(authorizations=authzr)
def obtain_and_enroll_certificate(self, domains, certname):
@@ -506,8 +507,6 @@ class Client:
cert_file.write(cert_pem)
finally:
cert_file.close()
logger.info("Server issued certificate; certificate written to %s",
abs_cert_path)
chain_file, abs_chain_path =\
_open_pem_file('chain_path', chain_path)
@@ -519,8 +518,7 @@ class Client:
return abs_cert_path, abs_chain_path, abs_fullchain_path
def deploy_certificate(self, domains, privkey_path,
cert_path, chain_path, fullchain_path):
def deploy_certificate(self, domains, privkey_path, cert_path, chain_path, fullchain_path):
"""Install certificate
:param list domains: list of domains to install the certificate
@@ -530,13 +528,15 @@ class Client:
"""
if self.installer is None:
logger.warning("No installer specified, client is unable to deploy"
logger.error("No installer specified, client is unable to deploy"
"the certificate")
raise errors.Error("No installer available")
chain_path = None if chain_path is None else os.path.abspath(chain_path)
msg = ("Unable to install the certificate")
display_util.notify("Deploying certificate")
msg = "Could not install certificate"
with error_handler.ErrorHandler(self._recovery_routine_with_msg, msg):
for dom in domains:
self.installer.deploy_cert(
@@ -568,7 +568,7 @@ class Client:
"""
if self.installer is None:
logger.warning("No installer is specified, there isn't any "
logger.error("No installer is specified, there isn't any "
"configuration to enhance.")
raise errors.Error("No installer available")
@@ -589,7 +589,7 @@ class Client:
self.apply_enhancement(domains, enhancement_name, option)
enhanced = True
elif config_value:
logger.warning(
logger.error(
"Option %s is not supported by the selected installer. "
"Skipping enhancement.", config_name)
@@ -612,22 +612,20 @@ class Client:
"""
msg = ("We were unable to set up enhancement %s for your server, "
"however, we successfully installed your certificate."
% (enhancement))
msg = f"Could not set up {enhancement} enhancement"
with error_handler.ErrorHandler(self._recovery_routine_with_msg, msg):
for dom in domains:
try:
self.installer.enhance(dom, enhancement, options)
except errors.PluginEnhancementAlreadyPresent:
if enhancement == "ensure-http-header":
logger.warning("Enhancement %s was already set.",
logger.info("Enhancement %s was already set.",
options)
else:
logger.warning("Enhancement %s was already set.",
logger.info("Enhancement %s was already set.",
enhancement)
except errors.PluginError:
logger.warning("Unable to set enhancement %s for %s",
logger.error("Unable to set enhancement %s for %s",
enhancement, dom)
raise
@@ -640,8 +638,7 @@ class Client:
"""
self.installer.recovery_routine()
reporter = zope.component.getUtility(interfaces.IReporter)
reporter.add_message(success_msg, reporter.HIGH_PRIORITY)
display_util.notify(success_msg)
def _rollback_and_restart(self, success_msg):
"""Rollback the most recent checkpoint and restart the webserver
@@ -649,20 +646,19 @@ class Client:
:param str success_msg: message to show on successful rollback
"""
logger.critical("Rolling back to previous server configuration...")
reporter = zope.component.getUtility(interfaces.IReporter)
logger.info("Rolling back to previous server configuration...")
try:
self.installer.rollback_checkpoints()
self.installer.restart()
except:
reporter.add_message(
logger.error(
"An error occurred and we failed to restore your config and "
"restart your server. Please post to "
"https://community.letsencrypt.org/c/help "
"with details about your configuration and this error you received.",
reporter.HIGH_PRIORITY)
"with details about your configuration and this error you received."
)
raise
reporter.add_message(success_msg, reporter.HIGH_PRIORITY)
display_util.notify(success_msg)
def validate_key_csr(privkey, csr=None):
@@ -761,5 +757,3 @@ def _save_chain(chain_pem, chain_file):
chain_file.write(chain_pem)
finally:
chain_file.close()
logger.info("Cert chain written to %s", chain_file.name)

View File

@@ -22,7 +22,7 @@ CLI_DEFAULTS = dict(
],
# Main parser
verbose_count=-int(logging.INFO / 10),
verbose_count=-int(logging.WARNING / 10),
text_mode=False,
max_log_backups=1000,
preconfigured_renewal=False,
@@ -139,7 +139,7 @@ REVOCATION_REASONS = {
"""Defaults for CLI flags and `.IConfig` attributes."""
QUIET_LOGGING_LEVEL = logging.WARNING
QUIET_LOGGING_LEVEL = logging.ERROR
"""Logging level to use in quiet mode."""
RENEWER_DEFAULTS = dict(

View File

@@ -9,6 +9,7 @@ from certbot import util
from certbot.compat import filesystem
from certbot.compat import misc
from certbot.compat import os
from certbot.display import ops as display_ops
from certbot.plugins import util as plug_util
logger = logging.getLogger(__name__)
@@ -210,7 +211,7 @@ def _run_deploy_hook(command, domains, lineage_path, dry_run):
"""
if dry_run:
logger.warning("Dry run: skipping deploy hook command: %s",
logger.info("Dry run: skipping deploy hook command: %s",
command)
return
@@ -227,7 +228,9 @@ def _run_hook(cmd_name, shell_cmd):
:type shell_cmd: `list` of `str` or `str`
:returns: stderr if there was any"""
err, _ = misc.execute_command(cmd_name, shell_cmd, env=util.env_no_snap_for_external_calls())
returncode, err, out = misc.execute_command_status(
cmd_name, shell_cmd, env=util.env_no_snap_for_external_calls())
display_ops.report_executed_command(f"Hook '{cmd_name}'", returncode, out, err)
return err

View File

@@ -13,7 +13,7 @@ and properly flushed before program exit.
The `logging` module is useful for recording messages about about what
Certbot is doing under the hood, but do not necessarily need to be shown
to the user on the terminal. The default verbosity is INFO.
to the user on the terminal. The default verbosity is WARNING.
The preferred method to display important information to the user is to
use `certbot.display.util` and `certbot.display.ops`.
@@ -28,6 +28,7 @@ import shutil
import sys
import tempfile
import traceback
from types import TracebackType
from acme import messages
from certbot import errors
@@ -78,7 +79,9 @@ def pre_arg_parse_setup():
util.atexit_register(logging.shutdown)
sys.excepthook = functools.partial(
pre_arg_parse_except_hook, memory_handler,
debug='--debug' in sys.argv, log_path=temp_handler.path)
debug='--debug' in sys.argv,
quiet='--quiet' in sys.argv or '-q' in sys.argv,
log_path=temp_handler.path)
def post_arg_parse_setup(config):
@@ -95,7 +98,6 @@ def post_arg_parse_setup(config):
"""
file_handler, file_path = setup_log_file_handler(
config, 'letsencrypt.log', FILE_FMT)
logs_dir = os.path.dirname(file_path)
root_logger = logging.getLogger()
memory_handler = stderr_handler = None
@@ -122,10 +124,13 @@ def post_arg_parse_setup(config):
level = -config.verbose_count * 10
stderr_handler.setLevel(level)
logger.debug('Root logging level set at %d', level)
logger.info('Saving debug log to %s', file_path)
if not config.quiet:
print(f'Saving debug log to {file_path}', file=sys.stderr)
sys.excepthook = functools.partial(
post_arg_parse_except_hook, debug=config.debug, log_path=logs_dir)
post_arg_parse_except_hook,
debug=config.debug, quiet=config.quiet, log_path=file_path)
def setup_log_file_handler(config, logfile, fmt):
@@ -307,7 +312,8 @@ def pre_arg_parse_except_hook(memory_handler, *args, **kwargs):
memory_handler.flush(force=True)
def post_arg_parse_except_hook(exc_type, exc_value, trace, debug, log_path):
def post_arg_parse_except_hook(exc_type: type, exc_value: BaseException, trace: TracebackType,
debug: bool, quiet: bool, log_path: str):
"""Logs fatal exceptions and reports them to the user.
If debug is True, the full exception and traceback is shown to the
@@ -318,10 +324,13 @@ def post_arg_parse_except_hook(exc_type, exc_value, trace, debug, log_path):
:param BaseException exc_value: raised exception
:param traceback trace: traceback of where the exception was raised
:param bool debug: True if the traceback should be shown to the user
:param bool quiet: True if Certbot is running in quiet mode
:param str log_path: path to file or directory containing the log
"""
exc_info = (exc_type, exc_value, trace)
# Only print human advice if not running under --quiet
exit_func = lambda: sys.exit(1) if quiet else exit_with_advice(log_path)
# constants.QUIET_LOGGING_LEVEL or higher should be used to
# display message the user, otherwise, a lower level like
# logger.DEBUG should be used
@@ -337,7 +346,7 @@ def post_arg_parse_except_hook(exc_type, exc_value, trace, debug, log_path):
# our logger printing warnings and errors in red text.
if issubclass(exc_type, errors.Error):
logger.error(str(exc_value))
sys.exit(1)
exit_func()
logger.error('An unexpected error occurred:')
if messages.is_acme_error(exc_value):
# Remove the ACME error prefix from the exception
@@ -350,11 +359,11 @@ def post_arg_parse_except_hook(exc_type, exc_value, trace, debug, log_path):
# and remove the final newline before passing it to
# logger.error.
logger.error(''.join(output).rstrip())
exit_with_log_path(log_path)
exit_func()
def exit_with_log_path(log_path):
"""Print a message about the log location and exit.
def exit_with_advice(log_path: str):
"""Print a link to the community forums, the debug log path, and exit
The message is printed to stderr and the program will exit with a
nonzero status.
@@ -362,10 +371,11 @@ def exit_with_log_path(log_path):
:param str log_path: path to file or directory containing the log
"""
msg = 'Please see the '
msg = ("Ask for help or search for solutions at https://community.letsencrypt.org. "
"See the ")
if os.path.isdir(log_path):
msg += 'logfiles in {0} '.format(log_path)
msg += f'logfiles in {log_path} '
else:
msg += "logfile '{0}' ".format(log_path)
msg += 'for more details.'
msg += f"logfile {log_path} "
msg += 'or re-run Certbot with -v for more details.'
sys.exit(msg)

View File

@@ -67,26 +67,14 @@ def _suggest_donation_if_appropriate(config):
if config.staging:
# --dry-run implies --staging
return
reporter_util = zope.component.getUtility(interfaces.IReporter)
msg = ("If you like Certbot, please consider supporting our work by:\n\n"
"Donating to ISRG / Let's Encrypt: https://letsencrypt.org/donate\n"
"Donating to EFF: https://eff.org/donate-le\n\n")
reporter_util.add_message(msg, reporter_util.LOW_PRIORITY)
def _report_successful_dry_run(config):
"""Reports on successful dry run
:param config: Configuration object
:type config: interfaces.IConfig
:returns: `None`
:rtype: None
"""
reporter_util = zope.component.getUtility(interfaces.IReporter)
assert config.verb != "renew"
reporter_util.add_message("The dry run was successful.",
reporter_util.HIGH_PRIORITY, on_crash=False)
disp = zope.component.getUtility(interfaces.IDisplay)
util.atexit_register(
disp.notification,
"If you like Certbot, please consider supporting our work by:\n"
" * Donating to ISRG / Let's Encrypt: https://letsencrypt.org/donate\n"
" * Donating to EFF: https://eff.org/donate-le",
pause=False
)
def _get_and_save_cert(le_client, config, domains=None, certname=None, lineage=None):
@@ -214,7 +202,7 @@ def _handle_subset_cert_request(config: configuration.NamespaceConfig,
"--duplicate option.{br}{br}"
"For example:{br}{br}{1} --duplicate {2}".format(
existing,
sys.argv[0], " ".join(sys.argv[1:]),
cli.cli_command, " ".join(sys.argv[1:]),
br=os.linesep
))
raise errors.Error(USER_CANCELLED)
@@ -481,9 +469,74 @@ def _find_domains_or_certname(config, installer, question=None):
return domains, certname
def _report_next_steps(config: interfaces.IConfig, installer_err: Optional[errors.Error],
lineage: Optional[storage.RenewableCert],
new_or_renewed_cert: bool = True) -> None:
"""Displays post-run/certonly advice to the user about renewal and installation.
The output varies by runtime configuration and any errors encountered during installation.
:param config: Configuration object
:type config: interfaces.IConfig
:param installer_err: The installer/enhancement error encountered, if any.
:type error: Optional[errors.Error]
:param lineage: The resulting certificate lineage from the issuance, if any.
:type lineage: Optional[storage.RenewableCert]
:param bool new_or_renewed_cert: Whether the verb execution resulted in a certificate
being saved (created or renewed).
"""
steps: List[str] = []
# If the installation or enhancement raised an error, show advice on trying again
if installer_err:
steps.append(
"The certificate was saved, but could not be installed (installer: "
f"{config.installer}). After fixing the error shown below, try installing it again "
f"by running:\n {cli.cli_command} install --cert-name "
f"{_cert_name_from_config_or_lineage(config, lineage)}"
)
# If a certificate was obtained or renewed, show applicable renewal advice
if new_or_renewed_cert:
if config.csr:
steps.append(
"Certificates created using --csr will not be renewed automatically by Certbot. "
"You will need to renew the certificate before it expires, by running the same "
"Certbot command again.")
elif not config.preconfigured_renewal:
steps.append(
"The certificate will need to be renewed before it expires. Certbot can "
"automatically renew the certificate in the background, but you may need "
"to take steps to enable that functionality. "
"See https://certbot.org/renewal-setup for instructions.")
if not steps:
return
# TODO: refactor ANSI escapes during https://github.com/certbot/certbot/issues/8848
(bold_on, bold_off) = [c if sys.stdout.isatty() and not config.quiet else '' \
for c in (util.ANSI_SGR_BOLD, util.ANSI_SGR_RESET)]
print(bold_on, '\n', 'NEXT STEPS:', bold_off, sep='')
for step in steps:
display_util.notify(f"- {step}")
# If there was an installer error, segregate the error output with a trailing newline
if installer_err:
print()
def _report_new_cert(config, cert_path, fullchain_path, key_path=None):
# type: (interfaces.IConfig, Optional[str], Optional[str], Optional[str]) -> None
"""Reports the creation of a new certificate to the user.
:param config: Configuration object
:type config: interfaces.IConfig
:param cert_path: path to certificate
:type cert_path: str
@@ -498,29 +551,62 @@ def _report_new_cert(config, cert_path, fullchain_path, key_path=None):
"""
if config.dry_run:
_report_successful_dry_run(config)
display_util.notify("The dry run was successful.")
return
assert cert_path and fullchain_path, "No certificates saved to report."
display_util.notify(
("\nSuccessfully received certificate.\n"
"Certificate is saved at: {cert_path}\n{key_msg}"
"This certificate expires on {expiry}.\n"
"These files will be updated when the certificate renews.{renewal_msg}{nl}").format(
cert_path=fullchain_path,
expiry=crypto_util.notAfter(cert_path).date(),
key_msg="Key is saved at: {}\n".format(key_path) if key_path else "",
renewal_msg="\nCertbot has set up a scheduled task to automatically renew this "
"certificate in the background." if config.preconfigured_renewal else "",
nl="\n" if config.verb == "run" else "" # Normalize spacing across verbs
)
)
def _csr_report_new_cert(config: interfaces.IConfig, cert_path: Optional[str],
chain_path: Optional[str], fullchain_path: Optional[str]):
""" --csr variant of _report_new_cert.
Until --csr is overhauled (#8332) this is transitional function to report the creation
of a new certificate using --csr.
TODO: remove this function and just call _report_new_cert when --csr is overhauled.
:param config: Configuration object
:type config: interfaces.IConfig
:param str cert_path: path to cert.pem
:param str chain_path: path to chain.pem
:param str fullchain_path: path to fullchain.pem
"""
if config.dry_run:
display_util.notify("The dry run was successful.")
return
assert cert_path and fullchain_path, "No certificates saved to report."
expiry = crypto_util.notAfter(cert_path).date()
reporter_util = zope.component.getUtility(interfaces.IReporter)
# Print the path to fullchain.pem because that's what modern webservers
# (Nginx and Apache2.4) will want.
verbswitch = ' with the "certonly" option' if config.verb == "run" else ""
privkey_statement = 'Your key file has been saved at:{br}{0}{br}'.format(
key_path, br=os.linesep) if key_path else ""
# XXX Perhaps one day we could detect the presence of known old webservers
# and say something more informative here.
msg = ('Congratulations! Your certificate and chain have been saved at:{br}'
'{0}{br}{1}'
'Your certificate will expire on {2}. To obtain a new or tweaked version of this '
'certificate in the future, simply run {3} again{4}. '
'To non-interactively renew *all* of your certificates, run "{3} renew"'
.format(fullchain_path, privkey_statement, expiry, cli.cli_command, verbswitch,
br=os.linesep))
reporter_util.add_message(msg, reporter_util.MEDIUM_PRIORITY)
display_util.notify(
("\nSuccessfully received certificate.\n"
"Certificate is saved at: {cert_path}\n"
"Intermediate CA chain is saved at: {chain_path}\n"
"Full certificate chain is saved at: {fullchain_path}\n"
"This certificate expires on {expiry}.").format(
cert_path=cert_path, chain_path=chain_path,
fullchain_path=fullchain_path, expiry=expiry,
)
)
def _determine_account(config):
@@ -616,7 +702,9 @@ def _delete_if_appropriate(config):
# don't delete if the archive_dir is used by some other lineage
archive_dir = storage.full_archive_path(
configobj.ConfigObj(storage.renewal_file_for_certname(config, config.certname)),
configobj.ConfigObj(
storage.renewal_file_for_certname(config, config.certname),
encoding='utf-8', default_encoding='utf-8'),
config, config.certname)
try:
cert_manager.match_and_check_overlaps(config, [lambda x: archive_dir],
@@ -781,6 +869,21 @@ def update_account(config, unused_plugins):
return None
def _cert_name_from_config_or_lineage(config: interfaces.IConfig,
lineage: Optional[storage.RenewableCert]) -> Optional[str]:
if lineage:
return lineage.lineagename
elif config.certname:
return config.certname
try:
cert_name = cert_manager.cert_path_to_lineage(config)
return cert_name
except errors.Error:
pass
return None
def _install_cert(config, le_client, domains, lineage=None):
"""Install a cert
@@ -803,8 +906,8 @@ def _install_cert(config, le_client, domains, lineage=None):
path_provider = lineage if lineage else config
assert path_provider.cert_path is not None
le_client.deploy_certificate(domains, path_provider.key_path,
path_provider.cert_path, path_provider.chain_path, path_provider.fullchain_path)
le_client.deploy_certificate(domains, path_provider.key_path, path_provider.cert_path,
path_provider.chain_path, path_provider.fullchain_path)
le_client.enhance_config(domains, path_provider.chain_path)
@@ -949,7 +1052,7 @@ def enhance(config, plugins):
if not enhancements.are_requested(config) and not oldstyle_enh:
msg = ("Please specify one or more enhancement types to configure. To list "
"the available enhancement types, run:\n\n%s --help enhance\n")
logger.warning(msg, sys.argv[0])
logger.error(msg, cli.cli_command)
raise errors.MisconfigurationError("No enhancements requested, exiting.")
try:
@@ -1172,15 +1275,27 @@ def run(config, plugins):
if should_get_cert:
_report_new_cert(config, cert_path, fullchain_path, key_path)
_install_cert(config, le_client, domains, new_lineage)
# The installer error, if any, is being stored as a value here, in order to first print
# relevant advice in a nice way, before re-raising the error for normal processing.
installer_err: Optional[errors.Error] = None
try:
_install_cert(config, le_client, domains, new_lineage)
if enhancements.are_requested(config) and new_lineage:
enhancements.enable(new_lineage, domains, installer, config)
if enhancements.are_requested(config) and new_lineage:
enhancements.enable(new_lineage, domains, installer, config)
if lineage is None or not should_get_cert:
display_ops.success_installation(domains)
else:
display_ops.success_renewal(domains)
if lineage is None or not should_get_cert:
display_ops.success_installation(domains)
else:
display_ops.success_renewal(domains)
except errors.Error as e:
installer_err = e
finally:
_report_next_steps(config, installer_err, new_lineage,
new_or_renewed_cert=should_get_cert)
# If the installer did fail, re-raise the error to bail out
if installer_err:
raise installer_err
_suggest_donation_if_appropriate(config)
eff.handle_subscription(config, le_client.account)
@@ -1188,6 +1303,7 @@ def run(config, plugins):
def _csr_get_and_save_cert(config, le_client):
# type: (interfaces.IConfig, client.Client) -> Tuple[Optional[str], Optional[str], Optional[str]] # pylint: disable=line-too-long
"""Obtain a cert using a user-supplied CSR
This works differently in the CSR case (for now) because we don't
@@ -1200,20 +1316,29 @@ def _csr_get_and_save_cert(config, le_client):
:param client: Client object
:type client: client.Client
:returns: `cert_path` and `fullchain_path` as absolute paths to the actual files
:returns: `cert_path`, `chain_path` and `fullchain_path` as absolute
paths to the actual files, or None for each if it's a dry-run.
:rtype: `tuple` of `str`
"""
csr, _ = config.actual_csr
csr_names = crypto_util.get_names_from_req(csr.data)
display_util.notify(
"{action} for {domains}".format(
action="Simulating a certificate request" if config.dry_run else
"Requesting a certificate",
domains=display_util.summarize_domain_list(csr_names)
)
)
cert, chain = le_client.obtain_certificate_from_csr(csr)
if config.dry_run:
logger.debug(
"Dry run: skipping saving certificate to %s", config.cert_path)
return None, None
cert_path, _, fullchain_path = le_client.save_certificate(
return None, None, None
cert_path, chain_path, fullchain_path = le_client.save_certificate(
cert, chain, os.path.normpath(config.cert_path),
os.path.normpath(config.chain_path), os.path.normpath(config.fullchain_path))
return cert_path, fullchain_path
return cert_path, chain_path, fullchain_path
def renew_cert(config, plugins, lineage):
@@ -1234,29 +1359,17 @@ def renew_cert(config, plugins, lineage):
:raises errors.PluginSelectionError: MissingCommandlineFlag if supplied parameters do not pass
"""
try:
# installers are used in auth mode to determine domain names
installer, auth = plug_sel.choose_configurator_plugins(config, plugins, "certonly")
except errors.PluginSelectionError as e:
logger.info("Could not choose appropriate plugin: %s", e)
raise
# installers are used in auth mode to determine domain names
installer, auth = plug_sel.choose_configurator_plugins(config, plugins, "certonly")
le_client = _init_le_client(config, auth, installer)
renewed_lineage = _get_and_save_cert(le_client, config, lineage=lineage)
notify = zope.component.getUtility(interfaces.IDisplay).notification
if installer is None:
notify("new certificate deployed without reload, fullchain is {0}".format(
lineage.fullchain), pause=False)
else:
if installer and not config.dry_run:
# In case of a renewal, reload server to pick up new certificate.
# In principle we could have a configuration option to inhibit this
# from happening.
# Run deployer
updater.run_renewal_deployer(config, renewed_lineage, installer)
installer.restart()
notify("new certificate deployed with reload of {0} server; fullchain is {1}".format(
config.installer, lineage.fullchain), pause=False)
display_util.notify(f"Reloading {config.installer} server after certificate renewal")
installer.restart() # type: ignore
def certonly(config, plugins):
@@ -1277,18 +1390,15 @@ def certonly(config, plugins):
"""
# SETUP: Select plugins and construct a client instance
try:
# installers are used in auth mode to determine domain names
installer, auth = plug_sel.choose_configurator_plugins(config, plugins, "certonly")
except errors.PluginSelectionError as e:
logger.info("Could not choose appropriate plugin: %s", e)
raise
# installers are used in auth mode to determine domain names
installer, auth = plug_sel.choose_configurator_plugins(config, plugins, "certonly")
le_client = _init_le_client(config, auth, installer)
if config.csr:
cert_path, fullchain_path = _csr_get_and_save_cert(config, le_client)
_report_new_cert(config, cert_path, fullchain_path)
cert_path, chain_path, fullchain_path = _csr_get_and_save_cert(config, le_client)
_csr_report_new_cert(config, cert_path, chain_path, fullchain_path)
_report_next_steps(config, None, None)
_suggest_donation_if_appropriate(config)
eff.handle_subscription(config, le_client.account)
return
@@ -1307,6 +1417,7 @@ def certonly(config, plugins):
fullchain_path = lineage.fullchain_path if lineage else None
key_path = lineage.key_path if lineage else None
_report_new_cert(config, cert_path, fullchain_path, key_path)
_report_next_steps(config, None, lineage, new_or_renewed_cert=should_get_cert)
_suggest_donation_if_appropriate(config)
eff.handle_subscription(config, le_client.account)
@@ -1367,7 +1478,7 @@ def make_displayer(config: configuration.NamespaceConfig
if config.quiet:
config.noninteractive_mode = True
devnull = open(os.devnull, "w")
devnull = open(os.devnull, "w") # pylint: disable=consider-using-with
displayer = display_util.NoninteractiveDisplay(devnull)
elif config.noninteractive_mode:
displayer = display_util.NoninteractiveDisplay(sys.stdout)
@@ -1407,9 +1518,15 @@ def main(cli_args=None):
logger.debug("Arguments: %r", cli_args)
logger.debug("Discovered plugins: %r", plugins)
# Some releases of Windows require escape sequences to be enable explicitly
misc.prepare_virtual_console()
# note: arg parser internally handles --help (and exits afterwards)
args = cli.prepare_and_parse_args(plugins, cli_args)
config = configuration.NamespaceConfig(args)
# This call is done only for retro-compatibility purposes.
# TODO: Remove this call once zope dependencies are removed from Certbot.
zope.component.provideUtility(config)
# On windows, shell without administrative right cannot create symlinks required by certbot.

View File

@@ -1,4 +1,5 @@
"""Manual authenticator plugin"""
import logging
from typing import Dict
import zope.component
@@ -10,11 +11,14 @@ from certbot import errors
from certbot import interfaces
from certbot import reverter
from certbot import util
from certbot._internal.cli import cli_constants
from certbot._internal import hooks
from certbot.compat import misc
from certbot.compat import os
from certbot.display import ops as display_ops
from certbot.plugins import common
logger = logging.getLogger(__name__)
@zope.interface.implementer(interfaces.IAuthenticator)
@zope.interface.provider(interfaces.IPluginFactory)
@@ -60,7 +64,7 @@ with the following value:
{validation}
"""
_DNS_VERIFY_INSTRUCTIONS = """
Before continuing, verify the TXT record has been deployed. Depending on the DNS
Before continuing, verify the TXT record has been deployed. Depending on the DNS
provider, this may take some time, from a few seconds to multiple minutes. You can
check if it has finished deploying with aid of online tools, such as the Google
Admin Toolbox: https://toolbox.googleapps.com/apps/dig/#TXT/{domain}.
@@ -125,6 +129,42 @@ permitted by DNS standards.)
'validation challenges either through shell scripts provided by '
'the user or by performing the setup manually.')
def auth_hint(self, failed_achalls):
has_chall = lambda cls: any(isinstance(achall.chall, cls) for achall in failed_achalls)
has_dns = has_chall(challenges.DNS01)
resource_names = {
challenges.DNS01: 'DNS TXT records',
challenges.HTTP01: 'challenge files',
challenges.TLSALPN01: 'TLS-ALPN certificates'
}
resources = ' and '.join(sorted([v for k, v in resource_names.items() if has_chall(k)]))
if self.conf('auth-hook'):
return (
'The Certificate Authority failed to verify the {resources} created by the '
'--manual-auth-hook. Ensure that this hook is functioning correctly{dns_hint}. '
'Refer to "{certbot} --help manual" and the Certbot User Guide.'
.format(
certbot=cli_constants.cli_command,
resources=resources,
dns_hint=(
' and that it waits a sufficient duration of time for DNS propagation'
) if has_dns else ''
)
)
else:
return (
'The Certificate Authority failed to verify the manually created {resources}. '
'Ensure that you created these in the correct location{dns_hint}.'
.format(
resources=resources,
dns_hint=(
', or try waiting longer for DNS propagation on the next attempt'
) if has_dns else ''
)
)
def get_chall_pref(self, domain):
# pylint: disable=unused-argument,missing-function-docstring
return [challenges.HTTP01, challenges.DNS01]
@@ -153,7 +193,7 @@ permitted by DNS standards.)
else:
os.environ.pop('CERTBOT_TOKEN', None)
os.environ.update(env)
_, out = self._execute_hook('auth-hook')
_, out = self._execute_hook('auth-hook', achall.domain)
env['CERTBOT_AUTH_OUTPUT'] = out.strip()
self.env[achall] = env
@@ -196,9 +236,16 @@ permitted by DNS standards.)
if 'CERTBOT_TOKEN' not in env:
os.environ.pop('CERTBOT_TOKEN', None)
os.environ.update(env)
self._execute_hook('cleanup-hook')
self._execute_hook('cleanup-hook', achall.domain)
self.reverter.recovery_routine()
def _execute_hook(self, hook_name):
return misc.execute_command(self.option_name(hook_name), self.conf(hook_name),
env=util.env_no_snap_for_external_calls())
def _execute_hook(self, hook_name, achall_domain):
returncode, err, out = misc.execute_command_status(
self.option_name(hook_name), self.conf(hook_name),
env=util.env_no_snap_for_external_calls()
)
display_ops.report_executed_command(
f"Hook '--manual-{hook_name}' for {achall_domain}", returncode, out, err)
return err, out

View File

@@ -2,10 +2,12 @@
import logging
from typing import Optional, Tuple
import zope.component
from certbot import errors
from certbot import interfaces
from certbot._internal.plugins import disco
from certbot.compat import os
from certbot.display import util as display_util
@@ -164,7 +166,9 @@ def record_chosen_plugins(config, plugins, auth, inst):
config.authenticator, config.installer)
def choose_configurator_plugins(config, plugins, verb):
def choose_configurator_plugins(config: interfaces.IConfig, plugins: disco.PluginsRegistry,
verb: str) -> Tuple[Optional[interfaces.IInstaller],
Optional[interfaces.IAuthenticator]]:
"""
Figure out which configurator we're going to use, modifies
config.authenticator and config.installer strings to reflect that choice if
@@ -172,7 +176,7 @@ def choose_configurator_plugins(config, plugins, verb):
:raises errors.PluginSelectionError if there was a problem
:returns: (an `IAuthenticator` or None, an `IInstaller` or None)
:returns: tuple of (`IInstaller` or None, `IAuthenticator` or None)
:rtype: tuple
"""

View File

@@ -61,6 +61,12 @@ to serve all files under specified web root ({0})."""
"file, it needs to be on a single line, like: webroot-map = "
'{"example.com":"/var/www"}.')
def auth_hint(self, failed_achalls): # pragma: no cover
return ("The Certificate Authority failed to download the temporary challenge files "
"created by Certbot. Ensure that the listed domains serve their content from "
"the provided --webroot-path/-w and that files created there can be downloaded "
"from the internet.")
def get_chall_pref(self, domain): # pragma: no cover
# pylint: disable=unused-argument,missing-function-docstring
return [challenges.HTTP01]
@@ -134,7 +140,7 @@ to serve all files under specified web root ({0})."""
"webroot when using the webroot plugin.")
return None if index == 0 else known_webroots[index - 1] # code == display_util.OK
def _prompt_for_new_webroot(self, domain, allowraise=False): # pylint: no-self-use
def _prompt_for_new_webroot(self, domain, allowraise=False):
code, webroot = ops.validated_directory(
_validate_webroot,
"Input the webroot for {0}:".format(domain),
@@ -183,7 +189,7 @@ to serve all files under specified web root ({0})."""
filesystem.copy_ownership_and_apply_mode(
path, prefix, 0o755, copy_user=True, copy_group=True)
except (OSError, AttributeError) as exception:
logger.info("Unable to change owner and uid of webroot directory")
logger.warning("Unable to change owner and uid of webroot directory")
logger.debug("Error was: %s", exception)
except OSError as exception:
raise errors.PluginError(
@@ -192,7 +198,7 @@ to serve all files under specified web root ({0})."""
finally:
filesystem.umask(old_umask)
def _get_validation_path(self, root_path, achall): # pylint: no-self-use
def _get_validation_path(self, root_path, achall):
return os.path.join(root_path, achall.chall.encode("token"))
def _perform_single(self, achall):

View File

@@ -14,7 +14,6 @@ from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives.serialization import load_pem_private_key
import OpenSSL
import zope.component
from certbot import crypto_util
@@ -68,18 +67,18 @@ def _reconstitute(config, full_path):
"""
try:
renewal_candidate = storage.RenewableCert(full_path, config)
except (errors.CertStorageError, IOError):
logger.warning("", exc_info=True)
logger.warning("Renewal configuration file %s is broken. Skipping.", full_path)
except (errors.CertStorageError, IOError) as error:
logger.error("Renewal configuration file %s is broken.", full_path)
logger.error("The error was: %s\nSkipping.", str(error))
logger.debug("Traceback was:\n%s", traceback.format_exc())
return None
if "renewalparams" not in renewal_candidate.configuration:
logger.warning("Renewal configuration file %s lacks "
logger.error("Renewal configuration file %s lacks "
"renewalparams. Skipping.", full_path)
return None
renewalparams = renewal_candidate.configuration["renewalparams"]
if "authenticator" not in renewalparams:
logger.warning("Renewal configuration file %s does not specify "
logger.error("Renewal configuration file %s does not specify "
"an authenticator. Skipping.", full_path)
return None
# Now restore specific values along with their data types, if
@@ -89,7 +88,7 @@ def _reconstitute(config, full_path):
restore_required_config_elements(config, renewalparams)
_restore_plugin_configs(config, renewalparams)
except (ValueError, errors.Error) as error:
logger.warning(
logger.error(
"An error occurred while parsing %s. The error was %s. "
"Skipping the file.", full_path, str(error))
logger.debug("Traceback was:\n%s", traceback.format_exc())
@@ -99,7 +98,7 @@ def _reconstitute(config, full_path):
config.domains = [util.enforce_domain_sanity(d)
for d in renewal_candidate.names()]
except errors.ConfigurationError as error:
logger.warning("Renewal configuration file %s references a certificate "
logger.error("Renewal configuration file %s references a certificate "
"that contains an invalid domain name. The problem "
"was: %s. Skipping.", full_path, error)
return None
@@ -295,24 +294,17 @@ def should_renew(config, lineage):
logger.debug("Auto-renewal forced with --force-renewal...")
return True
if lineage.should_autorenew():
logger.info("Cert is due for renewal, auto-renewing...")
logger.info("Certificate is due for renewal, auto-renewing...")
return True
if config.dry_run:
logger.info("Cert not due for renewal, but simulating renewal for dry run")
logger.info("Certificate not due for renewal, but simulating renewal for dry run")
return True
logger.info("Cert not yet due for renewal")
display_util.notify("Certificate not yet due for renewal")
return False
def _avoid_invalidating_lineage(config, lineage, original_server):
"""Do not renew a valid cert with one from a staging server!"""
# Some lineages may have begun with --staging, but then had production
# certificates added to them
with open(lineage.cert) as the_file:
contents = the_file.read()
latest_cert = OpenSSL.crypto.load_certificate(
OpenSSL.crypto.FILETYPE_PEM, contents)
if util.is_staging(config.server):
if not util.is_staging(original_server):
if not config.break_my_certs:
@@ -447,7 +439,7 @@ def handle_renewal_request(config):
try:
renewal_candidate = _reconstitute(lineage_config, renewal_file)
except Exception as e: # pylint: disable=broad-except
logger.warning("Renewal configuration file %s (cert: %s) "
logger.error("Renewal configuration file %s (cert: %s) "
"produced an unexpected error: %s. Skipping.",
renewal_file, lineagename, e)
logger.debug("Traceback was:\n%s", traceback.format_exc())
@@ -458,7 +450,8 @@ def handle_renewal_request(config):
if renewal_candidate is None:
parse_failures.append(renewal_file)
else:
# XXX: ensure that each call here replaces the previous one
# This call is done only for retro-compatibility purposes.
# TODO: Remove this call once zope dependencies are removed from Certbot.
zope.component.provideUtility(lineage_config)
renewal_candidate.ensure_deployed()
from certbot._internal import main

View File

@@ -67,13 +67,16 @@ def cert_path_for_cert_name(config: interfaces.IConfig, cert_name: str) -> str:
"""
cert_name_implied_conf = renewal_file_for_certname(config, cert_name)
return configobj.ConfigObj(cert_name_implied_conf)["fullchain"]
return configobj.ConfigObj(
cert_name_implied_conf, encoding='utf-8', default_encoding='utf-8')["fullchain"]
def config_with_defaults(config=None):
"""Merge supplied config, if provided, on top of builtin defaults."""
defaults_copy = configobj.ConfigObj(constants.RENEWER_DEFAULTS)
defaults_copy.merge(config if config is not None else configobj.ConfigObj())
defaults_copy = configobj.ConfigObj(
constants.RENEWER_DEFAULTS, encoding='utf-8', default_encoding='utf-8')
defaults_copy.merge(config if config is not None else configobj.ConfigObj(
encoding='utf-8', default_encoding='utf-8'))
return defaults_copy
@@ -114,7 +117,7 @@ def write_renewal_config(o_filename, n_filename, archive_dir, target, relevant_d
:rtype: configobj.ConfigObj
"""
config = configobj.ConfigObj(o_filename)
config = configobj.ConfigObj(o_filename, encoding='utf-8', default_encoding='utf-8')
config["version"] = certbot.__version__
config["archive_dir"] = archive_dir
for kind in ALL_FOUR:
@@ -196,7 +199,7 @@ def update_configuration(lineagename, archive_dir, target, cli_config):
write_renewal_config(config_filename, temp_filename, archive_dir, target, values)
filesystem.replace(temp_filename, config_filename)
return configobj.ConfigObj(config_filename)
return configobj.ConfigObj(config_filename, encoding='utf-8', default_encoding='utf-8')
def get_link_target(link):
@@ -324,10 +327,11 @@ def delete_files(config, certname):
full_default_archive_dir = full_archive_path(None, config, certname)
full_default_live_dir = _full_live_path(config, certname)
try:
renewal_config = configobj.ConfigObj(renewal_filename)
renewal_config = configobj.ConfigObj(
renewal_filename, encoding='utf-8', default_encoding='utf-8')
except configobj.ConfigObjError:
# config is corrupted
logger.warning("Could not parse %s. You may wish to manually "
logger.error("Could not parse %s. You may wish to manually "
"delete the contents of %s and %s.", renewal_filename,
full_default_live_dir, full_default_archive_dir)
raise errors.CertStorageError(
@@ -336,7 +340,7 @@ def delete_files(config, certname):
# we couldn't read it, but let's at least delete it
# if this was going to fail, it already would have.
os.remove(renewal_filename)
logger.debug("Removed %s", renewal_filename)
logger.info("Removed %s", renewal_filename)
# cert files and (hopefully) live directory
# it's not guaranteed that the files are in our default storage
@@ -434,7 +438,8 @@ class RenewableCert(interfaces.RenewableCert):
# systemwide renewal configuration; self.configfile should be
# used to make and save changes.
try:
self.configfile = configobj.ConfigObj(config_filename)
self.configfile = configobj.ConfigObj(
config_filename, encoding='utf-8', default_encoding='utf-8')
except configobj.ConfigObjError:
raise errors.CertStorageError(
"error parsing {0}".format(config_filename))

View File

@@ -29,7 +29,7 @@ def run_generic_updaters(config, lineage, plugins):
try:
installer = plug_sel.get_unprepared_installer(config, plugins)
except errors.Error as e:
logger.warning("Could not choose appropriate plugin for updaters: %s", e)
logger.error("Could not choose appropriate plugin for updaters: %s", e)
return
if installer:
_run_updaters(lineage, installer, config)

View File

@@ -8,6 +8,7 @@ import logging
import select
import subprocess
import sys
import warnings
from typing import Optional
from typing import Tuple
@@ -16,6 +17,8 @@ from certbot.compat import os
try:
from win32com.shell import shell as shellwin32
from win32console import GetStdHandle, STD_OUTPUT_HANDLE
from pywintypes import error as pywinerror
POSIX_MODE = False
except ImportError: # pragma: no cover
POSIX_MODE = True
@@ -38,6 +41,26 @@ def raise_for_non_administrative_windows_rights() -> None:
raise errors.Error('Error, certbot must be run on a shell with administrative rights.')
def prepare_virtual_console() -> None:
"""
On Windows, ensure that Console Virtual Terminal Sequences are enabled.
"""
if POSIX_MODE:
return
# https://docs.microsoft.com/en-us/windows/console/setconsolemode
ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004
# stdout/stderr will be the same console screen buffer, but this could return None or raise
try:
h = GetStdHandle(STD_OUTPUT_HANDLE)
if h:
h.SetConsoleMode(h.GetConsoleMode() | ENABLE_VIRTUAL_TERMINAL_PROCESSING)
except pywinerror:
logger.debug("Failed to set console mode", exc_info=True)
def readline_with_timeout(timeout: float, prompt: str) -> str:
"""
Read user input to return the first line entered, or raise after specified timeout.
@@ -112,38 +135,68 @@ def underscores_for_unsupported_characters_in_path(path: str) -> str:
return drive + tail.replace(':', '_')
def execute_command(cmd_name: str, shell_cmd: str, env: Optional[dict] = None) -> Tuple[str, str]:
def execute_command_status(cmd_name: str, shell_cmd: str,
env: Optional[dict] = None) -> Tuple[int, str, str]:
"""
Run a command:
- on Linux command will be run by the standard shell selected with Popen(shell=True)
- on Linux command will be run by the standard shell selected with
subprocess.run(shell=True)
- on Windows command will be run in a Powershell shell
This differs from execute_command: it returns the exit code, and does not log the result
and output of the command.
:param str cmd_name: the user facing name of the hook being run
:param str shell_cmd: shell command to execute
:param dict env: environ to pass into Popen
:param dict env: environ to pass into subprocess.run
:returns: `tuple` (`str` stderr, `str` stdout)
:returns: `tuple` (`int` returncode, `str` stderr, `str` stdout)
"""
logger.info("Running %s command: %s", cmd_name, shell_cmd)
if POSIX_MODE:
cmd = subprocess.Popen(shell_cmd, shell=True, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, universal_newlines=True,
env=env)
proc = subprocess.run(shell_cmd, shell=True, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, universal_newlines=True,
check=False, env=env)
else:
line = ['powershell.exe', '-Command', shell_cmd]
cmd = subprocess.Popen(line, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
universal_newlines=True, env=env)
proc = subprocess.run(line, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
universal_newlines=True, check=False, env=env)
# universal_newlines causes Popen.communicate()
# to return str objects instead of bytes in Python 3
out, err = cmd.communicate()
# universal_newlines causes stdout and stderr to be str objects instead of
# bytes in Python 3
out, err = proc.stdout, proc.stderr
return proc.returncode, err, out
def execute_command(cmd_name: str, shell_cmd: str, env: Optional[dict] = None) -> Tuple[str, str]:
"""
Run a command:
- on Linux command will be run by the standard shell selected with
subprocess.run(shell=True)
- on Windows command will be run in a Powershell shell
This differs from execute_command: it returns the exit code, and does not log the result
and output of the command.
:param str cmd_name: the user facing name of the hook being run
:param str shell_cmd: shell command to execute
:param dict env: environ to pass into subprocess.run
:returns: `tuple` (`str` stderr, `str` stdout)
"""
# Deprecation per https://github.com/certbot/certbot/issues/8854
warnings.warn(
"execute_command will be deprecated in the future, use execute_command_status instead",
PendingDeprecationWarning
)
returncode, err, out = execute_command_status(cmd_name, shell_cmd, env)
base_cmd = os.path.basename(shell_cmd.split(None, 1)[0])
if out:
logger.info('Output from %s command %s:\n%s', cmd_name, base_cmd, out)
if cmd.returncode != 0:
if returncode != 0:
logger.error('%s command "%s" returned error code %d',
cmd_name, shell_cmd, cmd.returncode)
cmd_name, shell_cmd, returncode)
if err:
logger.error('Error output from %s command %s:\n%s', cmd_name, base_cmd, err)
return err, out

View File

@@ -9,6 +9,7 @@ import logging
import re
import warnings
from typing import List, Set
# See https://github.com/pyca/cryptography/issues/4275
from cryptography import x509 # type: ignore
from cryptography.exceptions import InvalidSignature
@@ -37,9 +38,10 @@ logger = logging.getLogger(__name__)
# High level functions
def init_save_key(
key_size, key_dir, key_type="rsa", elliptic_curve="secp256r1", keyname="key-certbot.pem"
):
def generate_key(key_size: int, key_dir: str, key_type: str = "rsa",
elliptic_curve: str = "secp256r1", keyname: str = "key-certbot.pem",
strict_permissions: bool = True) -> util.Key:
"""Initializes and saves a privkey.
Inits key and saves it in PEM format on the filesystem.
@@ -47,6 +49,56 @@ def init_save_key(
.. note:: keyname is the attempted filename, it may be different if a file
already exists at the path.
:param int key_size: key size in bits if key size is rsa.
:param str key_dir: Key save directory.
:param str key_type: Key Type [rsa, ecdsa]
:param str elliptic_curve: Name of the elliptic curve if key type is ecdsa.
:param str keyname: Filename of key
:param bool strict_permissions: If true and key_dir exists, an exception is raised if
the directory doesn't have 0700 permissions or isn't owned by the current user.
:returns: Key
:rtype: :class:`certbot.util.Key`
:raises ValueError: If unable to generate the key given key_size.
"""
try:
key_pem = make_key(
bits=key_size, elliptic_curve=elliptic_curve or "secp256r1", key_type=key_type,
)
except ValueError as err:
logger.debug("", exc_info=True)
logger.error("Encountered error while making key: %s", str(err))
raise err
# Save file
util.make_or_verify_dir(key_dir, 0o700, strict_permissions)
key_f, key_path = util.unique_file(
os.path.join(key_dir, keyname), 0o600, "wb")
with key_f:
key_f.write(key_pem)
if key_type == 'rsa':
logger.debug("Generating RSA key (%d bits): %s", key_size, key_path)
else:
logger.debug("Generating ECDSA key (%d bits): %s", key_size, key_path)
return util.Key(key_path, key_pem)
# TODO: Remove this call once zope dependencies are removed from Certbot.
def init_save_key(key_size, key_dir, key_type="rsa", elliptic_curve="secp256r1",
keyname="key-certbot.pem"):
"""Initializes and saves a privkey.
Inits key and saves it in PEM format on the filesystem.
.. note:: keyname is the attempted filename, it may be different if a file
already exists at the path.
.. deprecated:: 1.16.0
Use :func:`generate_key` instead.
:param int key_size: key size in bits if key size is rsa.
:param str key_dir: Key save directory.
:param str key_type: Key Type [rsa, ecdsa]
@@ -59,32 +111,52 @@ def init_save_key(
:raises ValueError: If unable to generate the key given key_size.
"""
try:
key_pem = make_key(
bits=key_size, elliptic_curve=elliptic_curve or "secp256r1", key_type=key_type,
)
except ValueError as err:
logger.error("", exc_info=True)
raise err
warnings.warn("certbot.crypto_util.init_save_key is deprecated, please use "
"certbot.crypto_util.generate_key instead.", DeprecationWarning)
config = zope.component.getUtility(interfaces.IConfig)
# Save file
util.make_or_verify_dir(key_dir, 0o700, config.strict_permissions)
key_f, key_path = util.unique_file(
os.path.join(key_dir, keyname), 0o600, "wb")
with key_f:
key_f.write(key_pem)
if key_type == 'rsa':
logger.debug("Generating RSA key (%d bits): %s", key_size, key_path)
else:
logger.debug("Generating ECDSA key (%d bits): %s", key_size, key_path)
return util.Key(key_path, key_pem)
return generate_key(key_size, key_dir, key_type=key_type, elliptic_curve=elliptic_curve,
keyname=keyname, strict_permissions=config.strict_permissions)
def generate_csr(privkey: util.Key, names: Set[str], path: str,
must_staple: bool = False, strict_permissions: bool = True) -> util.CSR:
"""Initialize a CSR with the given private key.
:param privkey: Key to include in the CSR
:type privkey: :class:`certbot.util.Key`
:param set names: `str` names to include in the CSR
:param str path: Certificate save directory.
:param bool must_staple: If true, include the TLS Feature extension "OCSP Must Staple"
:param bool strict_permissions: If true and path exists, an exception is raised if
the directory doesn't have 0755 permissions or isn't owned by the current user.
:returns: CSR
:rtype: :class:`certbot.util.CSR`
"""
csr_pem = acme_crypto_util.make_csr(
privkey.pem, names, must_staple=must_staple)
# Save CSR
util.make_or_verify_dir(path, 0o755, strict_permissions)
csr_f, csr_filename = util.unique_file(
os.path.join(path, "csr-certbot.pem"), 0o644, "wb")
with csr_f:
csr_f.write(csr_pem)
logger.debug("Creating CSR: %s", csr_filename)
return util.CSR(csr_filename, csr_pem, "pem")
# TODO: Remove this call once zope dependencies are removed from Certbot.
def init_save_csr(privkey, names, path):
"""Initialize a CSR with the given private key.
.. deprecated:: 1.16.0
Use :func:`generate_csr` instead.
:param privkey: Key to include in the CSR
:type privkey: :class:`certbot.util.Key`
@@ -96,20 +168,13 @@ def init_save_csr(privkey, names, path):
:rtype: :class:`certbot.util.CSR`
"""
warnings.warn("certbot.crypto_util.init_save_csr is deprecated, please use "
"certbot.crypto_util.generate_csr instead.", DeprecationWarning)
config = zope.component.getUtility(interfaces.IConfig)
csr_pem = acme_crypto_util.make_csr(
privkey.pem, names, must_staple=config.must_staple)
# Save CSR
util.make_or_verify_dir(path, 0o755, config.strict_permissions)
csr_f, csr_filename = util.unique_file(
os.path.join(path, "csr-certbot.pem"), 0o644, "wb")
with csr_f:
csr_f.write(csr_pem)
logger.debug("Creating CSR: %s", csr_filename)
return util.CSR(csr_filename, csr_pem, "pem")
return generate_csr(privkey, names, path, must_staple=config.must_staple,
strict_permissions=config.strict_permissions)
# WARNING: the csr and private key file are possible attack vectors for TOCTOU
@@ -387,8 +452,9 @@ def _load_cert_or_req(cert_or_req_str, load_func,
typ=crypto.FILETYPE_PEM):
try:
return load_func(typ, cert_or_req_str)
except crypto.Error:
logger.error("", exc_info=True)
except crypto.Error as err:
logger.debug("", exc_info=True)
logger.error("Encountered error while loading certificate or csr: %s", str(err))
raise
@@ -437,6 +503,18 @@ def get_names_from_cert(csr, typ=crypto.FILETYPE_PEM):
csr, crypto.load_certificate, typ)
def get_names_from_req(csr: str, typ: int = crypto.FILETYPE_PEM) -> List[str]:
"""Get a list of domains from a CSR, including the CN if it is set.
:param str cert: CSR (encoded).
:param typ: `crypto.FILETYPE_PEM` or `crypto.FILETYPE_ASN1`
:returns: A list of domain names.
:rtype: list
"""
return _get_names_from_cert_or_req(csr, crypto.load_certificate_request, typ)
def dump_pyopenssl_chain(chain, filetype=crypto.FILETYPE_PEM):
"""Dump certificate chain into a bundle.
@@ -589,7 +667,7 @@ def find_chain_with_issuer(fullchains, issuer_cn, warn_on_no_match=False):
# Nothing matched, return whatever was first in the list.
if warn_on_no_match:
logger.info("Certbot has been configured to prefer certificate chains with "
logger.warning("Certbot has been configured to prefer certificate chains with "
"issuer '%s', but no chain from the CA matched this issuer. Using "
"the default certificate chain instead.", issuer_cn)
return fullchains[0]

View File

@@ -1,5 +1,6 @@
"""Contains UI methods for LE user operations."""
import logging
from textwrap import indent
import zope.component
@@ -122,8 +123,7 @@ def choose_names(installer, question=None):
names = get_valid_domains(domains)
if not names:
return _choose_names_manually(
"No names were found in your configuration files. ")
return _choose_names_manually()
code, names = _filter_names(names, question)
if code == display_util.OK and names:
@@ -191,7 +191,8 @@ def _choose_names_manually(prompt_prefix=""):
"""
code, input_ = z_util(interfaces.IDisplay).input(
prompt_prefix +
"Please enter in your domain name(s) (comma and/or space separated) ",
"Please enter the domain name(s) you would like on your certificate "
"(comma and/or space separated)",
cli_flag="--domains", force_interactive=True)
if code == display_util.OK:
@@ -240,25 +241,22 @@ def success_installation(domains):
:param list domains: domain names which were enabled
"""
z_util(interfaces.IDisplay).notification(
"Congratulations! You have successfully enabled {0}".format(
_gen_https_names(domains)),
pause=False)
display_util.notify(
"Congratulations! You have successfully enabled HTTPS on {0}"
.format(_gen_https_names(domains))
)
def success_renewal(domains):
def success_renewal(unused_domains):
"""Display a box confirming the renewal of an existing certificate.
:param list domains: domain names which were renewed
"""
z_util(interfaces.IDisplay).notification(
display_util.notify(
"Your existing certificate has been successfully renewed, and the "
"new certificate has been installed.{1}{1}"
"The new certificate covers the following domains: {0}".format(
_gen_https_names(domains),
os.linesep),
pause=False)
"new certificate has been installed."
)
def success_revocation(cert_path):
@@ -273,6 +271,24 @@ def success_revocation(cert_path):
)
def report_executed_command(command_name: str, returncode: int, stdout: str, stderr: str) -> None:
"""Display a message describing the success or failure of an executed process (e.g. hook).
:param str command_name: Human-readable description of the executed command
:param int returncode: The exit code of the executed command
:param str stdout: The stdout output of the executed command
:param str stderr: The stderr output of the executed command
"""
out_s, err_s = stdout.strip(), stderr.strip()
if returncode != 0:
logger.warning("%s reported error code %d", command_name, returncode)
if out_s:
display_util.notify(f"{command_name} ran with output:\n{indent(out_s, ' ')}")
if err_s:
logger.warning("%s ran with error output:\n%s", command_name, indent(err_s, ' '))
def _gen_https_names(domains):
"""Returns a string of the https domains.

View File

@@ -3,8 +3,8 @@ from datetime import datetime
from datetime import timedelta
import logging
import re
import subprocess
from subprocess import PIPE
from subprocess import Popen
from typing import Optional
from typing import Tuple
@@ -50,11 +50,10 @@ class RevocationChecker:
return
# New versions of openssl want -header var=val, old ones want -header var val
test_host_format = Popen(["openssl", "ocsp", "-header", "var", "val"],
test_host_format = subprocess.run(["openssl", "ocsp", "-header", "var", "val"],
stdout=PIPE, stderr=PIPE, universal_newlines=True,
env=util.env_no_snap_for_external_calls())
_out, err = test_host_format.communicate()
if "Missing =" in err:
check=False, env=util.env_no_snap_for_external_calls())
if "Missing =" in test_host_format.stderr:
self.host_args = lambda host: ["Host=" + host]
else:
self.host_args = lambda host: ["Host", host]
@@ -193,7 +192,7 @@ def _check_ocsp_cryptography(cert_path: str, chain_path: str, url: str, timeout:
# Check OCSP response validity
if response_ocsp.response_status != ocsp.OCSPResponseStatus.SUCCESSFUL:
logger.error("Invalid OCSP response status for %s: %s",
logger.warning("Invalid OCSP response status for %s: %s",
cert_path, response_ocsp.response_status)
return False
@@ -201,13 +200,13 @@ def _check_ocsp_cryptography(cert_path: str, chain_path: str, url: str, timeout:
try:
_check_ocsp_response(response_ocsp, request, issuer, cert_path)
except UnsupportedAlgorithm as e:
logger.error(str(e))
logger.warning(str(e))
except errors.Error as e:
logger.error(str(e))
logger.warning(str(e))
except InvalidSignature:
logger.error('Invalid signature on OCSP response for %s', cert_path)
logger.warning('Invalid signature on OCSP response for %s', cert_path)
except AssertionError as error:
logger.error('Invalid OCSP response for %s: %s.', cert_path, str(error))
logger.warning('Invalid OCSP response for %s: %s.', cert_path, str(error))
else:
# Check OCSP certificate status
logger.debug("OCSP certificate status for %s is: %s",

View File

@@ -97,6 +97,33 @@ class Plugin:
"""Find a configuration value for variable ``var``."""
return getattr(self.config, self.dest(var))
def auth_hint(self, failed_achalls):
# type: (List[achallenges.AnnotatedChallenge]) -> str
"""Human-readable string to help the user troubleshoot the authenticator.
Shown to the user if one or more of the attempted challenges were not a success.
Should describe, in simple language, what the authenticator tried to do, what went
wrong and what the user should try as their "next steps".
TODO: auth_hint belongs in IAuthenticator but can't be added until the next major
version of Certbot. For now, it lives in .Plugin and auth_handler will only call it
on authenticators that subclass .Plugin. For now, inherit from `.Plugin` to implement
and/or override the method.
:param list failed_achalls: List of one or more failed challenges
(:class:`achallenges.AnnotatedChallenge` subclasses).
:rtype str:
"""
# This is a fallback hint. Authenticators should implement their own auth_hint that
# addresses the specific mechanics of that authenticator.
challs = " and ".join(sorted({achall.typ for achall in failed_achalls}))
return ("The Certificate Authority couldn't externally verify that the {name} plugin "
"completed the required {challs} challenges. Ensure the plugin is configured "
"correctly and that the changes it makes are accessible from the internet."
.format(name=self.name, challs=challs))
class Installer(Plugin):
"""An installer base class with reverter and ssl_dhparam methods defined.

View File

@@ -37,6 +37,15 @@ class DNSAuthenticator(common.Plugin):
help='The number of seconds to wait for DNS to propagate before asking the ACME server '
'to verify the DNS record.')
def auth_hint(self, failed_achalls):
delay = self.conf('propagation-seconds')
return (
'The Certificate Authority failed to verify the DNS TXT records created by --{name}. '
'Ensure the above domains are hosted by this DNS provider, or try increasing '
'--{name}-propagation-seconds (currently {secs} second{suffix}).'
.format(name=self.name, secs=delay, suffix='s' if delay != 1 else '')
)
def get_chall_pref(self, unused_domain): # pylint: disable=missing-function-docstring
return [challenges.DNS01]
@@ -63,7 +72,7 @@ class DNSAuthenticator(common.Plugin):
# DNS updates take time to propagate and checking to see if the update has occurred is not
# reliable (the machine this code is running on might be able to see an update before
# the ACME server). So: we sleep for a short amount of time we believe to be long enough.
logger.info("Waiting %d seconds for DNS changes to propagate",
display_util.notify("Waiting %d seconds for DNS changes to propagate" %
self.conf('propagation-seconds'))
sleep(self.conf('propagation-seconds'))

View File

@@ -45,7 +45,7 @@ class LexiconClient:
self._find_domain_id(domain)
try:
self.provider.create_record(type='TXT', name=record_name, content=record_content)
self.provider.create_record(rtype='TXT', name=record_name, content=record_content)
except RequestException as e:
logger.debug('Encountered error adding TXT record: %s', e, exc_info=True)
raise errors.PluginError('Error adding TXT record: {0}'.format(e))
@@ -67,7 +67,7 @@ class LexiconClient:
return
try:
self.provider.delete_record(type='TXT', name=record_name, content=record_content)
self.provider.delete_record(rtype='TXT', name=record_name, content=record_content)
except RequestException as e:
logger.debug('Encountered error deleting TXT record: %s', e, exc_info=True)

View File

@@ -67,7 +67,8 @@ class _LexiconAwareTestCase(Protocol):
class BaseLexiconAuthenticatorTest(dns_test_common.BaseAuthenticatorTest):
def test_perform(self: _AuthenticatorCallableLexiconTestCase):
@test_util.patch_get_utility()
def test_perform(self: _AuthenticatorCallableLexiconTestCase, unused_mock_get_utility):
self.auth.perform([self.achall])
expected = [mock.call.add_txt_record(DOMAIN, '_acme-challenge.'+DOMAIN, mock.ANY)]
@@ -94,7 +95,7 @@ class BaseLexiconClientTest:
def test_add_txt_record(self: _LexiconAwareTestCase):
self.client.add_txt_record(DOMAIN, self.record_name, self.record_content)
self.provider_mock.create_record.assert_called_with(type='TXT',
self.provider_mock.create_record.assert_called_with(rtype='TXT',
name=self.record_name,
content=self.record_content)
@@ -103,7 +104,7 @@ class BaseLexiconClientTest:
self.client.add_txt_record(DOMAIN, self.record_name, self.record_content)
self.provider_mock.create_record.assert_called_with(type='TXT',
self.provider_mock.create_record.assert_called_with(rtype='TXT',
name=self.record_name,
content=self.record_content)
@@ -147,7 +148,7 @@ class BaseLexiconClientTest:
def test_del_txt_record(self: _LexiconAwareTestCase):
self.client.del_txt_record(DOMAIN, self.record_name, self.record_content)
self.provider_mock.delete_record.assert_called_with(type='TXT',
self.provider_mock.delete_record.assert_called_with(rtype='TXT',
name=self.record_name,
content=self.record_content)

View File

@@ -198,6 +198,7 @@ class Reverter:
Read the file returning the lines, and a pointer to the end of the file.
"""
# pylint: disable=consider-using-with
# Open up filepath differently depending on if it already exists
if os.path.isfile(filepath):
op_fd = open(filepath, "r+")
@@ -348,26 +349,18 @@ class Reverter:
"""
commands_fp = os.path.join(self._get_cp_dir(temporary), "COMMANDS")
command_file = None
# It is strongly advised to set newline = '' on Python 3 with CSV,
# and it fixes problems on Windows.
kwargs = {'newline': ''}
try:
if os.path.isfile(commands_fp):
command_file = open(commands_fp, "a", **kwargs) # type: ignore
else:
command_file = open(commands_fp, "w", **kwargs) # type: ignore
csvwriter = csv.writer(command_file)
csvwriter.writerow(command)
mode = "a" if os.path.isfile(commands_fp) else "w"
with open(commands_fp, mode, **kwargs) as f: # type: ignore
csvwriter = csv.writer(f)
csvwriter.writerow(command)
except (IOError, OSError):
logger.error("Unable to register undo command")
raise errors.ReverterError(
"Unable to register undo command.")
finally:
if command_file is not None:
command_file.close()
def _get_cp_dir(self, temporary):
"""Return the proper reverter directory."""
@@ -521,7 +514,7 @@ class Reverter:
filesystem.replace(self.config.in_progress_dir, final_dir)
return
except OSError:
logger.warning("Extreme, unexpected race condition, retrying (%s)", timestamp)
logger.warning("Unexpected race condition, retrying (%s)", timestamp)
# After 10 attempts... something is probably wrong here...
logger.error(

View File

@@ -259,7 +259,7 @@ class FreezableMock:
def _create_get_utility_mock():
display = FreezableMock()
# Use pylint code for disable to keep on single line under line length limit
for name in interfaces.IDisplay.names(): # pylint: E1120
for name in interfaces.IDisplay.names():
if name != 'notification':
frozen_mock = FreezableMock(frozen=True, func=_assert_valid_call)
setattr(display, name, frozen_mock)
@@ -284,7 +284,7 @@ def _create_get_utility_mock_with_stdout(stdout):
display = FreezableMock()
# Use pylint code for disable to keep on single line under line length limit
for name in interfaces.IDisplay.names(): # pylint: E1120
for name in interfaces.IDisplay.names():
if name == 'notification':
frozen_mock = FreezableMock(frozen=True,
func=_write_msg)

View File

@@ -16,6 +16,7 @@ from typing import Dict
from typing import Text
from typing import Tuple
from typing import Union
import warnings
import configargparse
@@ -89,32 +90,31 @@ def env_no_snap_for_external_calls():
def run_script(params, log=logger.error):
"""Run the script with the given params.
:param list params: List of parameters to pass to Popen
:param list params: List of parameters to pass to subprocess.run
:param callable log: Logger method to use for errors
"""
try:
proc = subprocess.Popen(params,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True,
env=env_no_snap_for_external_calls())
proc = subprocess.run(params,
check=False,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True,
env=env_no_snap_for_external_calls())
except (OSError, ValueError):
msg = "Unable to run the command: %s" % " ".join(params)
log(msg)
raise errors.SubprocessError(msg)
stdout, stderr = proc.communicate()
if proc.returncode != 0:
msg = "Error while running %s.\n%s\n%s" % (
" ".join(params), stdout, stderr)
" ".join(params), proc.stdout, proc.stderr)
# Enter recovery routine...
log(msg)
raise errors.SubprocessError(msg)
return stdout, stderr
return proc.stdout, proc.stderr
def exe_exists(exe):
@@ -399,20 +399,20 @@ def get_python_os_info(pretty=False):
os_ver = info[1]
elif os_type.startswith('darwin'):
try:
proc = subprocess.Popen(
proc = subprocess.run(
["/usr/bin/sw_vers", "-productVersion"],
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
universal_newlines=True,
check=False, universal_newlines=True,
env=env_no_snap_for_external_calls(),
)
except OSError:
proc = subprocess.Popen(
proc = subprocess.run(
["sw_vers", "-productVersion"],
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
universal_newlines=True,
check=False, universal_newlines=True,
env=env_no_snap_for_external_calls(),
)
os_ver = proc.communicate()[0].rstrip('\n')
os_ver = proc.stdout.rstrip('\n')
elif os_type.startswith('freebsd'):
# eg "9.3-RC3-p1"
os_ver = os_ver.partition("-")[0]
@@ -434,14 +434,14 @@ def safe_email(email):
"""Scrub email address before using it."""
if EMAIL_REGEX.match(email) is not None:
return not email.startswith(".") and ".." not in email
logger.warning("Invalid email address: %s.", email)
logger.error("Invalid email address: %s.", email)
return False
class DeprecatedArgumentAction(argparse.Action):
"""Action to log a warning when an argument is used."""
def __call__(self, unused1, unused2, unused3, option_string=None):
logger.warning("Use of %s is deprecated.", option_string)
warnings.warn("Use of %s is deprecated." % option_string, DeprecationWarning)
def add_deprecated_argument(add_argument, argument_name, nargs):

View File

@@ -41,7 +41,7 @@ optional arguments:
and ~/.config/letsencrypt/cli.ini)
-v, --verbose This flag can be used multiple times to incrementally
increase the verbosity of output, e.g. -vvv. (default:
-2)
-3)
--max-log-backups MAX_LOG_BACKUPS
Specifies the maximum number of backup logs that
should be kept by Certbot's built in log rotation.
@@ -118,7 +118,7 @@ optional arguments:
case, and to know when to deprecate support for past
Python versions and flags. If you wish to hide this
information from the Let's Encrypt server, set this to
"". (default: CertbotACMEClient/1.15.0 (certbot;
"". (default: CertbotACMEClient/1.16.0 (certbot;
OS_NAME OS_VERSION) Authenticator/XXX Installer/YYY
(SUBCOMMAND; flags: FLAGS) Py/major.minor.patchlevel).
The flags encoded in the user agent are: --duplicate,

View File

@@ -84,7 +84,7 @@ Apache
The Apache plugin currently `supports
<https://github.com/certbot/certbot/blob/master/certbot-apache/certbot_apache/_internal/entrypoint.py>`_
modern OSes based on Debian, Fedora, SUSE, Gentoo and Darwin.
modern OSes based on Debian, Fedora, SUSE, Gentoo, CentOS and Darwin.
This automates both obtaining *and* installing certificates on an Apache
webserver. To specify this plugin on the command line, simply include
``--apache``.
@@ -285,6 +285,7 @@ dns-clouddns_ Y N DNS Authentication using CloudDNS API
dns-lightsail_ Y N DNS Authentication using Amazon Lightsail DNS API
dns-inwx_ Y Y DNS Authentication for INWX through the XML API
dns-azure_ Y N DNS Authentication using Azure DNS
dns-godaddy_ Y N DNS Authentication using Godaddy DNS
================== ==== ==== ===============================================================
.. _haproxy: https://github.com/greenhost/certbot-haproxy
@@ -300,6 +301,7 @@ dns-azure_ Y N DNS Authentication using Azure DNS
.. _dns-lightsail: https://github.com/noi/certbot-dns-lightsail
.. _dns-inwx: https://github.com/oGGy990/certbot-dns-inwx/
.. _dns-azure: https://github.com/binkhq/certbot-dns-azure
.. _dns-godaddy: https://github.com/miigotu/certbot-dns-godaddy
If you're interested, you can also :ref:`write your own plugin <dev-plugin>`.
@@ -693,10 +695,50 @@ is done by means of a scheduled task which runs ``certbot renew`` periodically.
If you are unsure whether you need to configure automated renewal:
1. Review the instructions for your system at https://certbot.eff.org/instructions.
They will describe how to set up a scheduled task, if necessary.
2. (Linux/BSD): Check your system's crontab (typically `/etc/crontab` and
`/etc/cron.*/*`) and systemd timers (``systemctl list-timers``).
1. Review the instructions for your system and installation method at
https://certbot.eff.org/instructions. They will describe how to set up a scheduled task,
if necessary. If no step is listed, your system comes with automated renewal pre-installed,
and you should not need to take any additional actions.
2. On Linux and BSD, you can check to see if your installation method has pre-installed a timer
for you. To do so, look for the ``certbot renew`` command in either your system's crontab
(typically `/etc/crontab` or `/etc/cron.*/*`) or systemd timers (``systemctl list-timers``).
3. If you're still not sure, you can configure automated renewal manually by following the steps
in the next section. Certbot has been carefully engineered to handle the case where both manual
automated renewal and pre-installed automated renewal are set up.
Setting up automated renewal
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If you think you may need to set up automated renewal, follow these instructions to set up a
scheduled task to automatically renew your certificates in the background. If you are unsure
whether your system has a pre-installed scheduled task for Certbot, it is safe to follow these
instructions to create one.
If you're using Windows, these instructions are not neccessary as Certbot on Windows comes with
a scheduled task for automated renewal pre-installed.
Run the following line, which will add a cron job to `/etc/crontab`:
.. code-block:: shell
SLEEPTIME=$(awk 'BEGIN{srand(); print int(rand()*(3600+1))}'); echo "0 0,12 * * * root sleep $SLEEPTIME && certbot renew -q" | sudo tee -a /etc/crontab > /dev/null
If you needed to stop your webserver to run Certbot, you'll want to
add ``pre`` and ``post`` hooks to stop and start your webserver automatically.
For example, if your webserver is HAProxy, run the following commands to create the hook files
in the appropriate directory:
.. code-block:: shell
sudo sh -c 'printf "#!/bin/sh\nservice haproxy stop\n" > /etc/letsencrypt/renewal-hooks/pre/haproxy.sh'
sudo sh -c 'printf "#!/bin/sh\nservice haproxy start\n" > /etc/letsencrypt/renewal-hooks/post/haproxy.sh'
sudo chmod 755 /etc/letsencrypt/renewal-hooks/pre/haproxy.sh
sudo chmod 755 /etc/letsencrypt/renewal-hooks/post/haproxy.sh
Congratulations, Certbot will now automatically renew your certificates in the background.
If you are interested in learning more about how Certbot renews your certificates, see the
`Renewing certificates`_ section above.
.. _where-certs:

View File

@@ -13,8 +13,6 @@ domains = example.com
text = True
agree-tos = True
debug = True
# Unfortunately, it's not possible to specify "verbose" multiple times
# (correspondingly to -vvvvvv)
verbose = True
verbose = 3
authenticator = standalone

View File

@@ -1,2 +0,0 @@
# Remember to update setup.py to match the package versions below.
acme[dev]==1.8.0

View File

@@ -41,7 +41,10 @@ version = meta['version']
# here to avoid masking the more specific request requirements in acme. See
# https://github.com/pypa/pip/issues/988 for more info.
install_requires = [
'acme>=1.8.0',
# We specify the minimum acme version as the current Certbot version for
# simplicity. See https://github.com/certbot/certbot/issues/8761 for more
# info.
f'acme>={version}',
# We technically need ConfigArgParse 0.10.0 for Python 2.6 support, but
# saying so here causes a runtime error against our temporary fork of 0.9.3
# in which we added 2.6 support (see #2243), so we relax the requirement.

View File

@@ -17,6 +17,7 @@ from certbot import achallenges
from certbot import errors
from certbot import interfaces
from certbot import util
from certbot.plugins import common as plugin_common
from certbot.tests import acme_util
from certbot.tests import util as test_util
@@ -68,10 +69,9 @@ class HandleAuthorizationsTest(unittest.TestCase):
from certbot._internal.auth_handler import AuthHandler
self.mock_display = mock.Mock()
self.mock_config = mock.Mock(debug_challenges=False)
zope.component.provideUtility(
self.mock_display, interfaces.IDisplay)
zope.component.provideUtility(
mock.Mock(debug_challenges=False), interfaces.IConfig)
self.mock_auth = mock.MagicMock(name="ApacheConfigurator")
@@ -98,7 +98,7 @@ class HandleAuthorizationsTest(unittest.TestCase):
self.mock_net.poll.side_effect = _gen_mock_on_poll(retry=1, wait_value=30)
with mock.patch('certbot._internal.auth_handler.time') as mock_time:
authzr = self.handler.handle_authorizations(mock_order)
authzr = self.handler.handle_authorizations(mock_order, self.mock_config)
self.assertEqual(self.mock_net.answer_challenge.call_count, 1)
@@ -130,7 +130,7 @@ class HandleAuthorizationsTest(unittest.TestCase):
authzr = gen_dom_authzr(domain="0", challs=acme_util.CHALLENGES, combos=False)
mock_order = mock.MagicMock(authorizations=[authzr])
authzr = self.handler.handle_authorizations(mock_order)
authzr = self.handler.handle_authorizations(mock_order, self.mock_config)
self.assertEqual(self.mock_net.answer_challenge.call_count, 2)
@@ -151,7 +151,7 @@ class HandleAuthorizationsTest(unittest.TestCase):
authzr = gen_dom_authzr(domain="0", challs=acme_util.CHALLENGES, combos=False)
mock_order = mock.MagicMock(authorizations=[authzr])
authzr = self.handler.handle_authorizations(mock_order)
authzr = self.handler.handle_authorizations(mock_order, self.mock_config)
self.assertEqual(self.mock_net.answer_challenge.call_count, 1)
@@ -175,7 +175,7 @@ class HandleAuthorizationsTest(unittest.TestCase):
mock_order = mock.MagicMock(authorizations=authzrs)
self.mock_net.poll.side_effect = _gen_mock_on_poll()
authzr = self.handler.handle_authorizations(mock_order)
authzr = self.handler.handle_authorizations(mock_order, self.mock_config)
self.assertEqual(self.mock_net.answer_challenge.call_count, 3)
@@ -194,14 +194,13 @@ class HandleAuthorizationsTest(unittest.TestCase):
self._test_name3_http_01_3_common(combos=False)
def test_debug_challenges(self):
zope.component.provideUtility(
mock.Mock(debug_challenges=True), interfaces.IConfig)
config = mock.Mock(debug_challenges=True)
authzrs = [gen_dom_authzr(domain="0", challs=acme_util.CHALLENGES)]
mock_order = mock.MagicMock(authorizations=authzrs)
self.mock_net.poll.side_effect = _gen_mock_on_poll()
self.handler.handle_authorizations(mock_order)
self.handler.handle_authorizations(mock_order, config)
self.assertEqual(self.mock_net.answer_challenge.call_count, 1)
self.assertEqual(self.mock_display.notification.call_count, 1)
@@ -213,7 +212,8 @@ class HandleAuthorizationsTest(unittest.TestCase):
self.mock_auth.perform.side_effect = errors.AuthorizationError
self.assertRaises(
errors.AuthorizationError, self.handler.handle_authorizations, mock_order)
errors.AuthorizationError, self.handler.handle_authorizations,
mock_order, self.mock_config)
def test_max_retries_exceeded(self):
authzrs = [gen_dom_authzr(domain="0", challs=acme_util.CHALLENGES)]
@@ -224,12 +224,13 @@ class HandleAuthorizationsTest(unittest.TestCase):
with self.assertRaises(errors.AuthorizationError) as error:
# We retry only once, so retries will be exhausted before STATUS_VALID is returned.
self.handler.handle_authorizations(mock_order, False, 1)
self.handler.handle_authorizations(mock_order, self.mock_config, False, 1)
self.assertIn('All authorizations were not finalized by the CA.', str(error.exception))
def test_no_domains(self):
mock_order = mock.MagicMock(authorizations=[])
self.assertRaises(errors.AuthorizationError, self.handler.handle_authorizations, mock_order)
self.assertRaises(errors.AuthorizationError, self.handler.handle_authorizations,
mock_order, self.mock_config)
def _test_preferred_challenge_choice_common(self, combos):
authzrs = [gen_dom_authzr(domain="0", challs=acme_util.CHALLENGES, combos=combos)]
@@ -241,7 +242,7 @@ class HandleAuthorizationsTest(unittest.TestCase):
challenges.DNS01.typ,))
self.mock_net.poll.side_effect = _gen_mock_on_poll()
self.handler.handle_authorizations(mock_order)
self.handler.handle_authorizations(mock_order, self.mock_config)
self.assertEqual(self.mock_auth.cleanup.call_count, 1)
self.assertEqual(
@@ -259,7 +260,8 @@ class HandleAuthorizationsTest(unittest.TestCase):
mock_order = mock.MagicMock(authorizations=authzrs)
self.handler.pref_challs.append(challenges.DNS01.typ)
self.assertRaises(
errors.AuthorizationError, self.handler.handle_authorizations, mock_order)
errors.AuthorizationError, self.handler.handle_authorizations,
mock_order, self.mock_config)
def test_preferred_challenges_not_supported_acme_1(self):
self._test_preferred_challenges_not_supported_common(combos=True)
@@ -272,14 +274,16 @@ class HandleAuthorizationsTest(unittest.TestCase):
authzrs = [gen_dom_authzr(domain="0", challs=[acme_util.DNS01])]
mock_order = mock.MagicMock(authorizations=authzrs)
self.assertRaises(
errors.AuthorizationError, self.handler.handle_authorizations, mock_order)
errors.AuthorizationError, self.handler.handle_authorizations,
mock_order, self.mock_config)
def test_perform_error(self):
self.mock_auth.perform.side_effect = errors.AuthorizationError
authzr = gen_dom_authzr(domain="0", challs=acme_util.CHALLENGES, combos=True)
mock_order = mock.MagicMock(authorizations=[authzr])
self.assertRaises(errors.AuthorizationError, self.handler.handle_authorizations, mock_order)
self.assertRaises(errors.AuthorizationError, self.handler.handle_authorizations,
mock_order, self.mock_config)
self.assertEqual(self.mock_auth.cleanup.call_count, 1)
self.assertEqual(
@@ -292,7 +296,8 @@ class HandleAuthorizationsTest(unittest.TestCase):
mock_order = mock.MagicMock(authorizations=authzrs)
self.assertRaises(
errors.AuthorizationError, self.handler.handle_authorizations, mock_order)
errors.AuthorizationError, self.handler.handle_authorizations,
mock_order, self.mock_config)
self.assertEqual(self.mock_auth.cleanup.call_count, 1)
self.assertEqual(
self.mock_auth.cleanup.call_args[0][0][0].typ, "http-01")
@@ -304,7 +309,7 @@ class HandleAuthorizationsTest(unittest.TestCase):
with test_util.patch_get_utility():
with self.assertRaises(errors.AuthorizationError) as error:
self.handler.handle_authorizations(mock_order, False)
self.handler.handle_authorizations(mock_order, self.mock_config, False)
self.assertIn('Some challenges have failed.', str(error.exception))
self.assertEqual(self.mock_auth.cleanup.call_count, 1)
self.assertEqual(
@@ -327,8 +332,9 @@ class HandleAuthorizationsTest(unittest.TestCase):
mock_order = mock.MagicMock(authorizations=authzrs)
with mock.patch('certbot._internal.auth_handler._report_failed_authzrs') as mock_report:
valid_authzr = self.handler.handle_authorizations(mock_order, True)
with mock.patch('certbot._internal.auth_handler.AuthHandler._report_failed_authzrs') \
as mock_report:
valid_authzr = self.handler.handle_authorizations(mock_order, self.mock_config, True)
# Because best_effort=True, we did not blow up. Instead ...
self.assertEqual(len(valid_authzr), 1) # ... the valid authzr has been processed
@@ -338,7 +344,7 @@ class HandleAuthorizationsTest(unittest.TestCase):
with test_util.patch_get_utility():
with self.assertRaises(errors.AuthorizationError) as error:
self.handler.handle_authorizations(mock_order, True)
self.handler.handle_authorizations(mock_order, self.mock_config, True)
# Despite best_effort=True, process will fail because no authzr is valid.
self.assertIn('All challenges have failed.', str(error.exception))
@@ -352,7 +358,8 @@ class HandleAuthorizationsTest(unittest.TestCase):
[messages.STATUS_PENDING], False)
mock_order = mock.MagicMock(authorizations=[authzr])
self.assertRaises(
errors.AuthorizationError, self.handler.handle_authorizations, mock_order)
errors.AuthorizationError, self.handler.handle_authorizations,
mock_order, self.mock_config)
# With a validated challenge that is not supported by the plugin, we
# expect the challenge to not be solved again and
@@ -362,7 +369,7 @@ class HandleAuthorizationsTest(unittest.TestCase):
[acme_util.DNS01],
[messages.STATUS_VALID], False)
mock_order = mock.MagicMock(authorizations=[authzr])
self.handler.handle_authorizations(mock_order)
self.handler.handle_authorizations(mock_order, self.mock_config)
def test_valid_authzrs_deactivated(self):
"""When we deactivate valid authzrs in an orderr, we expect them to become deactivated
@@ -474,10 +481,18 @@ class GenChallengePathTest(unittest.TestCase):
class ReportFailedAuthzrsTest(unittest.TestCase):
"""Tests for certbot._internal.auth_handler._report_failed_authzrs."""
"""Tests for certbot._internal.auth_handler.AuthHandler._report_failed_authzrs."""
# pylint: disable=protected-access
def setUp(self):
from certbot._internal.auth_handler import AuthHandler
self.mock_auth = mock.MagicMock(spec=plugin_common.Plugin, name="buzz")
self.mock_auth.name = "buzz"
self.mock_auth.auth_hint.return_value = "the buzz hint"
self.handler = AuthHandler(self.mock_auth, mock.MagicMock(), mock.MagicMock(), [])
kwargs = {
"chall": acme_util.HTTP01,
"uri": "uri",
@@ -504,21 +519,57 @@ class ReportFailedAuthzrsTest(unittest.TestCase):
self.authzr2.body.identifier.value = 'foo.bar'
self.authzr2.body.challenges = [http_01_diff]
@test_util.patch_get_utility()
def test_same_error_and_domain(self, mock_zope):
from certbot._internal import auth_handler
@mock.patch('certbot._internal.auth_handler.display_util.notify')
def test_same_error_and_domain(self, mock_notify):
self.handler._report_failed_authzrs([self.authzr1])
mock_notify.assert_called_with(
'\n'
'Certbot failed to authenticate some domains (authenticator: buzz). '
'The Certificate Authority reported these problems:\n'
' Domain: example.com\n'
' Type: tls\n'
' Detail: detail\n'
'\n'
' Domain: example.com\n'
' Type: tls\n'
' Detail: detail\n'
'\nHint: the buzz hint\n'
)
auth_handler._report_failed_authzrs([self.authzr1], 'key')
call_list = mock_zope().add_message.call_args_list
self.assertEqual(len(call_list), 1)
self.assertIn("Domain: example.com\nType: tls\nDetail: detail", call_list[0][0][0])
@mock.patch('certbot._internal.auth_handler.display_util.notify')
def test_different_errors_and_domains(self, mock_notify):
self.mock_auth.name = "quux"
self.mock_auth.auth_hint.return_value = "quuuuuux"
self.handler._report_failed_authzrs([self.authzr1, self.authzr2])
mock_notify.assert_called_with(
'\n'
'Certbot failed to authenticate some domains (authenticator: quux). '
'The Certificate Authority reported these problems:\n'
' Domain: foo.bar\n'
' Type: dnssec\n'
' Detail: detail\n'
'\n'
' Domain: example.com\n'
' Type: tls\n'
' Detail: detail\n'
'\n'
' Domain: example.com\n'
' Type: tls\n'
' Detail: detail\n'
'\nHint: quuuuuux\n'
)
@test_util.patch_get_utility()
def test_different_errors_and_domains(self, mock_zope):
from certbot._internal import auth_handler
@mock.patch('certbot._internal.auth_handler.display_util.notify')
def test_non_subclassed_authenticator(self, mock_notify):
"""If authenticator not derived from common.Plugin, we shouldn't call .auth_hint"""
from certbot._internal.auth_handler import AuthHandler
auth_handler._report_failed_authzrs([self.authzr1, self.authzr2], 'key')
self.assertEqual(mock_zope().add_message.call_count, 2)
self.mock_auth = mock.MagicMock(name="quuz")
self.mock_auth.name = "quuz"
self.mock_auth.auth_hint.side_effect = Exception
self.handler = AuthHandler(self.mock_auth, mock.MagicMock(), mock.MagicMock(), [])
self.handler._report_failed_authzrs([self.authzr1])
self.assertEqual(mock_notify.call_count, 1)
def gen_auth_resp(chall_list):

Some files were not shown because too many files have changed in this diff Show More