Merge branch 'master' into repin-oldest

This commit is contained in:
Brad Warren 2021-07-16 16:35:12 -07:00
commit 4c87fff29a
62 changed files with 377 additions and 4097 deletions

View file

@ -79,6 +79,9 @@ jobs:
TOXENV: integration-dns-rfc2136
docker-dev:
TOXENV: docker_dev
le-modification:
IMAGE_NAME: ubuntu-18.04
TOXENV: modification
macos-farmtest-apache2:
# We run one of these test farm tests on macOS to help ensure the
# tests continue to work on the platform.

View file

@ -56,11 +56,6 @@ jobs:
apache-compat:
IMAGE_NAME: ubuntu-18.04
TOXENV: apache_compat
# le-modification can be moved to the extended test suite once
# https://github.com/certbot/certbot/issues/8742 is resolved.
le-modification:
IMAGE_NAME: ubuntu-18.04
TOXENV: modification
apacheconftest:
IMAGE_NAME: ubuntu-18.04
PYTHON_VERSION: 3.6

View file

@ -314,6 +314,15 @@ class HTTP01Response(KeyAuthorizationChallengeResponse):
except requests.exceptions.RequestException as error:
logger.error("Unable to reach %s: %s", uri, error)
return False
# By default, http_response.text will try to guess the encoding to use
# when decoding the response to Python unicode strings. This guesswork
# is error prone. RFC 8555 specifies that HTTP-01 responses should be
# key authorizations with possible trailing whitespace. Since key
# authorizations must be composed entirely of the base64url alphabet
# plus ".", we tell requests that the response should be ASCII. See
# https://datatracker.ietf.org/doc/html/rfc8555#section-8.3 for more
# info.
http_response.encoding = "ascii"
logger.debug("Received %s: %s. Headers: %s", http_response,
http_response.text, http_response.headers)

View file

@ -14,6 +14,7 @@ from typing import List
from typing import Set
from typing import Text
from typing import Union
import warnings
import josepy as jose
import OpenSSL
@ -224,6 +225,9 @@ class ClientBase:
class Client(ClientBase):
"""ACME client for a v1 API.
.. deprecated:: 1.18.0
Use :class:`ClientV2` instead.
.. todo::
Clean up raised error types hierarchy, document, and handle (wrap)
instances of `.DeserializationError` raised in `from_json()`.
@ -246,6 +250,8 @@ class Client(ClientBase):
URI from which the resource will be downloaded.
"""
warnings.warn("acme.client.Client (ACMEv1) is deprecated, "
"use acme.client.ClientV2 instead.", PendingDeprecationWarning)
self.key = key
if net is None:
net = ClientNetwork(key, alg=alg, verify_ssl=verify_ssl)
@ -805,6 +811,9 @@ class BackwardsCompatibleClientV2:
"""ACME client wrapper that tends towards V2-style calls, but
supports V1 servers.
.. deprecated:: 1.18.0
Use :class:`ClientV2` instead.
.. note:: While this class handles the majority of the differences
between versions of the ACME protocol, if you need to support an
ACME server based on version 3 or older of the IETF ACME draft
@ -821,6 +830,8 @@ class BackwardsCompatibleClientV2:
"""
def __init__(self, net, key, server):
warnings.warn("acme.client.BackwardsCompatibleClientV2 is deprecated, use "
"acme.client.ClientV2 instead.", PendingDeprecationWarning)
directory = messages.Directory.from_json(net.get(server).json())
self.acme_version = self._acme_version_from_directory(directory)
self.client: Union[Client, ClientV2]

View file

@ -114,7 +114,7 @@ class Error(jose.JSONObjectWithFields, errors.Error):
:rtype: unicode
"""
code = str(self.typ).split(':')[-1]
code = str(self.typ).rsplit(':', maxsplit=1)[-1]
if code in ERROR_CODES:
return code
return None

View file

@ -8,6 +8,7 @@ import socket
import socketserver
import threading
from typing import List
from typing import Optional
from acme import challenges
from acme import crypto_util
@ -66,6 +67,9 @@ class BaseDualNetworkedServers:
self.threads: List[threading.Thread] = []
self.servers: List[socketserver.BaseServer] = []
# Preserve socket error for re-raising, if no servers can be started
last_socket_err: Optional[socket.error] = None
# Must try True first.
# Ubuntu, for example, will fail to bind to IPv4 if we've already bound
# to IPv6. But that's ok, since it will accept IPv4 connections on the IPv6
@ -82,7 +86,8 @@ class BaseDualNetworkedServers:
logger.debug(
"Successfully bound to %s:%s using %s", new_address[0],
new_address[1], "IPv6" if ip_version else "IPv4")
except socket.error:
except socket.error as e:
last_socket_err = e
if self.servers:
# Already bound using IPv6.
logger.debug(
@ -101,7 +106,10 @@ class BaseDualNetworkedServers:
# bind to the same port for both servers.
port = server.socket.getsockname()[1]
if not self.servers:
raise socket.error("Could not bind to IPv4 or IPv6.")
if last_socket_err:
raise last_socket_err
else: # pragma: no cover
raise socket.error("Could not bind to IPv4 or IPv6.")
def serve_forever(self):
"""Wraps socketserver.TCPServer.serve_forever"""

View file

@ -3,7 +3,7 @@ import sys
from setuptools import find_packages
from setuptools import setup
version = '1.17.0.dev0'
version = '1.18.0.dev0'
install_requires = [
'cryptography>=2.1.4',

View file

@ -190,12 +190,18 @@ class BaseDualNetworkedServersTest(unittest.TestCase):
@mock.patch("socket.socket.bind")
def test_fail_to_bind(self, mock_bind):
mock_bind.side_effect = socket.error
from errno import EADDRINUSE
from acme.standalone import BaseDualNetworkedServers
self.assertRaises(socket.error, BaseDualNetworkedServers,
BaseDualNetworkedServersTest.SingleProtocolServer,
('', 0),
socketserver.BaseRequestHandler)
mock_bind.side_effect = socket.error(EADDRINUSE, "Fake addr in use error")
with self.assertRaises(socket.error) as em:
BaseDualNetworkedServers(
BaseDualNetworkedServersTest.SingleProtocolServer,
('', 0), socketserver.BaseRequestHandler)
self.assertEqual(em.exception.errno, EADDRINUSE)
def test_ports_equal(self):
from acme.standalone import BaseDualNetworkedServers

View file

@ -10,6 +10,7 @@ from certbot_apache._internal import override_debian
from certbot_apache._internal import override_fedora
from certbot_apache._internal import override_gentoo
from certbot_apache._internal import override_suse
from certbot_apache._internal import override_void
OVERRIDE_CLASSES = {
"arch": override_arch.ArchConfigurator,
@ -35,6 +36,7 @@ OVERRIDE_CLASSES = {
"sles": override_suse.OpenSUSEConfigurator,
"scientific": override_centos.CentOSConfigurator,
"scientific linux": override_centos.CentOSConfigurator,
"void": override_void.VoidConfigurator,
}

View file

@ -95,10 +95,10 @@ class ApacheHttp01(common.ChallengePerformer):
def _mod_config(self):
selected_vhosts: List[VirtualHost] = []
http_port = str(self.configurator.config.http01_port)
# Search for VirtualHosts matching by name
for chall in self.achalls:
# Search for matching VirtualHosts
for vh in self._matching_vhosts(chall.domain):
selected_vhosts.append(vh)
selected_vhosts += self._matching_vhosts(chall.domain)
# Ensure that we have one or more VirtualHosts that we can continue
# with. (one that listens to port configured with --http-01-port)
@ -107,9 +107,13 @@ class ApacheHttp01(common.ChallengePerformer):
if any(a.is_wildcard() or a.get_port() == http_port for a in vhost.addrs):
found = True
if not found:
for vh in self._relevant_vhosts():
selected_vhosts.append(vh)
# If there's at least one elgible VirtualHost, also add all unnamed VirtualHosts
# because they might match at runtime (#8890)
if found:
selected_vhosts += self._unnamed_vhosts()
# Otherwise, add every Virtualhost which listens on the right port
else:
selected_vhosts += self._relevant_vhosts()
# Add the challenge configuration
for vh in selected_vhosts:
@ -167,6 +171,10 @@ class ApacheHttp01(common.ChallengePerformer):
return relevant_vhosts
def _unnamed_vhosts(self) -> List[VirtualHost]:
"""Return all VirtualHost objects with no ServerName"""
return [vh for vh in self.configurator.vhosts if vh.name is None]
def _set_up_challenges(self):
if not os.path.isdir(self.challenge_dir):
old_umask = filesystem.umask(0o022)

View file

@ -177,8 +177,8 @@ class CentOSParser(parser.ApacheParser):
def parse_sysconfig_var(self):
""" Parses Apache CLI options from CentOS configuration file """
defines = apache_util.parse_define_file(self.sysconfig_filep, "OPTIONS")
for k in defines:
self.variables[k] = defines[k]
for k, v in defines.items():
self.variables[k] = v
def not_modssl_ifmodule(self, path):
"""Checks if the provided Augeas path has argument !mod_ssl"""

View file

@ -87,5 +87,5 @@ class FedoraParser(parser.ApacheParser):
def _parse_sysconfig_var(self):
""" Parses Apache CLI options from Fedora configuration file """
defines = apache_util.parse_define_file(self.sysconfig_filep, "OPTIONS")
for k in defines:
self.variables[k] = defines[k]
for k, v in defines.items():
self.variables[k] = v

View file

@ -53,8 +53,8 @@ class GentooParser(parser.ApacheParser):
""" Parses Apache CLI options from Gentoo configuration file """
defines = apache_util.parse_define_file(self.apacheconfig_filep,
"APACHE2_OPTS")
for k in defines:
self.variables[k] = defines[k]
for k, v in defines.items():
self.variables[k] = v
def update_modules(self):
"""Get loaded modules from httpd process, and add them to DOM"""

View file

@ -0,0 +1,23 @@
""" Distribution specific override class for Void Linux """
import zope.interface
from certbot import interfaces
from certbot_apache._internal import configurator
from certbot_apache._internal.configurator import OsOptions
@zope.interface.provider(interfaces.IPluginFactory)
class VoidConfigurator(configurator.ApacheConfigurator):
"""Void Linux specific ApacheConfigurator override class"""
OS_DEFAULTS = OsOptions(
server_root="/etc/apache",
vhost_root="/etc/apache/extra",
vhost_files="*.conf",
logs_root="/var/log/httpd",
ctl="apachectl",
version_cmd=['apachectl', '-v'],
restart_cmd=['apachectl', 'graceful'],
conftest_cmd=['apachectl', 'configtest'],
challenge_location="/etc/apache/extra",
)

View file

@ -440,7 +440,11 @@ class ApacheParser:
:type args: list or str
"""
first_dir = aug_conf_path + "/directive[1]"
self.aug.insert(first_dir, "directive", True)
if self.aug.get(first_dir):
self.aug.insert(first_dir, "directive", True)
else:
self.aug.set(first_dir, "directive")
self.aug.set(first_dir, dirname)
if isinstance(args, list):
for i, value in enumerate(args, 1):

View file

@ -1,7 +1,7 @@
from setuptools import find_packages
from setuptools import setup
version = '1.17.0.dev0'
version = '1.18.0.dev0'
install_requires = [
# We specify the minimum acme and certbot version as the current plugin

View file

@ -125,6 +125,18 @@ class ApacheHttp01Test(util.ApacheTest):
domain="duplicate.example.com", account_key=self.account_key)]
self.common_perform_test(achalls, vhosts)
def test_configure_name_and_blank(self):
domain = "certbot.demo"
vhosts = [v for v in self.config.vhosts if v.name == domain or v.name is None]
achalls = [
achallenges.KeyAuthorizationAnnotatedChallenge(
challb=acme_util.chall_to_challb(
challenges.HTTP01(token=((b'a' * 16))),
"pending"),
domain=domain, account_key=self.account_key),
]
self.common_perform_test(achalls, vhosts)
def test_no_vhost(self):
for achall in self.achalls:
self.http.add_chall(achall)

View file

@ -105,6 +105,11 @@ class BasicParserTest(util.ParserTest):
for i, match in enumerate(matches):
self.assertEqual(self.parser.aug.get(match), str(i + 1))
for name in ("empty.conf", "no-directives.conf"):
conf = "/files" + os.path.join(self.parser.root, "sites-available", name)
self.parser.add_dir_beginning(conf, "AddDirectiveBeginning", "testBegin")
self.assertTrue(self.parser.find_dir("AddDirectiveBeginning", "testBegin", conf))
def test_empty_arg(self):
self.assertEqual(None,
self.parser.get_arg("/files/whatever/nonexistent"))

View file

@ -0,0 +1,5 @@
<VirtualHost *:80>
<Location />
Require all denied
</Location>
</VirtualHost>

File diff suppressed because it is too large Load diff

View file

@ -346,7 +346,8 @@ def test_renew_empty_hook_scripts(context):
for hook_dir in misc.list_renewal_hooks_dirs(context.config_dir):
shutil.rmtree(hook_dir)
os.makedirs(join(hook_dir, 'dir'))
open(join(hook_dir, 'file'), 'w').close()
with open(join(hook_dir, 'file'), 'w'):
pass
context.certbot(['renew'])
assert_cert_count_for_lineage(context.config_dir, certname, 2)
@ -368,7 +369,8 @@ def test_renew_hook_override(context):
assert_hook_execution(context.hook_probe, 'deploy')
# Now we override all previous hooks during next renew.
open(context.hook_probe, 'w').close()
with open(context.hook_probe, 'w'):
pass
context.certbot([
'renew', '--cert-name', certname,
'--pre-hook', misc.echo('pre_override', context.hook_probe),
@ -387,7 +389,8 @@ def test_renew_hook_override(context):
assert_hook_execution(context.hook_probe, 'deploy')
# Expect that this renew will reuse new hooks registered in the previous renew.
open(context.hook_probe, 'w').close()
with open(context.hook_probe, 'w'):
pass
context.certbot(['renew', '--cert-name', certname])
assert_hook_execution(context.hook_probe, 'pre_override')

View file

@ -52,7 +52,7 @@ class ACMEServer:
self._proxy = http_proxy
self._workspace = tempfile.mkdtemp()
self._processes: List[subprocess.Popen] = []
self._stdout = sys.stdout if stdout else open(os.devnull, 'w')
self._stdout = sys.stdout if stdout else open(os.devnull, 'w') # pylint: disable=consider-using-with
self._dns_server = dns_server
self._http_01_port = http_01_port
if http_01_port != DEFAULT_HTTP_01_PORT:

View file

@ -45,6 +45,7 @@ class DNSServer:
# Unfortunately the BIND9 image forces everything to stderr with -g and we can't
# modify the verbosity.
# pylint: disable=consider-using-with
self._output = sys.stderr if show_output else open(os.devnull, "w")
def start(self):

View file

@ -79,11 +79,12 @@ def _get_names(config):
def _get_server_names(root, filename):
"""Returns all names in a config file path"""
all_names = set()
for line in open(os.path.join(root, filename)):
if line.strip().startswith("server_name"):
names = line.partition("server_name")[2].rpartition(";")[0]
for n in names.split():
# Filter out wildcards in both all_names and test_names
if not n.startswith("*."):
all_names.add(n)
with open(os.path.join(root, filename)) as f:
for line in f:
if line.strip().startswith("server_name"):
names = line.partition("server_name")[2].rpartition(";")[0]
for n in names.split():
# Filter out wildcards in both all_names and test_names
if not n.startswith("*."):
all_names.add(n)
return all_names

View file

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

View file

@ -4,7 +4,7 @@ import sys
from setuptools import find_packages
from setuptools import setup
version = '1.17.0.dev0'
version = '1.18.0.dev0'
install_requires = [
'cloudflare>=1.5.1',

View file

@ -4,7 +4,7 @@ import sys
from setuptools import find_packages
from setuptools import setup
version = '1.17.0.dev0'
version = '1.18.0.dev0'
install_requires = [
'dns-lexicon>=3.2.1',

View file

@ -4,7 +4,7 @@ import sys
from setuptools import find_packages
from setuptools import setup
version = '1.17.0.dev0'
version = '1.18.0.dev0'
install_requires = [
'python-digitalocean>=1.11', # 1.15.0 or newer is recommended for TTL support

View file

@ -4,7 +4,7 @@ import sys
from setuptools import find_packages
from setuptools import setup
version = '1.17.0.dev0'
version = '1.18.0.dev0'
install_requires = [
# This version of lexicon is required to address the problem described in

View file

@ -4,7 +4,7 @@ import sys
from setuptools import find_packages
from setuptools import setup
version = '1.17.0.dev0'
version = '1.18.0.dev0'
install_requires = [
'dns-lexicon>=3.2.1',

View file

@ -4,7 +4,7 @@ import sys
from setuptools import find_packages
from setuptools import setup
version = '1.17.0.dev0'
version = '1.18.0.dev0'
install_requires = [
'dns-lexicon>=3.2.1',

View file

@ -4,7 +4,7 @@ import sys
from setuptools import find_packages
from setuptools import setup
version = '1.17.0.dev0'
version = '1.18.0.dev0'
install_requires = [
'google-api-python-client>=1.5.5',

View file

@ -4,7 +4,7 @@ import sys
from setuptools import find_packages
from setuptools import setup
version = '1.17.0.dev0'
version = '1.18.0.dev0'
install_requires = [
'dns-lexicon>=3.2.1',

View file

@ -4,7 +4,7 @@ import sys
from setuptools import find_packages
from setuptools import setup
version = '1.17.0.dev0'
version = '1.18.0.dev0'
install_requires = [
'dns-lexicon>=3.2.1',

View file

@ -4,7 +4,7 @@ import sys
from setuptools import find_packages
from setuptools import setup
version = '1.17.0.dev0'
version = '1.18.0.dev0'
install_requires = [
'dns-lexicon>=3.2.1',

View file

@ -4,7 +4,7 @@ import sys
from setuptools import find_packages
from setuptools import setup
version = '1.17.0.dev0'
version = '1.18.0.dev0'
install_requires = [
'dns-lexicon>=3.2.1',

View file

@ -4,7 +4,7 @@ import sys
from setuptools import find_packages
from setuptools import setup
version = '1.17.0.dev0'
version = '1.18.0.dev0'
install_requires = [
'dnspython',

View file

@ -4,7 +4,7 @@ import sys
from setuptools import find_packages
from setuptools import setup
version = '1.17.0.dev0'
version = '1.18.0.dev0'
install_requires = [
'boto3',

View file

@ -4,7 +4,7 @@ import sys
from setuptools import find_packages
from setuptools import setup
version = '1.17.0.dev0'
version = '1.18.0.dev0'
install_requires = [
'dns-lexicon>=3.2.1',

View file

@ -96,8 +96,8 @@ class NginxParser:
servers = self._get_raw_servers()
addr_to_ssl: Dict[Tuple[str, str], bool] = {}
for filename in servers:
for server, _ in servers[filename]:
for server_list in servers.values():
for server, _ in server_list:
# Parse the server block to save addr info
parsed_server = _parse_server_raw(server)
for addr in parsed_server['addrs']:
@ -112,8 +112,7 @@ class NginxParser:
"""Get a map of unparsed all server blocks
"""
servers: Dict[str, Union[List, nginxparser.UnspacedList]] = {}
for filename in self.parsed:
tree = self.parsed[filename]
for filename, tree in self.parsed.items():
servers[filename] = []
srv = servers[filename] # workaround undefined loop var in lambdas
@ -141,8 +140,8 @@ class NginxParser:
servers = self._get_raw_servers()
vhosts = []
for filename in servers:
for server, path in servers[filename]:
for filename, server_list in servers.items():
for server, path in server_list:
# Parse the server block into a VirtualHost object
parsed_server = _parse_server_raw(server)
@ -240,8 +239,7 @@ class NginxParser:
"""
# Best-effort atomicity is enforced above us by reverter.py
for filename in self.parsed:
tree = self.parsed[filename]
for filename, tree in self.parsed.items():
if ext:
filename = filename + os.path.extsep + ext
if not isinstance(tree, UnspacedList):

View file

@ -1,7 +1,7 @@
from setuptools import find_packages
from setuptools import setup
version = '1.17.0.dev0'
version = '1.18.0.dev0'
install_requires = [
# We specify the minimum acme and certbot version as the current plugin

View file

@ -2,7 +2,7 @@
Certbot adheres to [Semantic Versioning](https://semver.org/).
## 1.17.0 - master
## 1.18.0 - master
### Added
@ -10,12 +10,38 @@ Certbot adheres to [Semantic Versioning](https://semver.org/).
### Changed
* When self-validating HTTP-01 challenges using
acme.challenges.HTTP01Response.simple_verify, we now assume that the response
is composed of only ASCII characters. Previously we were relying on the
default behavior of the requests library which tries to guess the encoding of
the response which was error prone.
* `acme`: the `.client.Client` and `.client.BackwardsCompatibleClientV2` classes
are now deprecated in favor of `.client.ClientV2`.
### Fixed
* The Apache authenticator no longer crashes with "Unable to insert label"
when encountering a completely empty vhost. This issue affected Certbot 1.17.0.
More details about these changes can be found on our GitHub repo.
## 1.17.0 - 2021-07-06
### Added
* Add Void Linux overrides for certbot-apache.
### 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.
* The Apache authenticator now always configures virtual hosts which do not have
an explicit `ServerName`. This should make it work more reliably with the
default Apache configuration in Debian-based environments.
### Fixed

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.17.0.dev0'
__version__ = '1.18.0.dev0'

View file

@ -311,8 +311,8 @@ class AccountFileStorage(interfaces.AccountStorage):
# does an appropriate directory link to me? if so, make sure that's gone
reused_servers = {}
for k in constants.LE_REUSE_SERVERS:
reused_servers[constants.LE_REUSE_SERVERS[k]] = k
for k, v in constants.LE_REUSE_SERVERS.items():
reused_servers[v] = k
# is there a next one up?
possible_next_link = True

View file

@ -40,9 +40,9 @@ from certbot._internal.cli.plugins_parsing import _plugins_parsing
from certbot._internal.cli.subparsers import _create_subparsers
from certbot._internal.cli.verb_help import VERB_HELP
from certbot._internal.cli.verb_help import VERB_HELP_MAP
from certbot.plugins import enhancements
from certbot._internal.plugins import disco as plugins_disco
import certbot._internal.plugins.selection as plugin_selection
import certbot.plugins.enhancements as enhancements
logger = logging.getLogger(__name__)

View file

@ -3,6 +3,7 @@ import datetime
import logging
import platform
from typing import List, Optional, Union
import warnings
from cryptography.hazmat.backends import default_backend
# See https://github.com/pyca/cryptography/issues/4275
@ -32,13 +33,23 @@ from certbot.display import util as display_util
logger = logging.getLogger(__name__)
def acme_from_config_key(config, key, regr=None):
"Wrangle ACME client construction"
# TODO: Allow for other alg types besides RS256
net = acme_client.ClientNetwork(key, account=regr, verify_ssl=(not config.no_verify_ssl),
user_agent=determine_user_agent(config))
return acme_client.BackwardsCompatibleClientV2(net, key, config.server)
with warnings.catch_warnings():
# TODO: full removal of ACMEv1 support: https://github.com/certbot/certbot/issues/6844
warnings.simplefilter("ignore", PendingDeprecationWarning)
client = acme_client.BackwardsCompatibleClientV2(net, key, config.server)
if client.acme_version == 1:
logger.warning(
"Certbot is configured to use an ACMEv1 server (%s). ACMEv1 support is deprecated"
" and will soon be removed. See https://community.letsencrypt.org/t/143839 for "
"more information.", config.server)
return client
def determine_user_agent(config):

View file

@ -139,8 +139,8 @@ class ErrorHandler:
def _reset_signal_handlers(self):
"""Resets signal handlers for signals in _SIGNALS."""
for signum in self.prev_handlers:
signal.signal(signum, self.prev_handlers[signum])
for signum, handler in self.prev_handlers.items():
signal.signal(signum, handler)
self.prev_handlers.clear()
def _signal_handler(self, signum, unused_frame):

View file

@ -507,6 +507,13 @@ def _report_next_steps(config: interfaces.IConfig, installer_err: Optional[error
"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 _is_interactive_only_auth(config):
steps.append(
"This certificate will not be renewed automatically. Autorenewal of "
"--manual certificates requires the use of an authentication hook script "
"(--manual-auth-hook) but one was not provided. To renew this certificate, repeat "
f"this same {cli.cli_command} command before the certificate's expiry date."
)
elif not config.preconfigured_renewal:
steps.append(
"The certificate will need to be renewed before it expires. Certbot can "
@ -556,6 +563,11 @@ def _report_new_cert(config, cert_path, fullchain_path, key_path=None):
assert cert_path and fullchain_path, "No certificates saved to report."
renewal_msg = ""
if config.preconfigured_renewal and not _is_interactive_only_auth(config):
renewal_msg = ("\nCertbot has set up a scheduled task to automatically renew this "
"certificate in the background.")
display_util.notify(
("\nSuccessfully received certificate.\n"
"Certificate is saved at: {cert_path}\n{key_msg}"
@ -564,13 +576,22 @@ def _report_new_cert(config, cert_path, fullchain_path, key_path=None):
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 "",
renewal_msg=renewal_msg,
nl="\n" if config.verb == "run" else "" # Normalize spacing across verbs
)
)
def _is_interactive_only_auth(config: interfaces.IConfig) -> bool:
""" Whether the current authenticator params only support interactive renewal.
"""
# --manual without --manual-auth-hook can never autorenew
if config.authenticator == "manual" and config.manual_auth_hook is None:
return True
return False
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.
@ -1398,7 +1419,7 @@ def certonly(config, plugins):
if config.csr:
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)
_report_next_steps(config, None, None, new_or_renewed_cert=not config.dry_run)
_suggest_donation_if_appropriate(config)
eff.handle_subscription(config, le_client.account)
return
@ -1417,7 +1438,8 @@ 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)
_report_next_steps(config, None, lineage,
new_or_renewed_cert=should_get_cert and not config.dry_run)
_suggest_donation_if_appropriate(config)
eff.handle_subscription(config, le_client.account)

View file

@ -5,6 +5,7 @@ import logging
import socket
from typing import DefaultDict
from typing import Dict
from typing import List
from typing import Set
from typing import Tuple
from typing import TYPE_CHECKING
@ -184,6 +185,14 @@ class Authenticator(common.Plugin):
if not self.served[servers]:
self.servers.stop(port)
def auth_hint(self, failed_achalls: List[achallenges.AnnotatedChallenge]) -> str:
port, addr = self.config.http01_port, self.config.http01_address
neat_addr = f"{addr}:{port}" if addr else f"port {port}"
return ("The Certificate Authority failed to download the challenge files from "
f"the temporary standalone webserver started by Certbot on {neat_addr}. "
"Ensure that the listed domains point to this machine and that it can "
"accept inbound connections from the internet.")
def _handle_perform_error(error):
if error.socket_error.errno == errno.EACCES:

View file

@ -144,7 +144,8 @@ def write_renewal_config(o_filename, n_filename, archive_dir, target, relevant_d
logger.debug("Writing new config %s.", n_filename)
# Ensure that the file exists
open(n_filename, 'a').close()
with open(n_filename, 'a'):
pass
# Copy permissions from the old version of the file, if it exists.
if os.path.exists(o_filename):

View file

@ -4,7 +4,7 @@ import logging
from certbot import errors
from certbot import interfaces
from certbot._internal.plugins import selection as plug_sel
import certbot.plugins.enhancements as enhancements
from certbot.plugins import enhancements
logger = logging.getLogger(__name__)

View file

@ -217,9 +217,9 @@ def _choose_names_manually(prompt_prefix=""):
retry_message = (
"One or more of the entered domain names was not valid:"
"{0}{0}").format(os.linesep)
for domain in invalid_domains:
for invalid_domain, err in invalid_domains.items():
retry_message = retry_message + "{1}: {2}{0}".format(
os.linesep, domain, invalid_domains[domain])
os.linesep, invalid_domain, err)
retry_message = retry_message + (
"{0}Would you like to re-enter the names?{0}").format(
os.linesep)

View file

@ -549,7 +549,8 @@ class IReporter(zope.interface.Interface):
class RenewableCert(object, metaclass=abc.ABCMeta):
"""Interface to a certificate lineage."""
@abc.abstractproperty
@property
@abc.abstractmethod
def cert_path(self):
"""Path to the certificate file.
@ -557,7 +558,8 @@ class RenewableCert(object, metaclass=abc.ABCMeta):
"""
@abc.abstractproperty
@property
@abc.abstractmethod
def key_path(self):
"""Path to the private key file.
@ -565,7 +567,8 @@ class RenewableCert(object, metaclass=abc.ABCMeta):
"""
@abc.abstractproperty
@property
@abc.abstractmethod
def chain_path(self):
"""Path to the certificate chain file.
@ -573,7 +576,8 @@ class RenewableCert(object, metaclass=abc.ABCMeta):
"""
@abc.abstractproperty
@property
@abc.abstractmethod
def fullchain_path(self):
"""Path to the full chain file.
@ -583,7 +587,8 @@ class RenewableCert(object, metaclass=abc.ABCMeta):
"""
@abc.abstractproperty
@property
@abc.abstractmethod
def lineagename(self):
"""Name given to the certificate lineage.

View file

@ -169,7 +169,7 @@ class DNSAuthenticator(common.Plugin):
indicate any issue.
"""
def __validator(filename):
def __validator(filename): # pylint: disable=unused-private-member
configuration = CredentialsConfiguration(filename, self.dest)
if required_variables:
@ -199,7 +199,7 @@ class DNSAuthenticator(common.Plugin):
:rtype: str
"""
def __validator(i):
def __validator(i): # pylint: disable=unused-private-member
if not i:
raise errors.PluginError('Please enter your {0}.'.format(label))
@ -225,7 +225,7 @@ class DNSAuthenticator(common.Plugin):
:rtype: str
"""
def __validator(filename):
def __validator(filename): # pylint: disable=unused-private-member
if not filename:
raise errors.PluginError('Please enter a valid path to your {0}.'.format(label))

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:
-3)
0)
--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.16.0 (certbot;
"". (default: CertbotACMEClient/1.17.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

@ -57,10 +57,11 @@ standalone_ Y N | Uses a "standalone" webserver to obtain a certificate.
| domain. Doing domain validation in this way is
| the only way to obtain wildcard certificates from Let's
| Encrypt.
manual_ Y N | Helps you obtain a certificate by giving you instructions to http-01_ (80) or
| perform domain validation yourself. Additionally allows you dns-01_ (53)
| to specify scripts to automate the validation task in a
| customized way.
manual_ Y N | Obtain a certificate by manually following instructions to http-01_ (80) or
| perform domain validation yourself. Certificates created this dns-01_ (53)
| way do not support autorenewal.
| Autorenewal may be enabled by providing an authentication
| hook script to automate the domain validation steps.
=========== ==== ==== =============================================================== =============================
.. |dns_plugs| replace:: :ref:`DNS plugins <dns_plugins>`
@ -229,11 +230,21 @@ For example, for the domain ``example.com``, a zone file entry would look like:
_acme-challenge.example.com. 300 IN TXT "gfj9Xq...Rg85nM"
.. _manual-renewal:
Additionally you can specify scripts to prepare for validation and
perform the authentication procedure and/or clean up after it by using
the ``--manual-auth-hook`` and ``--manual-cleanup-hook`` flags. This is
described in more depth in the hooks_ section.
**Renewal with the manual plugin**
Certificates created using ``--manual`` **do not** support automatic renewal unless
combined with an `authentication hook script <#hooks>`_ via ``--manual-auth-hook``
to automatically set up the required HTTP and/or TXT challenges.
If you can use one of the other plugins_ which support autorenewal to create
your certificate, doing so is highly recommended.
To manually renew a certificate using ``--manual`` without hooks, repeat the same
``certbot --manual`` command you used to create the certificate originally. As this
will require you to copy and paste new HTTP files or DNS TXT records, the command
cannot be automated with a cron job.
.. _combination:
@ -286,6 +297,10 @@ 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
njalla_ Y N DNS Authentication for njalla
DuckDNS_ Y N DNS Authentication for DuckDNS
Porkbun_ Y N DNS Authentication for Porkbun
Infomaniak_ Y N DNS Authentication using Infomaniak Domains API
================== ==== ==== ===============================================================
.. _haproxy: https://github.com/greenhost/certbot-haproxy
@ -302,6 +317,10 @@ dns-godaddy_ Y N DNS Authentication using Godaddy DNS
.. _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
.. _njalla: https://github.com/chaptergy/certbot-dns-njalla
.. _DuckDNS: https://github.com/infinityofspace/certbot_dns_duckdns
.. _Porkbun: https://github.com/infinityofspace/certbot_dns_porkbun
.. _Infomaniak: https://github.com/Infomaniak/certbot-dns-infomaniak
If you're interested, you can also :ref:`write your own plugin <dev-plugin>`.
@ -522,6 +541,10 @@ Renewing certificates
.. seealso:: Most Certbot installations come with automatic
renewal out of the box. See `Automated Renewals`_ for more details.
.. seealso:: Users of the `Manual`_ plugin should note that ``--manual`` certificates
will not renew automatically, unless combined with authentication hook scripts.
See `Renewal with the manual plugin <#manual-renewal>`_.
As of version 0.10.0, Certbot supports a ``renew`` action to check
all installed certificates for impending expiry and attempt to renew
them. The simplest form is simply
@ -710,7 +733,7 @@ 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
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.

View file

@ -271,6 +271,21 @@ class CertonlyTest(unittest.TestCase):
self._call(('certonly --webroot --cert-name example.com').split())
self.assertIs(mock_choose_names.called, True)
@mock.patch('certbot._internal.main._report_next_steps')
@mock.patch('certbot._internal.main._get_and_save_cert')
@mock.patch('certbot._internal.main._csr_get_and_save_cert')
@mock.patch('certbot._internal.cert_manager.lineage_for_certname')
def test_dryrun_next_steps_no_cert_saved(self, mock_lineage, mock_csr_get_cert,
unused_mock_get_cert, mock_report_next_steps):
"""certonly --dry-run shouldn't report creation of a certificate in NEXT STEPS."""
mock_lineage.return_value = None
mock_csr_get_cert.return_value = ("/cert", "/chain", "/fullchain")
for flag in (f"--csr {CSR}", "-d example.com"):
self._call(f"certonly {flag} --webroot --cert-name example.com --dry-run".split())
mock_report_next_steps.assert_called_once_with(
mock.ANY, mock.ANY, mock.ANY, new_or_renewed_cert=False)
mock_report_next_steps.reset_mock()
class FindDomainsOrCertnameTest(unittest.TestCase):
"""Tests for certbot._internal.main._find_domains_or_certname."""
@ -1886,6 +1901,71 @@ class ReportNewCertTest(unittest.TestCase):
'This certificate expires on 1970-01-01.'
)
def test_manual_no_hooks_report(self):
"""Shouldn't get a message about autorenewal if no --manual-auth-hook"""
self._call(mock.Mock(dry_run=False, authenticator='manual', manual_auth_hook=None),
'/path/to/cert.pem', '/path/to/fullchain.pem',
'/path/to/privkey.pem')
self.mock_notify.assert_called_with(
'\nSuccessfully received certificate.\n'
'Certificate is saved at: /path/to/fullchain.pem\n'
'Key is saved at: /path/to/privkey.pem\n'
'This certificate expires on 1970-01-01.\n'
'These files will be updated when the certificate renews.'
)
class ReportNextStepsTest(unittest.TestCase):
"""Tests for certbot._internal.main._report_next_steps"""
def setUp(self):
self.config = mock.MagicMock(
cert_name="example.com", preconfigured_renewal=True,
csr=None, authenticator="nginx", manual_auth_hook=None)
notify_patch = mock.patch('certbot._internal.main.display_util.notify')
self.mock_notify = notify_patch.start()
self.addCleanup(notify_patch.stop)
self.old_stdout = sys.stdout
sys.stdout = io.StringIO()
def tearDown(self):
sys.stdout = self.old_stdout
@classmethod
def _call(cls, *args, **kwargs):
from certbot._internal.main import _report_next_steps
_report_next_steps(*args, **kwargs)
def _output(self) -> str:
self.mock_notify.assert_called_once()
return self.mock_notify.call_args_list[0][0][0]
def test_report(self):
"""No steps for a normal renewal"""
self.config.authenticator = "manual"
self.config.manual_auth_hook = "/bin/true"
self._call(self.config, None, None)
self.mock_notify.assert_not_called()
def test_csr_report(self):
"""--csr requires manual renewal"""
self.config.csr = "foo.csr"
self._call(self.config, None, None)
self.assertIn("--csr will not be renewed", self._output())
def test_manual_no_hook_renewal(self):
"""--manual without a hook requires manual renewal"""
self.config.authenticator = "manual"
self._call(self.config, None, None)
self.assertIn("--manual certificates requires", self._output())
def test_no_preconfigured_renewal(self):
"""No --preconfigured-renewal needs manual cron setup"""
self.config.preconfigured_renewal = False
self._call(self.config, None, None)
self.assertIn("https://certbot.org/renewal-setup", self._output())
class UpdateAccountTest(test_util.ConfigTestCase):
"""Tests for certbot._internal.main.update_account"""

View file

@ -177,6 +177,13 @@ class AuthenticatorTest(unittest.TestCase):
"server1": set(), "server2": set()})
self.auth.servers.stop.assert_called_with(2)
def test_auth_hint(self):
self.config.http01_port = "80"
self.config.http01_address = None
self.assertIn("on port 80", self.auth.auth_hint([]))
self.config.http01_address = "127.0.0.1"
self.assertIn("on 127.0.0.1:80", self.auth.auth_hint([]))
if __name__ == "__main__":
unittest.main() # pragma: no cover

File diff suppressed because it is too large Load diff

View file

@ -1,11 +0,0 @@
-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEEos+1H6J1pyhiNOeyTRfJlc2XdfIFAmBsmUkACgkQTRfJlc2X
dfI7Bwf9FkNrf1HEh2G3uk1p+qLMd/s5kcVV2udK2FkRELee5nHlLZx2YmHA/8ID
gqsk8EsyRZNMX374nGrPm0syykdEsyVtMJTbHCEr+Ms3l54ZgE3HV6ywnhWSlAFo
Za50kdzhodBVTS5AEADbCKLKObVAWwO3fFKtKyv/iY29ykpHK0KSHCKRII3iQU7l
dnR6u35Z0wgfEmDxsH27K6uo0YepZaEL70qHHFk93MhCh9Z15rO17gRpsVzz7Z1j
YClI6h2K/VOfZtbkoQvoks7s+xd75Kjr3GNH+cznkJx8gNWSZLfkc1XX4Bjdm4GG
IWz3Ezy8tFg6PtITb7y+aIg75kWx4w==
=zEy4
-----END PGP SIGNATURE-----

View file

@ -10,11 +10,6 @@ import os
# taken from our v1.14.0 tag which was the last release we intended to make
# changes to certbot-auto.
#
# certbot-auto, letsencrypt-auto, and letsencrypt-auto-source/certbot-auto.asc
# can be removed from this dict after coordinating with tech ops to ensure we
# get the behavior we want from https://dl.eff.org. See
# https://github.com/certbot/certbot/issues/8742 for more info.
#
# Deleting letsencrypt-auto-source/letsencrypt-auto and
# letsencrypt-auto-source/letsencrypt-auto.sig can be done once we're
# comfortable breaking any certbot-auto scripts that haven't already updated to
@ -22,14 +17,8 @@ import os
# https://opensource.eff.org/eff-open-source/pl/65geri7c4tr6iqunc1rpb3mpna for
# more info.
EXPECTED_FILES = {
'certbot-auto':
'b997e3608526650a08e36e682fc3bf0c29903c06fa5ba4cc49308c43832450c2',
'letsencrypt-auto':
'b997e3608526650a08e36e682fc3bf0c29903c06fa5ba4cc49308c43832450c2',
os.path.join('letsencrypt-auto-source', 'letsencrypt-auto'):
'b997e3608526650a08e36e682fc3bf0c29903c06fa5ba4cc49308c43832450c2',
os.path.join('letsencrypt-auto-source', 'certbot-auto.asc'):
'0558ba7bd816732b38c092e8fedb6033dad01f263e290ec6b946263aaf6625a8',
os.path.join('letsencrypt-auto-source', 'letsencrypt-auto.sig'):
'61c036aabf75da350b0633da1b2bef0260303921ecda993455ea5e6d3af3b2fe',
}