diff --git a/.azure-pipelines/templates/jobs/extended-tests-jobs.yml b/.azure-pipelines/templates/jobs/extended-tests-jobs.yml
index c8cbe7fb37b..8d95ebdb645 100644
--- a/.azure-pipelines/templates/jobs/extended-tests-jobs.yml
+++ b/.azure-pipelines/templates/jobs/extended-tests-jobs.yml
@@ -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.
diff --git a/.azure-pipelines/templates/jobs/standard-tests-jobs.yml b/.azure-pipelines/templates/jobs/standard-tests-jobs.yml
index c949af44a9a..fda33a71d4b 100644
--- a/.azure-pipelines/templates/jobs/standard-tests-jobs.yml
+++ b/.azure-pipelines/templates/jobs/standard-tests-jobs.yml
@@ -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
diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py
index 2c8190be57b..69c2860728a 100644
--- a/acme/acme/challenges.py
+++ b/acme/acme/challenges.py
@@ -314,6 +314,15 @@ def simple_verify(self, chall, domain, account_public_key, port=None):
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)
diff --git a/acme/acme/client.py b/acme/acme/client.py
index 548c3d54877..28ed4f5bb13 100644
--- a/acme/acme/client.py
+++ b/acme/acme/client.py
@@ -14,6 +14,7 @@
from typing import Set
from typing import Text
from typing import Union
+import warnings
import josepy as jose
import OpenSSL
@@ -224,6 +225,9 @@ def _revoke(self, cert, rsn, url):
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 @@ def __init__(self, directory, key, alg=jose.RS256, verify_ssl=True,
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)
@@ -658,7 +664,10 @@ def new_order(self, csr_pem):
response = self._post(self.directory['newOrder'], order)
body = messages.Order.from_json(response.json())
authorizations = []
- for url in body.authorizations:
+ # pylint has trouble understanding our josepy based objects which use
+ # things like custom metaclass logic. body.authorizations should be a
+ # list of strings containing URLs so let's disable this check here.
+ for url in body.authorizations: # pylint: disable=not-an-iterable
authorizations.append(self._authzr_from_response(self._post_as_get(url), uri=url))
return messages.OrderResource(
body=body,
@@ -802,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
@@ -818,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]
diff --git a/acme/acme/messages.py b/acme/acme/messages.py
index 36207dba0f1..5c702ca44f0 100644
--- a/acme/acme/messages.py
+++ b/acme/acme/messages.py
@@ -114,7 +114,7 @@ def code(self):
: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
diff --git a/acme/acme/standalone.py b/acme/acme/standalone.py
index eda45304c43..e2672fb943f 100644
--- a/acme/acme/standalone.py
+++ b/acme/acme/standalone.py
@@ -8,6 +8,7 @@
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 @@ def __init__(self, ServerClass, server_address, *remaining_args, **kwargs):
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 @@ def __init__(self, ServerClass, server_address, *remaining_args, **kwargs):
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 @@ def __init__(self, ServerClass, server_address, *remaining_args, **kwargs):
# 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"""
diff --git a/acme/setup.py b/acme/setup.py
index 38e9208b6af..9ae7d5bd92d 100644
--- a/acme/setup.py
+++ b/acme/setup.py
@@ -3,7 +3,7 @@
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',
@@ -19,17 +19,16 @@
'setuptools>=39.0.1',
]
-dev_extras = [
- 'pytest',
- 'pytest-xdist',
- 'tox',
-]
-
docs_extras = [
'Sphinx>=1.0', # autodoc_member_order = 'bysource', autodoc_default_flags
'sphinx_rtd_theme',
]
+test_extras = [
+ 'pytest',
+ 'pytest-xdist',
+]
+
setup(
name='acme',
version=version,
@@ -57,7 +56,7 @@
include_package_data=True,
install_requires=install_requires,
extras_require={
- 'dev': dev_extras,
'docs': docs_extras,
+ 'test': test_extras,
},
)
diff --git a/acme/tests/standalone_test.py b/acme/tests/standalone_test.py
index 17d73dba876..e0aa5aa2248 100644
--- a/acme/tests/standalone_test.py
+++ b/acme/tests/standalone_test.py
@@ -190,12 +190,18 @@ def __init__(self, *args, **kwargs):
@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
diff --git a/certbot-apache/certbot_apache/_internal/configurator.py b/certbot-apache/certbot_apache/_internal/configurator.py
index 80dd8520be5..96334238acd 100644
--- a/certbot-apache/certbot_apache/_internal/configurator.py
+++ b/certbot-apache/certbot_apache/_internal/configurator.py
@@ -15,9 +15,6 @@
from typing import Set
from typing import Union
-import zope.component
-import zope.interface
-
from acme import challenges
from certbot import errors
from certbot import interfaces
@@ -120,10 +117,7 @@ def __init__(self,
# TODO: Add directives to sites-enabled... not sites-available.
# sites-available doesn't allow immediate find_dir search even with save()
# and load()
-
-@zope.interface.implementer(interfaces.IAuthenticator, interfaces.IInstaller)
-@zope.interface.provider(interfaces.IPluginFactory)
-class ApacheConfigurator(common.Installer):
+class ApacheConfigurator(common.Installer, interfaces.Authenticator):
"""Apache configurator.
:ivar config: Configuration.
@@ -884,7 +878,7 @@ def get_all_names(self):
all_names.add(name)
if vhost_macro:
- zope.component.getUtility(interfaces.IDisplay).notification(
+ display_util.notification(
"Apache mod_macro seems to be in use in file(s):\n{0}"
"\n\nUnfortunately mod_macro is not yet supported".format(
"\n ".join(vhost_macro)), force_interactive=True)
diff --git a/certbot-apache/certbot_apache/_internal/display_ops.py b/certbot-apache/certbot_apache/_internal/display_ops.py
index 875225eb998..86f69617333 100644
--- a/certbot-apache/certbot_apache/_internal/display_ops.py
+++ b/certbot-apache/certbot_apache/_internal/display_ops.py
@@ -1,12 +1,9 @@
"""Contains UI methods for Apache operations."""
import logging
-import zope.component
-
from certbot import errors
-from certbot import interfaces
from certbot.compat import os
-import certbot.display.util as display_util
+from certbot.display import util as display_util
logger = logging.getLogger(__name__)
@@ -26,7 +23,7 @@ def select_vhost_multiple(vhosts):
# Remove the extra newline from the last entry
if tags_list:
tags_list[-1] = tags_list[-1][:-1]
- code, names = zope.component.getUtility(interfaces.IDisplay).checklist(
+ code, names = display_util.checklist(
"Which VirtualHosts would you like to install the wildcard certificate for?",
tags=tags_list, force_interactive=True)
if code == display_util.OK:
@@ -34,6 +31,7 @@ def select_vhost_multiple(vhosts):
return return_vhosts
return []
+
def _reversemap_vhosts(names, vhosts):
"""Helper function for select_vhost_multiple for mapping string
representations back to actual vhost objects"""
@@ -45,6 +43,7 @@ def _reversemap_vhosts(names, vhosts):
return_vhosts.append(vhost)
return return_vhosts
+
def select_vhost(domain, vhosts):
"""Select an appropriate Apache Vhost.
@@ -62,6 +61,7 @@ def select_vhost(domain, vhosts):
return vhosts[tag]
return None
+
def _vhost_menu(domain, vhosts):
"""Select an appropriate Apache Vhost.
@@ -107,7 +107,7 @@ def _vhost_menu(domain, vhosts):
)
try:
- code, tag = zope.component.getUtility(interfaces.IDisplay).menu(
+ code, tag = display_util.menu(
"We were unable to find a vhost with a ServerName "
"or Address of {0}.{1}Which virtual host would you "
"like to choose?".format(domain, os.linesep),
diff --git a/certbot-apache/certbot_apache/_internal/entrypoint.py b/certbot-apache/certbot_apache/_internal/entrypoint.py
index 79337b381b1..96bef030c08 100644
--- a/certbot-apache/certbot_apache/_internal/entrypoint.py
+++ b/certbot-apache/certbot_apache/_internal/entrypoint.py
@@ -10,6 +10,7 @@
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 @@
"sles": override_suse.OpenSUSEConfigurator,
"scientific": override_centos.CentOSConfigurator,
"scientific linux": override_centos.CentOSConfigurator,
+ "void": override_void.VoidConfigurator,
}
diff --git a/certbot-apache/certbot_apache/_internal/http_01.py b/certbot-apache/certbot_apache/_internal/http_01.py
index 83a1a8e0825..872704db87c 100644
--- a/certbot-apache/certbot_apache/_internal/http_01.py
+++ b/certbot-apache/certbot_apache/_internal/http_01.py
@@ -95,10 +95,10 @@ def prepare_http01_modules(self):
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 @@ def _mod_config(self):
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 @@ def _relevant_vhosts(self):
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)
diff --git a/certbot-apache/certbot_apache/_internal/override_arch.py b/certbot-apache/certbot_apache/_internal/override_arch.py
index 30d161a4ed2..007f677b541 100644
--- a/certbot-apache/certbot_apache/_internal/override_arch.py
+++ b/certbot-apache/certbot_apache/_internal/override_arch.py
@@ -1,12 +1,8 @@
""" Distribution specific override class for Arch 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 ArchConfigurator(configurator.ApacheConfigurator):
"""Arch Linux specific ApacheConfigurator override class"""
diff --git a/certbot-apache/certbot_apache/_internal/override_centos.py b/certbot-apache/certbot_apache/_internal/override_centos.py
index c1a69885c63..431e8ec46ed 100644
--- a/certbot-apache/certbot_apache/_internal/override_centos.py
+++ b/certbot-apache/certbot_apache/_internal/override_centos.py
@@ -3,10 +3,7 @@
from typing import cast
from typing import List
-import zope.interface
-
from certbot import errors
-from certbot import interfaces
from certbot import util
from certbot.errors import MisconfigurationError
from certbot_apache._internal import apache_util
@@ -17,7 +14,6 @@
logger = logging.getLogger(__name__)
-@zope.interface.provider(interfaces.IPluginFactory)
class CentOSConfigurator(configurator.ApacheConfigurator):
"""CentOS specific ApacheConfigurator override class"""
@@ -177,8 +173,8 @@ def update_runtime_variables(self):
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"""
diff --git a/certbot-apache/certbot_apache/_internal/override_darwin.py b/certbot-apache/certbot_apache/_internal/override_darwin.py
index e1dca7f5ea9..4dfc8dc3824 100644
--- a/certbot-apache/certbot_apache/_internal/override_darwin.py
+++ b/certbot-apache/certbot_apache/_internal/override_darwin.py
@@ -1,12 +1,8 @@
""" Distribution specific override class for macOS """
-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 DarwinConfigurator(configurator.ApacheConfigurator):
"""macOS specific ApacheConfigurator override class"""
diff --git a/certbot-apache/certbot_apache/_internal/override_debian.py b/certbot-apache/certbot_apache/_internal/override_debian.py
index 14954f095a0..385d3d7a23e 100644
--- a/certbot-apache/certbot_apache/_internal/override_debian.py
+++ b/certbot-apache/certbot_apache/_internal/override_debian.py
@@ -1,10 +1,7 @@
""" Distribution specific override class for Debian family (Ubuntu/Debian) """
import logging
-import zope.interface
-
from certbot import errors
-from certbot import interfaces
from certbot import util
from certbot.compat import filesystem
from certbot.compat import os
@@ -15,7 +12,6 @@
logger = logging.getLogger(__name__)
-@zope.interface.provider(interfaces.IPluginFactory)
class DebianConfigurator(configurator.ApacheConfigurator):
"""Debian specific ApacheConfigurator override class"""
diff --git a/certbot-apache/certbot_apache/_internal/override_fedora.py b/certbot-apache/certbot_apache/_internal/override_fedora.py
index 3b947a8232e..cf0764d682c 100644
--- a/certbot-apache/certbot_apache/_internal/override_fedora.py
+++ b/certbot-apache/certbot_apache/_internal/override_fedora.py
@@ -1,8 +1,5 @@
""" Distribution specific override class for Fedora 29+ """
-import zope.interface
-
from certbot import errors
-from certbot import interfaces
from certbot import util
from certbot_apache._internal import apache_util
from certbot_apache._internal import configurator
@@ -10,7 +7,6 @@
from certbot_apache._internal.configurator import OsOptions
-@zope.interface.provider(interfaces.IPluginFactory)
class FedoraConfigurator(configurator.ApacheConfigurator):
"""Fedora 29+ specific ApacheConfigurator override class"""
@@ -87,5 +83,5 @@ def update_runtime_variables(self):
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
diff --git a/certbot-apache/certbot_apache/_internal/override_gentoo.py b/certbot-apache/certbot_apache/_internal/override_gentoo.py
index 1b86c925ed1..6e02c05e7b3 100644
--- a/certbot-apache/certbot_apache/_internal/override_gentoo.py
+++ b/certbot-apache/certbot_apache/_internal/override_gentoo.py
@@ -1,14 +1,10 @@
""" Distribution specific override class for Gentoo Linux """
-import zope.interface
-
-from certbot import interfaces
from certbot_apache._internal import apache_util
from certbot_apache._internal import configurator
from certbot_apache._internal import parser
from certbot_apache._internal.configurator import OsOptions
-@zope.interface.provider(interfaces.IPluginFactory)
class GentooConfigurator(configurator.ApacheConfigurator):
"""Gentoo specific ApacheConfigurator override class"""
@@ -53,8 +49,8 @@ def parse_sysconfig_var(self):
""" 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"""
diff --git a/certbot-apache/certbot_apache/_internal/override_suse.py b/certbot-apache/certbot_apache/_internal/override_suse.py
index d692fd239a6..f0bbb5b42e2 100644
--- a/certbot-apache/certbot_apache/_internal/override_suse.py
+++ b/certbot-apache/certbot_apache/_internal/override_suse.py
@@ -1,12 +1,8 @@
""" Distribution specific override class for OpenSUSE """
-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 OpenSUSEConfigurator(configurator.ApacheConfigurator):
"""OpenSUSE specific ApacheConfigurator override class"""
diff --git a/certbot-apache/certbot_apache/_internal/override_void.py b/certbot-apache/certbot_apache/_internal/override_void.py
new file mode 100644
index 00000000000..81187c437b3
--- /dev/null
+++ b/certbot-apache/certbot_apache/_internal/override_void.py
@@ -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",
+ )
diff --git a/certbot-apache/certbot_apache/_internal/parser.py b/certbot-apache/certbot_apache/_internal/parser.py
index 141991cccb6..3c705e066dd 100644
--- a/certbot-apache/certbot_apache/_internal/parser.py
+++ b/certbot-apache/certbot_apache/_internal/parser.py
@@ -440,7 +440,11 @@ def add_dir_beginning(self, aug_conf_path, dirname, args):
: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):
diff --git a/certbot-apache/setup.py b/certbot-apache/setup.py
index 3397671a6ab..fd03e6ca013 100644
--- a/certbot-apache/setup.py
+++ b/certbot-apache/setup.py
@@ -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
diff --git a/certbot-apache/tests/configurator_test.py b/certbot-apache/tests/configurator_test.py
index a5e471060c6..84f9e205369 100644
--- a/certbot-apache/tests/configurator_test.py
+++ b/certbot-apache/tests/configurator_test.py
@@ -136,7 +136,7 @@ def test_constant(self):
self.assertTrue("debian_apache_2_4/multiple_vhosts/apache" in
self.config.options.server_root)
- @certbot_util.patch_get_utility()
+ @certbot_util.patch_display_util()
def test_get_all_names(self, mock_getutility):
mock_utility = mock_getutility()
mock_utility.notification = mock.MagicMock(return_value=True)
@@ -145,7 +145,7 @@ def test_get_all_names(self, mock_getutility):
"nonsym.link", "vhost.in.rootconf", "www.certbot.demo",
"duplicate.example.com"})
- @certbot_util.patch_get_utility()
+ @certbot_util.patch_display_util()
@mock.patch("certbot_apache._internal.configurator.socket.gethostbyaddr")
def test_get_all_names_addrs(self, mock_gethost, mock_getutility):
mock_gethost.side_effect = [("google.com", "", ""), socket.error]
diff --git a/certbot-apache/tests/debian_test.py b/certbot-apache/tests/debian_test.py
index b5a486ff1a4..35425223bda 100644
--- a/certbot-apache/tests/debian_test.py
+++ b/certbot-apache/tests/debian_test.py
@@ -9,6 +9,7 @@
from certbot import errors
from certbot.compat import os
+from certbot.tests import util as certbot_util
from certbot_apache._internal import apache_util
from certbot_apache._internal import obj
import util
@@ -68,17 +69,18 @@ def test_deploy_cert_enable_new_vhost(self):
self.config.parser.modules["ssl_module"] = None
self.config.parser.modules["mod_ssl.c"] = None
self.assertFalse(ssl_vhost.enabled)
- self.config.deploy_cert(
- "encryption-example.demo", "example/cert.pem", "example/key.pem",
- "example/cert_chain.pem", "example/fullchain.pem")
- self.assertTrue(ssl_vhost.enabled)
- # Make sure that we don't error out if symlink already exists
- ssl_vhost.enabled = False
- self.assertFalse(ssl_vhost.enabled)
- self.config.deploy_cert(
- "encryption-example.demo", "example/cert.pem", "example/key.pem",
- "example/cert_chain.pem", "example/fullchain.pem")
- self.assertTrue(ssl_vhost.enabled)
+ with certbot_util.patch_display_util():
+ self.config.deploy_cert(
+ "encryption-example.demo", "example/cert.pem", "example/key.pem",
+ "example/cert_chain.pem", "example/fullchain.pem")
+ self.assertTrue(ssl_vhost.enabled)
+ # Make sure that we don't error out if symlink already exists
+ ssl_vhost.enabled = False
+ self.assertFalse(ssl_vhost.enabled)
+ self.config.deploy_cert(
+ "encryption-example.demo", "example/cert.pem", "example/key.pem",
+ "example/cert_chain.pem", "example/fullchain.pem")
+ self.assertTrue(ssl_vhost.enabled)
def test_enable_site_failure(self):
self.config.parser.root = "/tmp/nonexistent"
@@ -101,9 +103,10 @@ def test_deploy_cert_newssl(self):
# Get the default 443 vhost
self.config.assoc["random.demo"] = self.vh_truth[1]
- self.config.deploy_cert(
- "random.demo", "example/cert.pem", "example/key.pem",
- "example/cert_chain.pem", "example/fullchain.pem")
+ with certbot_util.patch_display_util():
+ self.config.deploy_cert(
+ "random.demo", "example/cert.pem", "example/key.pem",
+ "example/cert_chain.pem", "example/fullchain.pem")
self.config.save()
# Verify ssl_module was enabled.
diff --git a/certbot-apache/tests/display_ops_test.py b/certbot-apache/tests/display_ops_test.py
index 4559668acfb..fc3ab821ddb 100644
--- a/certbot-apache/tests/display_ops_test.py
+++ b/certbot-apache/tests/display_ops_test.py
@@ -3,8 +3,8 @@
try:
import mock
-except ImportError: # pragma: no cover
- from unittest import mock # type: ignore
+except ImportError: # pragma: no cover
+ from unittest import mock # type: ignore
from certbot import errors
from certbot.display import util as display_util
@@ -25,7 +25,7 @@ def setUp(self):
def test_select_no_input(self):
self.assertFalse(select_vhost_multiple([]))
- @certbot_util.patch_get_utility()
+ @certbot_util.patch_display_util()
def test_select_correct(self, mock_util):
mock_util().checklist.return_value = (
display_util.OK, [self.vhosts[3].display_repr(),
@@ -37,12 +37,13 @@ def test_select_correct(self, mock_util):
self.assertTrue(self.vhosts[3] in vhs)
self.assertFalse(self.vhosts[1] in vhs)
- @certbot_util.patch_get_utility()
+ @certbot_util.patch_display_util()
def test_select_cancel(self, mock_util):
mock_util().checklist.return_value = (display_util.CANCEL, "whatever")
vhs = select_vhost_multiple([self.vhosts[2], self.vhosts[3]])
self.assertFalse(vhs)
+
class SelectVhostTest(unittest.TestCase):
"""Tests for certbot_apache._internal.display_ops.select_vhost."""
@@ -56,12 +57,12 @@ def _call(cls, vhosts):
from certbot_apache._internal.display_ops import select_vhost
return select_vhost("example.com", vhosts)
- @certbot_util.patch_get_utility()
+ @certbot_util.patch_display_util()
def test_successful_choice(self, mock_util):
mock_util().menu.return_value = (display_util.OK, 3)
self.assertEqual(self.vhosts[3], self._call(self.vhosts))
- @certbot_util.patch_get_utility()
+ @certbot_util.patch_display_util()
def test_noninteractive(self, mock_util):
mock_util().menu.side_effect = errors.MissingCommandlineFlag("no vhost default")
try:
@@ -69,7 +70,7 @@ def test_noninteractive(self, mock_util):
except errors.MissingCommandlineFlag as e:
self.assertTrue("vhost ambiguity" in str(e))
- @certbot_util.patch_get_utility()
+ @certbot_util.patch_display_util()
def test_more_info_cancel(self, mock_util):
mock_util().menu.side_effect = [
(display_util.CANCEL, -1),
@@ -81,16 +82,15 @@ def test_no_vhosts(self):
self.assertEqual(self._call([]), None)
@mock.patch("certbot_apache._internal.display_ops.display_util")
- @certbot_util.patch_get_utility()
@mock.patch("certbot_apache._internal.display_ops.logger")
- def test_small_display(self, mock_logger, mock_util, mock_display_util):
+ def test_small_display(self, mock_logger, mock_display_util):
mock_display_util.WIDTH = 20
- mock_util().menu.return_value = (display_util.OK, 0)
+ mock_display_util.menu.return_value = (display_util.OK, 0)
self._call(self.vhosts)
self.assertEqual(mock_logger.debug.call_count, 1)
- @certbot_util.patch_get_utility()
+ @certbot_util.patch_display_util()
def test_multiple_names(self, mock_util):
mock_util().menu.return_value = (display_util.OK, 5)
diff --git a/certbot-apache/tests/http_01_test.py b/certbot-apache/tests/http_01_test.py
index 71f2db500f7..1ce47ed1a6e 100644
--- a/certbot-apache/tests/http_01_test.py
+++ b/certbot-apache/tests/http_01_test.py
@@ -125,6 +125,18 @@ def test_configure_multiple_vhosts(self):
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)
diff --git a/certbot-apache/tests/parser_test.py b/certbot-apache/tests/parser_test.py
index 00ca23f7a76..5ee64d3fdac 100644
--- a/certbot-apache/tests/parser_test.py
+++ b/certbot-apache/tests/parser_test.py
@@ -105,6 +105,11 @@ def test_add_dir_beginning(self):
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"))
diff --git a/certbot-apache/tests/testdata/debian_apache_2_4/multiple_vhosts/apache2/sites-available/empty.conf b/certbot-apache/tests/testdata/debian_apache_2_4/multiple_vhosts/apache2/sites-available/empty.conf
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/certbot-apache/tests/testdata/debian_apache_2_4/multiple_vhosts/apache2/sites-available/no-directives.conf b/certbot-apache/tests/testdata/debian_apache_2_4/multiple_vhosts/apache2/sites-available/no-directives.conf
new file mode 100644
index 00000000000..e7ceab441db
--- /dev/null
+++ b/certbot-apache/tests/testdata/debian_apache_2_4/multiple_vhosts/apache2/sites-available/no-directives.conf
@@ -0,0 +1,5 @@
+
+
+ Require all denied
+
+
diff --git a/certbot-apache/tests/util.py b/certbot-apache/tests/util.py
index a0b44d1881e..b9e7d2ea0a2 100644
--- a/certbot-apache/tests/util.py
+++ b/certbot-apache/tests/util.py
@@ -5,16 +5,16 @@
import augeas
import josepy as jose
+
try:
import mock
-except ImportError: # pragma: no cover
- from unittest import mock # type: ignore
-import zope.component
+except ImportError: # pragma: no cover
+ from unittest import mock # type: ignore
from certbot.compat import os
-from certbot.display import util as display_util
from certbot.plugins import common
from certbot.tests import util as test_util
+from certbot.display import util as display_util
from certbot_apache._internal import configurator
from certbot_apache._internal import entrypoint
from certbot_apache._internal import obj
@@ -69,9 +69,6 @@ def setUp(self, test_dir="debian_apache_2_4/multiple_vhosts",
vhost_root="debian_apache_2_4/multiple_vhosts/apache2/sites-available"):
super().setUp(test_dir, config_root, vhost_root)
- zope.component.provideUtility(display_util.FileDisplay(sys.stdout,
- False))
-
from certbot_apache._internal.parser import ApacheParser
self.aug = augeas.Augeas(
flags=augeas.Augeas.NONE | augeas.Augeas.NO_MODL_AUTOLOAD)
diff --git a/certbot-auto b/certbot-auto
deleted file mode 100755
index c37c45596ef..00000000000
--- a/certbot-auto
+++ /dev/null
@@ -1,1988 +0,0 @@
-#!/bin/sh
-#
-# Download and run the latest release version of the Certbot client.
-#
-# NOTE: THIS SCRIPT IS AUTO-GENERATED AND SELF-UPDATING
-#
-# IF YOU WANT TO EDIT IT LOCALLY, *ALWAYS* RUN YOUR COPY WITH THE
-# "--no-self-upgrade" FLAG
-#
-# IF YOU WANT TO SEND PULL REQUESTS, THE REAL SOURCE FOR THIS FILE IS
-# letsencrypt-auto-source/letsencrypt-auto.template AND
-# letsencrypt-auto-source/pieces/bootstrappers/*
-
-set -e # Work even if somebody does "sh thisscript.sh".
-
-# Note: you can set XDG_DATA_HOME or VENV_PATH before running this script,
-# if you want to change where the virtual environment will be installed
-
-# HOME might not be defined when being run through something like systemd
-if [ -z "$HOME" ]; then
- HOME=~root
-fi
-if [ -z "$XDG_DATA_HOME" ]; then
- XDG_DATA_HOME=~/.local/share
-fi
-if [ -z "$VENV_PATH" ]; then
- # We export these values so they are preserved properly if this script is
- # rerun with sudo/su where $HOME/$XDG_DATA_HOME may have a different value.
- export OLD_VENV_PATH="$XDG_DATA_HOME/letsencrypt"
- export VENV_PATH="/opt/eff.org/certbot/venv"
-fi
-VENV_BIN="$VENV_PATH/bin"
-BOOTSTRAP_VERSION_PATH="$VENV_PATH/certbot-auto-bootstrap-version.txt"
-LE_AUTO_VERSION="1.14.0"
-BASENAME=$(basename $0)
-USAGE="Usage: $BASENAME [OPTIONS]
-A self-updating wrapper script for the Certbot ACME client. When run, updates
-to both this script and certbot will be downloaded and installed. After
-ensuring you have the latest versions installed, certbot will be invoked with
-all arguments you have provided.
-
-Help for certbot itself cannot be provided until it is installed.
-
- --debug attempt experimental installation
- -h, --help print this help
- -n, --non-interactive, --noninteractive run without asking for user input
- --no-bootstrap do not install OS dependencies
- --no-permissions-check do not warn about file system permissions
- --no-self-upgrade do not download updates
- --os-packages-only install OS dependencies and exit
- --install-only install certbot, upgrade if needed, and exit
- -v, --verbose provide more output
- -q, --quiet provide only update/error output;
- implies --non-interactive
-
-All arguments are accepted and forwarded to the Certbot client when run."
-export CERTBOT_AUTO="$0"
-
-for arg in "$@" ; do
- case "$arg" in
- --debug)
- DEBUG=1;;
- --os-packages-only)
- OS_PACKAGES_ONLY=1;;
- --install-only)
- INSTALL_ONLY=1;;
- --no-self-upgrade)
- # Do not upgrade this script (also prevents client upgrades, because each
- # copy of the script pins a hash of the python client)
- NO_SELF_UPGRADE=1;;
- --no-permissions-check)
- NO_PERMISSIONS_CHECK=1;;
- --no-bootstrap)
- NO_BOOTSTRAP=1;;
- --help)
- HELP=1;;
- --noninteractive|--non-interactive)
- NONINTERACTIVE=1;;
- --quiet)
- QUIET=1;;
- renew)
- ASSUME_YES=1;;
- --verbose)
- VERBOSE=1;;
- -[!-]*)
- OPTIND=1
- while getopts ":hnvq" short_arg $arg; do
- case "$short_arg" in
- h)
- HELP=1;;
- n)
- NONINTERACTIVE=1;;
- q)
- QUIET=1;;
- v)
- VERBOSE=1;;
- esac
- done;;
- esac
-done
-
-if [ $BASENAME = "letsencrypt-auto" ]; then
- # letsencrypt-auto does not respect --help or --yes for backwards compatibility
- NONINTERACTIVE=1
- HELP=0
-fi
-
-# Set ASSUME_YES to 1 if QUIET or NONINTERACTIVE
-if [ "$QUIET" = 1 -o "$NONINTERACTIVE" = 1 ]; then
- ASSUME_YES=1
-fi
-
-say() {
- if [ "$QUIET" != 1 ]; then
- echo "$@"
- fi
-}
-
-error() {
- echo "$@"
-}
-
-# Support for busybox and others where there is no "command",
-# but "which" instead
-if command -v command > /dev/null 2>&1 ; then
- export EXISTS="command -v"
-elif which which > /dev/null 2>&1 ; then
- export EXISTS="which"
-else
- error "Cannot find command nor which... please install one!"
- exit 1
-fi
-
-# Certbot itself needs root access for almost all modes of operation.
-# certbot-auto needs root access to bootstrap OS dependencies and install
-# Certbot at a protected path so it can be safely run as root. To accomplish
-# this, this script will attempt to run itself as root if it doesn't have the
-# necessary privileges by using `sudo` or falling back to `su` if it is not
-# available. The mechanism used to obtain root access can be set explicitly by
-# setting the environment variable LE_AUTO_SUDO to 'sudo', 'su', 'su_sudo',
-# 'SuSudo', or '' as used below.
-
-# Because the parameters in `su -c` has to be a string,
-# we need to properly escape it.
-SuSudo() {
- args=""
- # This `while` loop iterates over all parameters given to this function.
- # For each parameter, all `'` will be replace by `'"'"'`, and the escaped string
- # will be wrapped in a pair of `'`, then appended to `$args` string
- # For example, `echo "It's only 1\$\!"` will be escaped to:
- # 'echo' 'It'"'"'s only 1$!'
- # │ │└┼┘│
- # │ │ │ └── `'s only 1$!'` the literal string
- # │ │ └── `\"'\"` is a single quote (as a string)
- # │ └── `'It'`, to be concatenated with the strings following it
- # └── `echo` wrapped in a pair of `'`, it's totally fine for the shell command itself
- while [ $# -ne 0 ]; do
- args="$args'$(printf "%s" "$1" | sed -e "s/'/'\"'\"'/g")' "
- shift
- done
- su root -c "$args"
-}
-
-# Sets the environment variable SUDO to be the name of the program or function
-# to call to get root access. If this script already has root privleges, SUDO
-# is set to an empty string. The value in SUDO should be run with the command
-# to called with root privileges as arguments.
-SetRootAuthMechanism() {
- SUDO=""
- if [ -n "${LE_AUTO_SUDO+x}" ]; then
- case "$LE_AUTO_SUDO" in
- SuSudo|su_sudo|su)
- SUDO=SuSudo
- ;;
- sudo)
- SUDO="sudo -E"
- ;;
- '')
- # If we're not running with root, don't check that this script can only
- # be modified by system users and groups.
- NO_PERMISSIONS_CHECK=1
- ;;
- *)
- error "Error: unknown root authorization mechanism '$LE_AUTO_SUDO'."
- exit 1
- esac
- say "Using preset root authorization mechanism '$LE_AUTO_SUDO'."
- else
- if test "`id -u`" -ne "0" ; then
- if $EXISTS sudo 1>/dev/null 2>&1; then
- SUDO="sudo -E"
- else
- say \"sudo\" is not available, will use \"su\" for installation steps...
- SUDO=SuSudo
- fi
- fi
- fi
-}
-
-if [ "$1" = "--cb-auto-has-root" ]; then
- shift 1
-else
- SetRootAuthMechanism
- if [ -n "$SUDO" ]; then
- say "Requesting to rerun $0 with root privileges..."
- $SUDO "$0" --cb-auto-has-root "$@"
- exit 0
- fi
-fi
-
-# Runs this script again with the given arguments. --cb-auto-has-root is added
-# to the command line arguments to ensure we don't try to acquire root a
-# second time. After the script is rerun, we exit the current script.
-RerunWithArgs() {
- "$0" --cb-auto-has-root "$@"
- exit 0
-}
-
-BootstrapMessage() {
- # Arguments: Platform name
- say "Bootstrapping dependencies for $1... (you can skip this with --no-bootstrap)"
-}
-
-ExperimentalBootstrap() {
- # Arguments: Platform name, bootstrap function name
- if [ "$DEBUG" = 1 ]; then
- if [ "$2" != "" ]; then
- BootstrapMessage $1
- $2
- fi
- else
- error "FATAL: $1 support is very experimental at present..."
- error "if you would like to work on improving it, please ensure you have backups"
- error "and then run this script again with the --debug flag!"
- error "Alternatively, you can install OS dependencies yourself and run this script"
- error "again with --no-bootstrap."
- exit 1
- fi
-}
-
-DeprecationBootstrap() {
- # Arguments: Platform name, bootstrap function name
- if [ "$DEBUG" = 1 ]; then
- if [ "$2" != "" ]; then
- BootstrapMessage $1
- $2
- fi
- else
- error "WARNING: certbot-auto support for this $1 is DEPRECATED!"
- error "Please visit certbot.eff.org to learn how to download a version of"
- error "Certbot that is packaged for your system. While an existing version"
- error "of certbot-auto may work currently, we have stopped supporting updating"
- error "system packages for your system. Please switch to a packaged version"
- error "as soon as possible."
- exit 1
- fi
-}
-
-MIN_PYTHON_2_VERSION="2.7"
-MIN_PYVER2=$(echo "$MIN_PYTHON_2_VERSION" | sed 's/\.//')
-MIN_PYTHON_3_VERSION="3.6"
-MIN_PYVER3=$(echo "$MIN_PYTHON_3_VERSION" | sed 's/\.//')
-# Sets LE_PYTHON to Python version string and PYVER to the first two
-# digits of the python version.
-# MIN_PYVER and MIN_PYTHON_VERSION are also set by this function, and their
-# values depend on if we try to use Python 3 or Python 2.
-DeterminePythonVersion() {
- # Arguments: "NOCRASH" if we shouldn't crash if we don't find a good python
- #
- # If no Python is found, PYVER is set to 0.
- if [ "$USE_PYTHON_3" = 1 ]; then
- MIN_PYVER=$MIN_PYVER3
- MIN_PYTHON_VERSION=$MIN_PYTHON_3_VERSION
- for LE_PYTHON in "$LE_PYTHON" python3; do
- # Break (while keeping the LE_PYTHON value) if found.
- $EXISTS "$LE_PYTHON" > /dev/null && break
- done
- else
- MIN_PYVER=$MIN_PYVER2
- MIN_PYTHON_VERSION=$MIN_PYTHON_2_VERSION
- for LE_PYTHON in "$LE_PYTHON" python2.7 python27 python2 python; do
- # Break (while keeping the LE_PYTHON value) if found.
- $EXISTS "$LE_PYTHON" > /dev/null && break
- done
- fi
- if [ "$?" != "0" ]; then
- if [ "$1" != "NOCRASH" ]; then
- error "Cannot find any Pythons; please install one!"
- exit 1
- else
- PYVER=0
- return 0
- fi
- fi
-
- PYVER=$("$LE_PYTHON" -V 2>&1 | cut -d" " -f 2 | cut -d. -f1,2 | sed 's/\.//')
- if [ "$PYVER" -lt "$MIN_PYVER" ]; then
- if [ "$1" != "NOCRASH" ]; then
- error "You have an ancient version of Python entombed in your operating system..."
- error "This isn't going to work; you'll need at least version $MIN_PYTHON_VERSION."
- exit 1
- fi
- fi
-}
-
-# If new packages are installed by BootstrapDebCommon below, this version
-# number must be increased.
-BOOTSTRAP_DEB_COMMON_VERSION=1
-
-BootstrapDebCommon() {
- # Current version tested with:
- #
- # - Ubuntu
- # - 14.04 (x64)
- # - 15.04 (x64)
- # - Debian
- # - 7.9 "wheezy" (x64)
- # - sid (2015-10-21) (x64)
-
- # Past versions tested with:
- #
- # - Debian 8.0 "jessie" (x64)
- # - Raspbian 7.8 (armhf)
-
- # Believed not to work:
- #
- # - Debian 6.0.10 "squeeze" (x64)
-
- if [ "$QUIET" = 1 ]; then
- QUIET_FLAG='-qq'
- fi
-
- apt-get $QUIET_FLAG update || error apt-get update hit problems but continuing anyway...
-
- # virtualenv binary can be found in different packages depending on
- # distro version (#346)
-
- virtualenv=
- # virtual env is known to apt and is installable
- if apt-cache show virtualenv > /dev/null 2>&1 ; then
- if ! LC_ALL=C apt-cache --quiet=0 show virtualenv 2>&1 | grep -q 'No packages found'; then
- virtualenv="virtualenv"
- fi
- fi
-
- if apt-cache show python-virtualenv > /dev/null 2>&1; then
- virtualenv="$virtualenv python-virtualenv"
- fi
-
- augeas_pkg="libaugeas0 augeas-lenses"
-
- if [ "$ASSUME_YES" = 1 ]; then
- YES_FLAG="-y"
- fi
-
- apt-get install $QUIET_FLAG $YES_FLAG --no-install-recommends \
- python \
- python-dev \
- $virtualenv \
- gcc \
- $augeas_pkg \
- libssl-dev \
- openssl \
- libffi-dev \
- ca-certificates \
-
-
- if ! $EXISTS virtualenv > /dev/null ; then
- error Failed to install a working \"virtualenv\" command, exiting
- exit 1
- fi
-}
-
-# If new packages are installed by BootstrapRpmCommonBase below, version
-# numbers in rpm_common.sh and rpm_python3.sh must be increased.
-
-# Sets TOOL to the name of the package manager
-# Sets appropriate values for YES_FLAG and QUIET_FLAG based on $ASSUME_YES and $QUIET_FLAG.
-# Note: this function is called both while selecting the bootstrap scripts and
-# during the actual bootstrap. Some things like prompting to user can be done in the latter
-# case, but not in the former one.
-InitializeRPMCommonBase() {
- if type dnf 2>/dev/null
- then
- TOOL=dnf
- elif type yum 2>/dev/null
- then
- TOOL=yum
-
- else
- error "Neither yum nor dnf found. Aborting bootstrap!"
- exit 1
- fi
-
- if [ "$ASSUME_YES" = 1 ]; then
- YES_FLAG="-y"
- fi
- if [ "$QUIET" = 1 ]; then
- QUIET_FLAG='--quiet'
- fi
-}
-
-BootstrapRpmCommonBase() {
- # Arguments: whitespace-delimited python packages to install
-
- InitializeRPMCommonBase # This call is superfluous in practice
-
- pkgs="
- gcc
- augeas-libs
- openssl
- openssl-devel
- libffi-devel
- redhat-rpm-config
- ca-certificates
- "
-
- # Add the python packages
- pkgs="$pkgs
- $1
- "
-
- if $TOOL list installed "httpd" >/dev/null 2>&1; then
- pkgs="$pkgs
- mod_ssl
- "
- fi
-
- if ! $TOOL install $YES_FLAG $QUIET_FLAG $pkgs; then
- error "Could not install OS dependencies. Aborting bootstrap!"
- exit 1
- fi
-}
-
-# If new packages are installed by BootstrapRpmCommon below, this version
-# number must be increased.
-BOOTSTRAP_RPM_COMMON_VERSION=1
-
-BootstrapRpmCommon() {
- # Tested with:
- # - Fedora 20, 21, 22, 23 (x64)
- # - Centos 7 (x64: on DigitalOcean droplet)
- # - CentOS 7 Minimal install in a Hyper-V VM
- # - CentOS 6
-
- InitializeRPMCommonBase
-
- # Most RPM distros use the "python" or "python-" naming convention. Let's try that first.
- if $TOOL list python >/dev/null 2>&1; then
- python_pkgs="$python
- python-devel
- python-virtualenv
- python-tools
- python-pip
- "
- # Fedora 26 starts to use the prefix python2 for python2 based packages.
- # this elseif is theoretically for any Fedora over version 26:
- elif $TOOL list python2 >/dev/null 2>&1; then
- python_pkgs="$python2
- python2-libs
- python2-setuptools
- python2-devel
- python2-virtualenv
- python2-tools
- python2-pip
- "
- # Some distros and older versions of current distros use a "python27"
- # instead of the "python" or "python-" naming convention.
- else
- python_pkgs="$python27
- python27-devel
- python27-virtualenv
- python27-tools
- python27-pip
- "
- fi
-
- BootstrapRpmCommonBase "$python_pkgs"
-}
-
-# If new packages are installed by BootstrapRpmPython3 below, this version
-# number must be increased.
-BOOTSTRAP_RPM_PYTHON3_LEGACY_VERSION=1
-
-# Checks if rh-python36 can be installed.
-Python36SclIsAvailable() {
- InitializeRPMCommonBase >/dev/null 2>&1;
-
- if "${TOOL}" list rh-python36 >/dev/null 2>&1; then
- return 0
- fi
- if "${TOOL}" list centos-release-scl >/dev/null 2>&1; then
- return 0
- fi
- return 1
-}
-
-# Try to enable rh-python36 from SCL if it is necessary and possible.
-EnablePython36SCL() {
- if "$EXISTS" python3.6 > /dev/null 2> /dev/null; then
- return 0
- fi
- if [ ! -f /opt/rh/rh-python36/enable ]; then
- return 0
- fi
- set +e
- if ! . /opt/rh/rh-python36/enable; then
- error 'Unable to enable rh-python36!'
- exit 1
- fi
- set -e
-}
-
-# This bootstrap concerns old RedHat-based distributions that do not ship by default
-# with Python 2.7, but only Python 2.6. We bootstrap them by enabling SCL and installing
-# Python 3.6. Some of these distributions are: CentOS/RHEL/OL/SL 6.
-BootstrapRpmPython3Legacy() {
- # Tested with:
- # - CentOS 6
-
- InitializeRPMCommonBase
-
- if ! "${TOOL}" list rh-python36 >/dev/null 2>&1; then
- echo "To use Certbot on this operating system, packages from the SCL repository need to be installed."
- if ! "${TOOL}" list centos-release-scl >/dev/null 2>&1; then
- error "Enable the SCL repository and try running Certbot again."
- exit 1
- fi
- if [ "${ASSUME_YES}" = 1 ]; then
- /bin/echo -n "Enabling the SCL repository in 3 seconds... (Press Ctrl-C to cancel)"
- sleep 1s
- /bin/echo -ne "\e[0K\rEnabling the SCL repository in 2 seconds... (Press Ctrl-C to cancel)"
- sleep 1s
- /bin/echo -e "\e[0K\rEnabling the SCL repository in 1 second... (Press Ctrl-C to cancel)"
- sleep 1s
- fi
- if ! "${TOOL}" install "${YES_FLAG}" "${QUIET_FLAG}" centos-release-scl; then
- error "Could not enable SCL. Aborting bootstrap!"
- exit 1
- fi
- fi
-
- # CentOS 6 must use rh-python36 from SCL
- if "${TOOL}" list rh-python36 >/dev/null 2>&1; then
- python_pkgs="rh-python36-python
- rh-python36-python-virtualenv
- rh-python36-python-devel
- "
- else
- error "No supported Python package available to install. Aborting bootstrap!"
- exit 1
- fi
-
- BootstrapRpmCommonBase "${python_pkgs}"
-
- # Enable SCL rh-python36 after bootstrapping.
- EnablePython36SCL
-}
-
-# If new packages are installed by BootstrapRpmPython3 below, this version
-# number must be increased.
-BOOTSTRAP_RPM_PYTHON3_VERSION=1
-
-BootstrapRpmPython3() {
- # Tested with:
- # - Fedora 29
-
- InitializeRPMCommonBase
-
- # Fedora 29 must use python3-virtualenv
- if $TOOL list python3-virtualenv >/dev/null 2>&1; then
- python_pkgs="python3
- python3-virtualenv
- python3-devel
- "
- else
- error "No supported Python package available to install. Aborting bootstrap!"
- exit 1
- fi
-
- BootstrapRpmCommonBase "$python_pkgs"
-}
-
-# If new packages are installed by BootstrapSuseCommon below, this version
-# number must be increased.
-BOOTSTRAP_SUSE_COMMON_VERSION=1
-
-BootstrapSuseCommon() {
- # SLE12 don't have python-virtualenv
-
- if [ "$ASSUME_YES" = 1 ]; then
- zypper_flags="-nq"
- install_flags="-l"
- fi
-
- if [ "$QUIET" = 1 ]; then
- QUIET_FLAG='-qq'
- fi
-
- if zypper search -x python-virtualenv >/dev/null 2>&1; then
- OPENSUSE_VIRTUALENV_PACKAGES="python-virtualenv"
- else
- # Since Leap 15.0 (and associated Tumbleweed version), python-virtualenv
- # is a source package, and python2-virtualenv must be used instead.
- # Also currently python2-setuptools is not a dependency of python2-virtualenv,
- # while it should be. Installing it explicitly until upstream fix.
- OPENSUSE_VIRTUALENV_PACKAGES="python2-virtualenv python2-setuptools"
- fi
-
- zypper $QUIET_FLAG $zypper_flags in $install_flags \
- python \
- python-devel \
- $OPENSUSE_VIRTUALENV_PACKAGES \
- gcc \
- augeas-lenses \
- libopenssl-devel \
- libffi-devel \
- ca-certificates
-}
-
-# If new packages are installed by BootstrapArchCommon below, this version
-# number must be increased.
-BOOTSTRAP_ARCH_COMMON_VERSION=1
-
-BootstrapArchCommon() {
- # Tested with:
- # - ArchLinux (x86_64)
- #
- # "python-virtualenv" is Python3, but "python2-virtualenv" provides
- # only "virtualenv2" binary, not "virtualenv".
-
- deps="
- python2
- python-virtualenv
- gcc
- augeas
- openssl
- libffi
- ca-certificates
- pkg-config
- "
-
- # pacman -T exits with 127 if there are missing dependencies
- missing=$(pacman -T $deps) || true
-
- if [ "$ASSUME_YES" = 1 ]; then
- noconfirm="--noconfirm"
- fi
-
- if [ "$missing" ]; then
- if [ "$QUIET" = 1 ]; then
- pacman -S --needed $missing $noconfirm > /dev/null
- else
- pacman -S --needed $missing $noconfirm
- fi
- fi
-}
-
-# If new packages are installed by BootstrapGentooCommon below, this version
-# number must be increased.
-BOOTSTRAP_GENTOO_COMMON_VERSION=1
-
-BootstrapGentooCommon() {
- PACKAGES="
- dev-lang/python:2.7
- dev-python/virtualenv
- app-admin/augeas
- dev-libs/openssl
- dev-libs/libffi
- app-misc/ca-certificates
- virtual/pkgconfig"
-
- ASK_OPTION="--ask"
- if [ "$ASSUME_YES" = 1 ]; then
- ASK_OPTION=""
- fi
-
- case "$PACKAGE_MANAGER" in
- (paludis)
- cave resolve --preserve-world --keep-targets if-possible $PACKAGES -x
- ;;
- (pkgcore)
- pmerge --noreplace --oneshot $ASK_OPTION $PACKAGES
- ;;
- (portage|*)
- emerge --noreplace --oneshot $ASK_OPTION $PACKAGES
- ;;
- esac
-}
-
-# If new packages are installed by BootstrapFreeBsd below, this version number
-# must be increased.
-BOOTSTRAP_FREEBSD_VERSION=1
-
-BootstrapFreeBsd() {
- if [ "$QUIET" = 1 ]; then
- QUIET_FLAG="--quiet"
- fi
-
- pkg install -Ay $QUIET_FLAG \
- python \
- py27-virtualenv \
- augeas \
- libffi
-}
-
-# If new packages are installed by BootstrapMac below, this version number must
-# be increased.
-BOOTSTRAP_MAC_VERSION=1
-
-BootstrapMac() {
- if hash brew 2>/dev/null; then
- say "Using Homebrew to install dependencies..."
- pkgman=brew
- pkgcmd="brew install"
- elif hash port 2>/dev/null; then
- say "Using MacPorts to install dependencies..."
- pkgman=port
- pkgcmd="port install"
- else
- say "No Homebrew/MacPorts; installing Homebrew..."
- ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
- pkgman=brew
- pkgcmd="brew install"
- fi
-
- $pkgcmd augeas
- if [ "$(which python)" = "/System/Library/Frameworks/Python.framework/Versions/2.7/bin/python" \
- -o "$(which python)" = "/usr/bin/python" ]; then
- # We want to avoid using the system Python because it requires root to use pip.
- # python.org, MacPorts or HomeBrew Python installations should all be OK.
- say "Installing python..."
- $pkgcmd python
- fi
-
- # Workaround for _dlopen not finding augeas on macOS
- if [ "$pkgman" = "port" ] && ! [ -e "/usr/local/lib/libaugeas.dylib" ] && [ -e "/opt/local/lib/libaugeas.dylib" ]; then
- say "Applying augeas workaround"
- mkdir -p /usr/local/lib/
- ln -s /opt/local/lib/libaugeas.dylib /usr/local/lib/
- fi
-
- if ! hash pip 2>/dev/null; then
- say "pip not installed"
- say "Installing pip..."
- curl --silent --show-error --retry 5 https://bootstrap.pypa.io/get-pip.py | python
- fi
-
- if ! hash virtualenv 2>/dev/null; then
- say "virtualenv not installed."
- say "Installing with pip..."
- pip install virtualenv
- fi
-}
-
-# If new packages are installed by BootstrapSmartOS below, this version number
-# must be increased.
-BOOTSTRAP_SMARTOS_VERSION=1
-
-BootstrapSmartOS() {
- pkgin update
- pkgin -y install 'gcc49' 'py27-augeas' 'py27-virtualenv'
-}
-
-# If new packages are installed by BootstrapMageiaCommon below, this version
-# number must be increased.
-BOOTSTRAP_MAGEIA_COMMON_VERSION=1
-
-BootstrapMageiaCommon() {
- if [ "$QUIET" = 1 ]; then
- QUIET_FLAG='--quiet'
- fi
-
- if ! urpmi --force $QUIET_FLAG \
- python \
- libpython-devel \
- python-virtualenv
- then
- error "Could not install Python dependencies. Aborting bootstrap!"
- exit 1
- fi
-
- if ! urpmi --force $QUIET_FLAG \
- git \
- gcc \
- python-augeas \
- libopenssl-devel \
- libffi-devel \
- rootcerts
- then
- error "Could not install additional dependencies. Aborting bootstrap!"
- exit 1
- fi
-}
-
-
-# Set Bootstrap to the function that installs OS dependencies on this system
-# and BOOTSTRAP_VERSION to the unique identifier for the current version of
-# that function. If Bootstrap is set to a function that doesn't install any
-# packages BOOTSTRAP_VERSION is not set.
-if [ -f /etc/debian_version ]; then
- DEPRECATED_OS=1
- NO_SELF_UPGRADE=1
-elif [ -f /etc/mageia-release ]; then
- # Mageia has both /etc/mageia-release and /etc/redhat-release
- DEPRECATED_OS=1
- NO_SELF_UPGRADE=1
-elif [ -f /etc/redhat-release ]; then
- DEPRECATED_OS=1
- NO_SELF_UPGRADE=1
- # Run DeterminePythonVersion to decide on the basis of available Python versions
- # whether to use 2.x or 3.x on RedHat-like systems.
- # Then, revert LE_PYTHON to its previous state.
- prev_le_python="$LE_PYTHON"
- unset LE_PYTHON
- DeterminePythonVersion "NOCRASH"
-
- RPM_DIST_NAME=`(. /etc/os-release 2> /dev/null && echo $ID) || echo "unknown"`
-
- if [ "$PYVER" -eq 26 -a $(uname -m) != 'x86_64' ]; then
- # 32 bits CentOS 6 and affiliates are not supported anymore by certbot-auto.
- DEPRECATED_OS=1
- fi
-
- # Set RPM_DIST_VERSION to VERSION_ID from /etc/os-release after splitting on
- # '.' characters (e.g. "8.0" becomes "8"). If the command exits with an
- # error, RPM_DIST_VERSION is set to "unknown".
- RPM_DIST_VERSION=$( (. /etc/os-release 2> /dev/null && echo "$VERSION_ID") | cut -d '.' -f1 || echo "unknown")
-
- # If RPM_DIST_VERSION is an empty string or it contains any nonnumeric
- # characters, the value is unexpected so we set RPM_DIST_VERSION to 0.
- if [ -z "$RPM_DIST_VERSION" ] || [ -n "$(echo "$RPM_DIST_VERSION" | tr -d '[0-9]')" ]; then
- RPM_DIST_VERSION=0
- fi
-
- # Handle legacy RPM distributions
- if [ "$PYVER" -eq 26 ]; then
- # Check if an automated bootstrap can be achieved on this system.
- if ! Python36SclIsAvailable; then
- INTERACTIVE_BOOTSTRAP=1
- fi
-
- USE_PYTHON_3=1
-
- # Try now to enable SCL rh-python36 for systems already bootstrapped
- # NB: EnablePython36SCL has been defined along with BootstrapRpmPython3Legacy in certbot-auto
- EnablePython36SCL
- else
- # Starting to Fedora 29, python2 is on a deprecation path. Let's move to python3 then.
- # RHEL 8 also uses python3 by default.
- if [ "$RPM_DIST_NAME" = "fedora" -a "$RPM_DIST_VERSION" -ge 29 ]; then
- RPM_USE_PYTHON_3=1
- elif [ "$RPM_DIST_NAME" = "rhel" -a "$RPM_DIST_VERSION" -ge 8 ]; then
- RPM_USE_PYTHON_3=1
- elif [ "$RPM_DIST_NAME" = "centos" -a "$RPM_DIST_VERSION" -ge 8 ]; then
- RPM_USE_PYTHON_3=1
- else
- RPM_USE_PYTHON_3=0
- fi
-
- if [ "$RPM_USE_PYTHON_3" = 1 ]; then
- USE_PYTHON_3=1
- fi
- fi
-
- LE_PYTHON="$prev_le_python"
-elif [ -f /etc/os-release ] && `grep -q openSUSE /etc/os-release` ; then
- DEPRECATED_OS=1
- NO_SELF_UPGRADE=1
-elif [ -f /etc/arch-release ]; then
- DEPRECATED_OS=1
- NO_SELF_UPGRADE=1
-elif [ -f /etc/manjaro-release ]; then
- DEPRECATED_OS=1
- NO_SELF_UPGRADE=1
-elif [ -f /etc/gentoo-release ]; then
- DEPRECATED_OS=1
- NO_SELF_UPGRADE=1
-elif uname | grep -iq FreeBSD ; then
- DEPRECATED_OS=1
- NO_SELF_UPGRADE=1
-elif uname | grep -iq Darwin ; then
- DEPRECATED_OS=1
- NO_SELF_UPGRADE=1
-elif [ -f /etc/issue ] && grep -iq "Amazon Linux" /etc/issue ; then
- DEPRECATED_OS=1
- NO_SELF_UPGRADE=1
-elif [ -f /etc/product ] && grep -q "Joyent Instance" /etc/product ; then
- DEPRECATED_OS=1
- NO_SELF_UPGRADE=1
-else
- DEPRECATED_OS=1
- NO_SELF_UPGRADE=1
-fi
-
-# We handle this case after determining the normal bootstrap version to allow
-# variables like USE_PYTHON_3 to be properly set. As described above, if the
-# Bootstrap function doesn't install any packages, BOOTSTRAP_VERSION should not
-# be set so we unset it here.
-if [ "$NO_BOOTSTRAP" = 1 ]; then
- Bootstrap() {
- :
- }
- unset BOOTSTRAP_VERSION
-fi
-
-if [ "$DEPRECATED_OS" = 1 ]; then
- Bootstrap() {
- error "Skipping bootstrap because certbot-auto is deprecated on this system."
- }
- unset BOOTSTRAP_VERSION
-fi
-
-# Sets PREV_BOOTSTRAP_VERSION to the identifier for the bootstrap script used
-# to install OS dependencies on this system. PREV_BOOTSTRAP_VERSION isn't set
-# if it is unknown how OS dependencies were installed on this system.
-SetPrevBootstrapVersion() {
- if [ -f $BOOTSTRAP_VERSION_PATH ]; then
- PREV_BOOTSTRAP_VERSION=$(cat "$BOOTSTRAP_VERSION_PATH")
- # The list below only contains bootstrap version strings that existed before
- # we started writing them to disk.
- #
- # DO NOT MODIFY THIS LIST UNLESS YOU KNOW WHAT YOU'RE DOING!
- elif grep -Fqx "$BOOTSTRAP_VERSION" << "UNLIKELY_EOF"
-BootstrapDebCommon 1
-BootstrapMageiaCommon 1
-BootstrapRpmCommon 1
-BootstrapSuseCommon 1
-BootstrapArchCommon 1
-BootstrapGentooCommon 1
-BootstrapFreeBsd 1
-BootstrapMac 1
-BootstrapSmartOS 1
-UNLIKELY_EOF
- then
- # If there's no bootstrap version saved to disk, but the currently selected
- # bootstrap script is from before we started saving the version number,
- # return the currently selected version to prevent us from rebootstrapping
- # unnecessarily.
- PREV_BOOTSTRAP_VERSION="$BOOTSTRAP_VERSION"
- fi
-}
-
-TempDir() {
- mktemp -d 2>/dev/null || mktemp -d -t 'le' # Linux || macOS
-}
-
-# Returns 0 if a letsencrypt installation exists at $OLD_VENV_PATH, otherwise,
-# returns a non-zero number.
-OldVenvExists() {
- [ -n "$OLD_VENV_PATH" -a -f "$OLD_VENV_PATH/bin/letsencrypt" ]
-}
-
-# Given python path, version 1 and version 2, check if version 1 is outdated compared to version 2.
-# An unofficial version provided as version 1 (eg. 0.28.0.dev0) will be treated
-# specifically by printing "UNOFFICIAL". Otherwise, print "OUTDATED" if version 1
-# is outdated, and "UP_TO_DATE" if not.
-# This function relies only on installed python environment (2.x or 3.x) by certbot-auto.
-CompareVersions() {
- "$1" - "$2" "$3" << "UNLIKELY_EOF"
-import sys
-from distutils.version import StrictVersion
-
-try:
- current = StrictVersion(sys.argv[1])
-except ValueError:
- sys.stdout.write('UNOFFICIAL')
- sys.exit()
-
-try:
- remote = StrictVersion(sys.argv[2])
-except ValueError:
- sys.stdout.write('UP_TO_DATE')
- sys.exit()
-
-if current < remote:
- sys.stdout.write('OUTDATED')
-else:
- sys.stdout.write('UP_TO_DATE')
-UNLIKELY_EOF
-}
-
-# Create a new virtual environment for Certbot. It will overwrite any existing one.
-# Parameters: LE_PYTHON, VENV_PATH, PYVER, VERBOSE
-CreateVenv() {
- "$1" - "$2" "$3" "$4" << "UNLIKELY_EOF"
-#!/usr/bin/env python
-import os
-import shutil
-import subprocess
-import sys
-
-
-def create_venv(venv_path, pyver, verbose):
- if os.path.exists(venv_path):
- shutil.rmtree(venv_path)
-
- stdout = sys.stdout if verbose == '1' else open(os.devnull, 'w')
-
- if int(pyver) <= 27:
- # Use virtualenv binary
- environ = os.environ.copy()
- environ['VIRTUALENV_NO_DOWNLOAD'] = '1'
- command = ['virtualenv', '--no-site-packages', '--python', sys.executable, venv_path]
- subprocess.check_call(command, stdout=stdout, env=environ)
- else:
- # Use embedded venv module in Python 3
- command = [sys.executable, '-m', 'venv', venv_path]
- subprocess.check_call(command, stdout=stdout)
-
-
-if __name__ == '__main__':
- create_venv(*sys.argv[1:])
-
-UNLIKELY_EOF
-}
-
-# Check that the given PATH_TO_CHECK has secured permissions.
-# Parameters: LE_PYTHON, PATH_TO_CHECK
-CheckPathPermissions() {
- "$1" - "$2" << "UNLIKELY_EOF"
-"""Verifies certbot-auto cannot be modified by unprivileged users.
-
-This script takes the path to certbot-auto as its only command line
-argument. It then checks that the file can only be modified by uid/gid
-< 1000 and if other users can modify the file, it prints a warning with
-a suggestion on how to solve the problem.
-
-Permissions on symlinks in the absolute path of certbot-auto are ignored
-and only the canonical path to certbot-auto is checked. There could be
-permissions problems due to the symlinks that are unreported by this
-script, however, issues like this were not caused by our documentation
-and are ignored for the sake of simplicity.
-
-All warnings are printed to stdout rather than stderr so all stderr
-output from this script can be suppressed to avoid printing messages if
-this script fails for some reason.
-
-"""
-from __future__ import print_function
-
-import os
-import stat
-import sys
-
-
-FORUM_POST_URL = 'https://community.letsencrypt.org/t/certbot-auto-deployment-best-practices/91979/'
-
-
-def has_safe_permissions(path):
- """Returns True if the given path has secure permissions.
-
- The permissions are considered safe if the file is only writable by
- uid/gid < 1000.
-
- The reason we allow more IDs than 0 is because on some systems such
- as Debian, system users/groups other than uid/gid 0 are used for the
- path we recommend in our instructions which is /usr/local/bin. 1000
- was chosen because on Debian 0-999 is reserved for system IDs[1] and
- on RHEL either 0-499 or 0-999 is reserved depending on the
- version[2][3]. Due to these differences across different OSes, this
- detection isn't perfect so we only determine permissions are
- insecure when we can be reasonably confident there is a problem
- regardless of the underlying OS.
-
- [1] https://www.debian.org/doc/debian-policy/ch-opersys.html#uid-and-gid-classes
- [2] https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/6/html/deployment_guide/ch-managing_users_and_groups
- [3] https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/7/html/system_administrators_guide/ch-managing_users_and_groups
-
- :param str path: filesystem path to check
- :returns: True if the path has secure permissions, otherwise, False
- :rtype: bool
-
- """
- # os.stat follows symlinks before obtaining information about a file.
- stat_result = os.stat(path)
- if stat_result.st_mode & stat.S_IWOTH:
- return False
- if stat_result.st_mode & stat.S_IWGRP and stat_result.st_gid >= 1000:
- return False
- if stat_result.st_mode & stat.S_IWUSR and stat_result.st_uid >= 1000:
- return False
- return True
-
-
-def main(certbot_auto_path):
- current_path = os.path.realpath(certbot_auto_path)
- last_path = None
- permissions_ok = True
- # This loop makes use of the fact that os.path.dirname('/') == '/'.
- while current_path != last_path and permissions_ok:
- permissions_ok = has_safe_permissions(current_path)
- last_path = current_path
- current_path = os.path.dirname(current_path)
-
- if not permissions_ok:
- print('{0} has insecure permissions!'.format(certbot_auto_path))
- print('To learn how to fix them, visit {0}'.format(FORUM_POST_URL))
-
-
-if __name__ == '__main__':
- main(sys.argv[1])
-
-UNLIKELY_EOF
-}
-
-if [ "$1" = "--le-auto-phase2" ]; then
- # Phase 2: Create venv, install LE, and run.
-
- shift 1 # the --le-auto-phase2 arg
-
- if [ "$DEPRECATED_OS" = 1 ]; then
- # Phase 2 damage control mode for deprecated OSes.
- # In this situation, we bypass any bootstrap or certbot venv setup.
- error "Your system is not supported by certbot-auto anymore."
-
- if [ ! -d "$VENV_PATH" ] && OldVenvExists; then
- VENV_BIN="$OLD_VENV_PATH/bin"
- fi
-
- if [ -f "$VENV_BIN/letsencrypt" -a "$INSTALL_ONLY" != 1 ]; then
- error "certbot-auto and its Certbot installation will no longer receive updates."
- error "You will not receive any bug fixes including those fixing server compatibility"
- error "or security problems."
- error "Please visit https://certbot.eff.org/ to check for other alternatives."
- "$VENV_BIN/letsencrypt" "$@"
- exit 0
- else
- error "Certbot cannot be installed."
- error "Please visit https://certbot.eff.org/ to check for other alternatives."
- exit 1
- fi
- fi
-
- SetPrevBootstrapVersion
-
- if [ -z "$PHASE_1_VERSION" -a "$USE_PYTHON_3" = 1 ]; then
- unset LE_PYTHON
- fi
-
- INSTALLED_VERSION="none"
- if [ -d "$VENV_PATH" ] || OldVenvExists; then
- # If the selected Bootstrap function isn't a noop and it differs from the
- # previously used version
- if [ -n "$BOOTSTRAP_VERSION" -a "$BOOTSTRAP_VERSION" != "$PREV_BOOTSTRAP_VERSION" ]; then
- # Check if we can rebootstrap without manual user intervention: this requires that
- # certbot-auto is in non-interactive mode AND selected bootstrap does not claim to
- # require a manual user intervention.
- if [ "$NONINTERACTIVE" = 1 -a "$INTERACTIVE_BOOTSTRAP" != 1 ]; then
- CAN_REBOOTSTRAP=1
- fi
- # Check if rebootstrap can be done non-interactively and current shell is non-interactive
- # (true if stdin and stdout are not attached to a terminal).
- if [ \( "$CAN_REBOOTSTRAP" = 1 \) -o \( \( -t 0 \) -a \( -t 1 \) \) ]; then
- if [ -d "$VENV_PATH" ]; then
- rm -rf "$VENV_PATH"
- fi
- # In the case the old venv was just a symlink to the new one,
- # OldVenvExists is now false because we deleted the venv at VENV_PATH.
- if OldVenvExists; then
- rm -rf "$OLD_VENV_PATH"
- ln -s "$VENV_PATH" "$OLD_VENV_PATH"
- fi
- RerunWithArgs "$@"
- # Otherwise bootstrap needs to be done manually by the user.
- else
- # If it is because bootstrapping is interactive, --non-interactive will be of no use.
- if [ "$INTERACTIVE_BOOTSTRAP" = 1 ]; then
- error "Skipping upgrade because new OS dependencies may need to be installed."
- error "This requires manual user intervention: please run this script again manually."
- # If this is because of the environment (eg. non interactive shell without
- # --non-interactive flag set), help the user in that direction.
- else
- error "Skipping upgrade because new OS dependencies may need to be installed."
- error
- error "To upgrade to a newer version, please run this script again manually so you can"
- error "approve changes or with --non-interactive on the command line to automatically"
- error "install any required packages."
- fi
- # Set INSTALLED_VERSION to be the same so we don't update the venv
- INSTALLED_VERSION="$LE_AUTO_VERSION"
- # Continue to use OLD_VENV_PATH if the new venv doesn't exist
- if [ ! -d "$VENV_PATH" ]; then
- VENV_BIN="$OLD_VENV_PATH/bin"
- fi
- fi
- elif [ -f "$VENV_BIN/letsencrypt" ]; then
- # --version output ran through grep due to python-cryptography DeprecationWarnings
- # grep for both certbot and letsencrypt until certbot and shim packages have been released
- INSTALLED_VERSION=$("$VENV_BIN/letsencrypt" --version 2>&1 | grep "^certbot\|^letsencrypt" | cut -d " " -f 2)
- if [ -z "$INSTALLED_VERSION" ]; then
- error "Error: couldn't get currently installed version for $VENV_BIN/letsencrypt: " 1>&2
- "$VENV_BIN/letsencrypt" --version
- exit 1
- fi
- fi
- fi
-
- if [ "$LE_AUTO_VERSION" != "$INSTALLED_VERSION" ]; then
- say "Creating virtual environment..."
- DeterminePythonVersion
- CreateVenv "$LE_PYTHON" "$VENV_PATH" "$PYVER" "$VERBOSE"
-
- if [ -n "$BOOTSTRAP_VERSION" ]; then
- echo "$BOOTSTRAP_VERSION" > "$BOOTSTRAP_VERSION_PATH"
- elif [ -n "$PREV_BOOTSTRAP_VERSION" ]; then
- echo "$PREV_BOOTSTRAP_VERSION" > "$BOOTSTRAP_VERSION_PATH"
- fi
-
- say "Installing Python packages..."
- TEMP_DIR=$(TempDir)
- trap 'rm -rf "$TEMP_DIR"' EXIT
- # There is no $ interpolation due to quotes on starting heredoc delimiter.
- # -------------------------------------------------------------------------
- cat << "UNLIKELY_EOF" > "$TEMP_DIR/letsencrypt-auto-requirements.txt"
-# This is the flattened list of packages certbot-auto installs.
-# To generate this, do (with docker and package hashin installed):
-# ```
-# letsencrypt-auto-source/rebuild_dependencies.py \
-# letsencrypt-auto-source/pieces/dependency-requirements.txt
-# ```
-# If you want to update a single dependency, run commands similar to these:
-# ```
-# pip install hashin
-# hashin -r dependency-requirements.txt cryptography==1.5.2
-# ```
-ConfigArgParse==1.2.3 \
- --hash=sha256:edd17be986d5c1ba2e307150b8e5f5107aba125f3574dddd02c85d5cdcfd37dc
-certifi==2020.4.5.1 \
- --hash=sha256:1d987a998c75633c40847cc966fcf5904906c920a7f17ef374f5aa4282abd304 \
- --hash=sha256:51fcb31174be6e6664c5f69e3e1691a2d72a1a12e90f872cbdb1567eb47b6519
-cffi==1.14.0 \
- --hash=sha256:001bf3242a1bb04d985d63e138230802c6c8d4db3668fb545fb5005ddf5bb5ff \
- --hash=sha256:00789914be39dffba161cfc5be31b55775de5ba2235fe49aa28c148236c4e06b \
- --hash=sha256:028a579fc9aed3af38f4892bdcc7390508adabc30c6af4a6e4f611b0c680e6ac \
- --hash=sha256:14491a910663bf9f13ddf2bc8f60562d6bc5315c1f09c704937ef17293fb85b0 \
- --hash=sha256:1cae98a7054b5c9391eb3249b86e0e99ab1e02bb0cc0575da191aedadbdf4384 \
- --hash=sha256:2089ed025da3919d2e75a4d963d008330c96751127dd6f73c8dc0c65041b4c26 \
- --hash=sha256:2d384f4a127a15ba701207f7639d94106693b6cd64173d6c8988e2c25f3ac2b6 \
- --hash=sha256:337d448e5a725bba2d8293c48d9353fc68d0e9e4088d62a9571def317797522b \
- --hash=sha256:399aed636c7d3749bbed55bc907c3288cb43c65c4389964ad5ff849b6370603e \
- --hash=sha256:3b911c2dbd4f423b4c4fcca138cadde747abdb20d196c4a48708b8a2d32b16dd \
- --hash=sha256:3d311bcc4a41408cf5854f06ef2c5cab88f9fded37a3b95936c9879c1640d4c2 \
- --hash=sha256:62ae9af2d069ea2698bf536dcfe1e4eed9090211dbaafeeedf5cb6c41b352f66 \
- --hash=sha256:66e41db66b47d0d8672d8ed2708ba91b2f2524ece3dee48b5dfb36be8c2f21dc \
- --hash=sha256:675686925a9fb403edba0114db74e741d8181683dcf216be697d208857e04ca8 \
- --hash=sha256:7e63cbcf2429a8dbfe48dcc2322d5f2220b77b2e17b7ba023d6166d84655da55 \
- --hash=sha256:8a6c688fefb4e1cd56feb6c511984a6c4f7ec7d2a1ff31a10254f3c817054ae4 \
- --hash=sha256:8c0ffc886aea5df6a1762d0019e9cb05f825d0eec1f520c51be9d198701daee5 \
- --hash=sha256:95cd16d3dee553f882540c1ffe331d085c9e629499ceadfbda4d4fde635f4b7d \
- --hash=sha256:99f748a7e71ff382613b4e1acc0ac83bf7ad167fb3802e35e90d9763daba4d78 \
- --hash=sha256:b8c78301cefcf5fd914aad35d3c04c2b21ce8629b5e4f4e45ae6812e461910fa \
- --hash=sha256:c420917b188a5582a56d8b93bdd8e0f6eca08c84ff623a4c16e809152cd35793 \
- --hash=sha256:c43866529f2f06fe0edc6246eb4faa34f03fe88b64a0a9a942561c8e22f4b71f \
- --hash=sha256:cab50b8c2250b46fe738c77dbd25ce017d5e6fb35d3407606e7a4180656a5a6a \
- --hash=sha256:cef128cb4d5e0b3493f058f10ce32365972c554572ff821e175dbc6f8ff6924f \
- --hash=sha256:cf16e3cf6c0a5fdd9bc10c21687e19d29ad1fe863372b5543deaec1039581a30 \
- --hash=sha256:e56c744aa6ff427a607763346e4170629caf7e48ead6921745986db3692f987f \
- --hash=sha256:e577934fc5f8779c554639376beeaa5657d54349096ef24abe8c74c5d9c117c3 \
- --hash=sha256:f2b0fa0c01d8a0c7483afd9f31d7ecf2d71760ca24499c8697aeb5ca37dc090c
-chardet==3.0.4 \
- --hash=sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae \
- --hash=sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691
-configobj==5.0.6 \
- --hash=sha256:a2f5650770e1c87fb335af19a9b7eb73fc05ccf22144eb68db7d00cd2bcb0902
-cryptography==2.8 \
- --hash=sha256:02079a6addc7b5140ba0825f542c0869ff4df9a69c360e339ecead5baefa843c \
- --hash=sha256:1df22371fbf2004c6f64e927668734070a8953362cd8370ddd336774d6743595 \
- --hash=sha256:369d2346db5934345787451504853ad9d342d7f721ae82d098083e1f49a582ad \
- --hash=sha256:3cda1f0ed8747339bbdf71b9f38ca74c7b592f24f65cdb3ab3765e4b02871651 \
- --hash=sha256:44ff04138935882fef7c686878e1c8fd80a723161ad6a98da31e14b7553170c2 \
- --hash=sha256:4b1030728872c59687badcca1e225a9103440e467c17d6d1730ab3d2d64bfeff \
- --hash=sha256:58363dbd966afb4f89b3b11dfb8ff200058fbc3b947507675c19ceb46104b48d \
- --hash=sha256:6ec280fb24d27e3d97aa731e16207d58bd8ae94ef6eab97249a2afe4ba643d42 \
- --hash=sha256:7270a6c29199adc1297776937a05b59720e8a782531f1f122f2eb8467f9aab4d \
- --hash=sha256:73fd30c57fa2d0a1d7a49c561c40c2f79c7d6c374cc7750e9ac7c99176f6428e \
- --hash=sha256:7f09806ed4fbea8f51585231ba742b58cbcfbfe823ea197d8c89a5e433c7e912 \
- --hash=sha256:90df0cc93e1f8d2fba8365fb59a858f51a11a394d64dbf3ef844f783844cc793 \
- --hash=sha256:971221ed40f058f5662a604bd1ae6e4521d84e6cad0b7b170564cc34169c8f13 \
- --hash=sha256:a518c153a2b5ed6b8cc03f7ae79d5ffad7315ad4569b2d5333a13c38d64bd8d7 \
- --hash=sha256:b0de590a8b0979649ebeef8bb9f54394d3a41f66c5584fff4220901739b6b2f0 \
- --hash=sha256:b43f53f29816ba1db8525f006fa6f49292e9b029554b3eb56a189a70f2a40879 \
- --hash=sha256:d31402aad60ed889c7e57934a03477b572a03af7794fa8fb1780f21ea8f6551f \
- --hash=sha256:de96157ec73458a7f14e3d26f17f8128c959084931e8997b9e655a39c8fde9f9 \
- --hash=sha256:df6b4dca2e11865e6cfbfb708e800efb18370f5a46fd601d3755bc7f85b3a8a2 \
- --hash=sha256:ecadccc7ba52193963c0475ac9f6fa28ac01e01349a2ca48509667ef41ffd2cf \
- --hash=sha256:fb81c17e0ebe3358486cd8cc3ad78adbae58af12fc2bf2bc0bb84e8090fa5ce8
-distro==1.5.0 \
- --hash=sha256:0e58756ae38fbd8fc3020d54badb8eae17c5b9dcbed388b17bb55b8a5928df92 \
- --hash=sha256:df74eed763e18d10d0da624258524ae80486432cd17392d9c3d96f5e83cd2799
-enum34==1.1.10; python_version < '3.4' \
- --hash=sha256:a98a201d6de3f2ab3db284e70a33b0f896fbf35f8086594e8c9e74b909058d53 \
- --hash=sha256:c3858660960c984d6ab0ebad691265180da2b43f07e061c0f8dca9ef3cffd328 \
- --hash=sha256:cce6a7477ed816bd2542d03d53db9f0db935dd013b70f336a95c73979289f248
-funcsigs==1.0.2 \
- --hash=sha256:330cc27ccbf7f1e992e69fef78261dc7c6569012cf397db8d3de0234e6c937ca \
- --hash=sha256:a7bb0f2cf3a3fd1ab2732cb49eba4252c2af4240442415b4abce3b87022a8f50
-idna==2.9 \
- --hash=sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb \
- --hash=sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa
-ipaddress==1.0.23 \
- --hash=sha256:6e0f4a39e66cb5bb9a137b00276a2eff74f93b71dcbdad6f10ff7df9d3557fcc \
- --hash=sha256:b7f8e0369580bb4a24d5ba1d7cc29660a4a6987763faf1d8a8046830e020e7e2
-josepy==1.3.0 \
- --hash=sha256:c341ffa403399b18e9eae9012f804843045764d1390f9cb4648980a7569b1619 \
- --hash=sha256:e54882c64be12a2a76533f73d33cba9e331950fda9e2731e843490b774e7a01c
-mock==1.3.0 \
- --hash=sha256:1e247dbecc6ce057299eb7ee019ad68314bb93152e81d9a6110d35f4d5eca0f6 \
- --hash=sha256:3f573a18be94de886d1191f27c168427ef693e8dcfcecf95b170577b2eb69cbb
-parsedatetime==2.5 \
- --hash=sha256:3b835fc54e472c17ef447be37458b400e3fefdf14bb1ffdedb5d2c853acf4ba1 \
- --hash=sha256:d2e9ddb1e463de871d32088a3f3cea3dc8282b1b2800e081bd0ef86900451667
-pbr==5.4.5 \
- --hash=sha256:07f558fece33b05caf857474a366dfcc00562bca13dd8b47b2b3e22d9f9bf55c \
- --hash=sha256:579170e23f8e0c2f24b0de612f71f648eccb79fb1322c814ae6b3c07b5ba23e8
-pyOpenSSL==19.1.0 \
- --hash=sha256:621880965a720b8ece2f1b2f54ea2071966ab00e2970ad2ce11d596102063504 \
- --hash=sha256:9a24494b2602aaf402be5c9e30a0b82d4a5c67528fe8fb475e3f3bc00dd69507
-pyRFC3339==1.1 \
- --hash=sha256:67196cb83b470709c580bb4738b83165e67c6cc60e1f2e4f286cfcb402a926f4 \
- --hash=sha256:81b8cbe1519cdb79bed04910dd6fa4e181faf8c88dff1e1b987b5f7ab23a5b1a
-pycparser==2.20 \
- --hash=sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0 \
- --hash=sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705
-pyparsing==2.4.7 \
- --hash=sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1 \
- --hash=sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b
-python-augeas==0.5.0 \
- --hash=sha256:67d59d66cdba8d624e0389b87b2a83a176f21f16a87553b50f5703b23f29bac2
-pytz==2020.1 \
- --hash=sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed \
- --hash=sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048
-requests==2.23.0 \
- --hash=sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee \
- --hash=sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6
-requests-toolbelt==0.9.1 \
- --hash=sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f \
- --hash=sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0
-six==1.15.0 \
- --hash=sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259 \
- --hash=sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced
-urllib3==1.25.9 \
- --hash=sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527 \
- --hash=sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115
-zope.component==4.6.1 \
- --hash=sha256:bfbe55d4a93e70a78b10edc3aad4de31bb8860919b7cbd8d66f717f7d7b279ac \
- --hash=sha256:d9c7c27673d787faff8a83797ce34d6ebcae26a370e25bddb465ac2182766aca
-zope.deferredimport==4.3.1 \
- --hash=sha256:57b2345e7b5eef47efcd4f634ff16c93e4265de3dcf325afc7315ade48d909e1 \
- --hash=sha256:9a0c211df44aa95f1c4e6d2626f90b400f56989180d3ef96032d708da3d23e0a
-zope.deprecation==4.4.0 \
- --hash=sha256:0d453338f04bacf91bbfba545d8bcdf529aa829e67b705eac8c1a7fdce66e2df \
- --hash=sha256:f1480b74995958b24ce37b0ef04d3663d2683e5d6debc96726eff18acf4ea113
-zope.event==4.4 \
- --hash=sha256:69c27debad9bdacd9ce9b735dad382142281ac770c4a432b533d6d65c4614bcf \
- --hash=sha256:d8e97d165fd5a0997b45f5303ae11ea3338becfe68c401dd88ffd2113fe5cae7
-zope.hookable==5.0.1 \
- --hash=sha256:0194b9b9e7f614abba60c90b231908861036578297515d3d6508eb10190f266d \
- --hash=sha256:0c2977473918bdefc6fa8dfb311f154e7f13c6133957fe649704deca79b92093 \
- --hash=sha256:17b8bdb3b77e03a152ca0d5ca185a7ae0156f5e5a2dbddf538676633a1f7380f \
- --hash=sha256:29d07681a78042cdd15b268ae9decffed9ace68a53eebeb61d65ae931d158841 \
- --hash=sha256:36fb1b35d1150267cb0543a1ddd950c0bc2c75ed0e6e92e3aaa6ac2e29416cb7 \
- --hash=sha256:3aed60c2bb5e812bbf9295c70f25b17ac37c233f30447a96c67913ba5073642f \
- --hash=sha256:3cac1565cc768911e72ca9ec4ddf5c5109e1fef0104f19f06649cf1874943b60 \
- --hash=sha256:3d4bc0cc4a37c3cd3081063142eeb2125511db3c13f6dc932d899c512690378e \
- --hash=sha256:3f73096f27b8c28be53ffb6604f7b570fbbb82f273c6febe5f58119009b59898 \
- --hash=sha256:522d1153d93f2d48aa0bd9fb778d8d4500be2e4dcf86c3150768f0e3adbbc4ef \
- --hash=sha256:523d2928fb7377bbdbc9af9c0b14ad73e6eaf226349f105733bdae27efd15b5a \
- --hash=sha256:5848309d4fc5c02150a45e8f8d2227e5bfda386a508bbd3160fed7c633c5a2fa \
- --hash=sha256:6781f86e6d54a110980a76e761eb54590630fd2af2a17d7edf02a079d2646c1d \
- --hash=sha256:6fd27921ebf3aaa945fa25d790f1f2046204f24dba4946f82f5f0a442577c3e9 \
- --hash=sha256:70d581862863f6bf9e175e85c9d70c2d7155f53fb04dcdb2f73cf288ca559a53 \
- --hash=sha256:81867c23b0dc66c8366f351d00923f2bc5902820a24c2534dfd7bf01a5879963 \
- --hash=sha256:81db29edadcbb740cd2716c95a297893a546ed89db1bfe9110168732d7f0afdd \
- --hash=sha256:86bd12624068cea60860a0759af5e2c3adc89c12aef6f71cf12f577e28deefe3 \
- --hash=sha256:9c184d8f9f7a76e1ced99855ccf390ffdd0ec3765e5cbf7b9cada600accc0a1e \
- --hash=sha256:acc789e8c29c13555e43fe4bf9fcd15a65512c9645e97bbaa5602e3201252b02 \
- --hash=sha256:afaa740206b7660d4cc3b8f120426c85761f51379af7a5b05451f624ad12b0af \
- --hash=sha256:b5f5fa323f878bb16eae68ea1ba7f6c0419d4695d0248bed4b18f51d7ce5ab85 \
- --hash=sha256:bd89e0e2c67bf4ac3aca2a19702b1a37269fb1923827f68324ac2e7afd6e3406 \
- --hash=sha256:c212de743283ec0735db24ec6ad913758df3af1b7217550ff270038062afd6ae \
- --hash=sha256:ca553f524293a0bdea05e7f44c3e685e4b7b022cb37d87bc4a3efa0f86587a8d \
- --hash=sha256:cab67065a3db92f636128d3157cc5424a145f82d96fb47159c539132833a6d36 \
- --hash=sha256:d3b3b3eedfdbf6b02898216e85aa6baf50207f4378a2a6803d6d47650cd37031 \
- --hash=sha256:d9f4a5a72f40256b686d31c5c0b1fde503172307beb12c1568296e76118e402c \
- --hash=sha256:df5067d87aaa111ed5d050e1ee853ba284969497f91806efd42425f5348f1c06 \
- --hash=sha256:e2587644812c6138f05b8a41594a8337c6790e3baf9a01915e52438c13fc6bef \
- --hash=sha256:e27fd877662db94f897f3fd532ef211ca4901eb1a70ba456f15c0866a985464a \
- --hash=sha256:e427ebbdd223c72e06ba94c004bb04e996c84dec8a0fa84e837556ae145c439e \
- --hash=sha256:e583ad4309c203ef75a09d43434cf9c2b4fa247997ecb0dcad769982c39411c7 \
- --hash=sha256:e760b2bc8ece9200804f0c2b64d10147ecaf18455a2a90827fbec4c9d84f3ad5 \
- --hash=sha256:ea9a9cc8bcc70e18023f30fa2f53d11ae069572a162791224e60cd65df55fb69 \
- --hash=sha256:ecb3f17dce4803c1099bd21742cd126b59817a4e76a6544d31d2cca6e30dbffd \
- --hash=sha256:ed794e3b3de42486d30444fb60b5561e724ee8a2d1b17b0c2e0f81e3ddaf7a87 \
- --hash=sha256:ee885d347279e38226d0a437b6a932f207f691c502ee565aba27a7022f1285df \
- --hash=sha256:fd5e7bc5f24f7e3d490698f7b854659a9851da2187414617cd5ed360af7efd63 \
- --hash=sha256:fe45f6870f7588ac7b2763ff1ce98cce59369717afe70cc353ec5218bc854bcc
-zope.interface==5.1.0 \
- --hash=sha256:0103cba5ed09f27d2e3de7e48bb320338592e2fabc5ce1432cf33808eb2dfd8b \
- --hash=sha256:14415d6979356629f1c386c8c4249b4d0082f2ea7f75871ebad2e29584bd16c5 \
- --hash=sha256:1ae4693ccee94c6e0c88a4568fb3b34af8871c60f5ba30cf9f94977ed0e53ddd \
- --hash=sha256:1b87ed2dc05cb835138f6a6e3595593fea3564d712cb2eb2de963a41fd35758c \
- --hash=sha256:269b27f60bcf45438e8683269f8ecd1235fa13e5411de93dae3b9ee4fe7f7bc7 \
- --hash=sha256:27d287e61639d692563d9dab76bafe071fbeb26818dd6a32a0022f3f7ca884b5 \
- --hash=sha256:39106649c3082972106f930766ae23d1464a73b7d30b3698c986f74bf1256a34 \
- --hash=sha256:40e4c42bd27ed3c11b2c983fecfb03356fae1209de10686d03c02c8696a1d90e \
- --hash=sha256:461d4339b3b8f3335d7e2c90ce335eb275488c587b61aca4b305196dde2ff086 \
- --hash=sha256:4f98f70328bc788c86a6a1a8a14b0ea979f81ae6015dd6c72978f1feff70ecda \
- --hash=sha256:558a20a0845d1a5dc6ff87cd0f63d7dac982d7c3be05d2ffb6322a87c17fa286 \
- --hash=sha256:562dccd37acec149458c1791da459f130c6cf8902c94c93b8d47c6337b9fb826 \
- --hash=sha256:5e86c66a6dea8ab6152e83b0facc856dc4d435fe0f872f01d66ce0a2131b7f1d \
- --hash=sha256:60a207efcd8c11d6bbeb7862e33418fba4e4ad79846d88d160d7231fcb42a5ee \
- --hash=sha256:645a7092b77fdbc3f68d3cc98f9d3e71510e419f54019d6e282328c0dd140dcd \
- --hash=sha256:6874367586c020705a44eecdad5d6b587c64b892e34305bb6ed87c9bbe22a5e9 \
- --hash=sha256:74bf0a4f9091131de09286f9a605db449840e313753949fe07c8d0fe7659ad1e \
- --hash=sha256:7b726194f938791a6691c7592c8b9e805fc6d1b9632a833b9c0640828cd49cbc \
- --hash=sha256:8149ded7f90154fdc1a40e0c8975df58041a6f693b8f7edcd9348484e9dc17fe \
- --hash=sha256:8cccf7057c7d19064a9e27660f5aec4e5c4001ffcf653a47531bde19b5aa2a8a \
- --hash=sha256:911714b08b63d155f9c948da2b5534b223a1a4fc50bb67139ab68b277c938578 \
- --hash=sha256:a5f8f85986197d1dd6444763c4a15c991bfed86d835a1f6f7d476f7198d5f56a \
- --hash=sha256:a744132d0abaa854d1aad50ba9bc64e79c6f835b3e92521db4235a1991176813 \
- --hash=sha256:af2c14efc0bb0e91af63d00080ccc067866fb8cbbaca2b0438ab4105f5e0f08d \
- --hash=sha256:b054eb0a8aa712c8e9030065a59b5e6a5cf0746ecdb5f087cca5ec7685690c19 \
- --hash=sha256:b0becb75418f8a130e9d465e718316cd17c7a8acce6fe8fe07adc72762bee425 \
- --hash=sha256:b1d2ed1cbda2ae107283befd9284e650d840f8f7568cb9060b5466d25dc48975 \
- --hash=sha256:ba4261c8ad00b49d48bbb3b5af388bb7576edfc0ca50a49c11dcb77caa1d897e \
- --hash=sha256:d1fe9d7d09bb07228650903d6a9dc48ea649e3b8c69b1d263419cc722b3938e8 \
- --hash=sha256:d7804f6a71fc2dda888ef2de266727ec2f3915373d5a785ed4ddc603bbc91e08 \
- --hash=sha256:da2844fba024dd58eaa712561da47dcd1e7ad544a257482392472eae1c86d5e5 \
- --hash=sha256:dcefc97d1daf8d55199420e9162ab584ed0893a109f45e438b9794ced44c9fd0 \
- --hash=sha256:dd98c436a1fc56f48c70882cc243df89ad036210d871c7427dc164b31500dc11 \
- --hash=sha256:e74671e43ed4569fbd7989e5eecc7d06dc134b571872ab1d5a88f4a123814e9f \
- --hash=sha256:eb9b92f456ff3ec746cd4935b73c1117538d6124b8617bc0fe6fda0b3816e345 \
- --hash=sha256:ebb4e637a1fb861c34e48a00d03cffa9234f42bef923aec44e5625ffb9a8e8f9 \
- --hash=sha256:ef739fe89e7f43fb6494a43b1878a36273e5924869ba1d866f752c5812ae8d58 \
- --hash=sha256:f40db0e02a8157d2b90857c24d89b6310f9b6c3642369852cdc3b5ac49b92afc \
- --hash=sha256:f68bf937f113b88c866d090fea0bc52a098695173fc613b055a17ff0cf9683b6 \
- --hash=sha256:fb55c182a3f7b84c1a2d6de5fa7b1a05d4660d866b91dbf8d74549c57a1499e8
-zope.proxy==4.3.5 \
- --hash=sha256:00573dfa755d0703ab84bb23cb6ecf97bb683c34b340d4df76651f97b0bab068 \
- --hash=sha256:092049280f2848d2ba1b57b71fe04881762a220a97b65288bcb0968bb199ec30 \
- --hash=sha256:0cbd27b4d3718b5ec74fc65ffa53c78d34c65c6fd9411b8352d2a4f855220cf1 \
- --hash=sha256:17fc7e16d0c81f833a138818a30f366696653d521febc8e892858041c4d88785 \
- --hash=sha256:19577dfeb70e8a67249ba92c8ad20589a1a2d86a8d693647fa8385408a4c17b0 \
- --hash=sha256:207aa914576b1181597a1516e1b90599dc690c095343ae281b0772e44945e6a4 \
- --hash=sha256:219a7db5ed53e523eb4a4769f13105118b6d5b04ed169a283c9775af221e231f \
- --hash=sha256:2b50ea79849e46b5f4f2b0247a3687505d32d161eeb16a75f6f7e6cd81936e43 \
- --hash=sha256:5903d38362b6c716e66bbe470f190579c530a5baf03dbc8500e5c2357aa569a5 \
- --hash=sha256:5c24903675e271bd688c6e9e7df5775ac6b168feb87dbe0e4bcc90805f21b28f \
- --hash=sha256:5ef6bc5ed98139e084f4e91100f2b098a0cd3493d4e76f9d6b3f7b95d7ad0f06 \
- --hash=sha256:61b55ae3c23a126a788b33ffb18f37d6668e79a05e756588d9e4d4be7246ab1c \
- --hash=sha256:63ddb992931a5e616c87d3d89f5a58db086e617548005c7f9059fac68c03a5cc \
- --hash=sha256:6943da9c09870490dcfd50c4909c0cc19f434fa6948f61282dc9cb07bcf08160 \
- --hash=sha256:6ad40f85c1207803d581d5d75e9ea25327cd524925699a83dfc03bf8e4ba72b7 \
- --hash=sha256:6b44433a79bdd7af0e3337bd7bbcf53dd1f9b0fa66bf21bcb756060ce32a96c1 \
- --hash=sha256:6bbaa245015d933a4172395baad7874373f162955d73612f0b66b6c2c33b6366 \
- --hash=sha256:7007227f4ea85b40a2f5e5a244479f6a6dfcf906db9b55e812a814a8f0e2c28d \
- --hash=sha256:74884a0aec1f1609190ec8b34b5d58fb3b5353cf22b96161e13e0e835f13518f \
- --hash=sha256:7d25fe5571ddb16369054f54cdd883f23de9941476d97f2b92eb6d7d83afe22d \
- --hash=sha256:7e162bdc5e3baad26b2262240be7d2bab36991d85a6a556e48b9dfb402370261 \
- --hash=sha256:814d62678dc3a30f4aa081982d830b7c342cf230ffc9d030b020cb154eeebf9e \
- --hash=sha256:8878a34c5313ee52e20aa50b03138af8d472bae465710fb954d133a9bfd3c38d \
- --hash=sha256:a66a0d94e5b081d5d695e66d6667e91e74d79e273eee95c1747717ba9cb70792 \
- --hash=sha256:a69f5cbf4addcfdf03dda564a671040127a6b7c34cf9fe4973582e68441b63fa \
- --hash=sha256:b00f9f0c334d07709d3f73a7cb8ae63c6ca1a90c790a63b5e7effa666ef96021 \
- --hash=sha256:b6ed71e4a7b4690447b626f499d978aa13197a0e592950e5d7020308f6054698 \
- --hash=sha256:bdf5041e5851526e885af579d2f455348dba68d74f14a32781933569a327fddf \
- --hash=sha256:be034360dd34e62608419f86e799c97d389c10a0e677a25f236a971b2f40dac9 \
- --hash=sha256:cc8f590a5eed30b314ae6b0232d925519ade433f663de79cc3783e4b10d662ba \
- --hash=sha256:cd7a318a15fe6cc4584bf3c4426f092ed08c0fd012cf2a9173114234fe193e11 \
- --hash=sha256:cf19b5f63a59c20306e034e691402b02055c8f4e38bf6792c23cad489162a642 \
- --hash=sha256:cfc781ce442ec407c841e9aa51d0e1024f72b6ec34caa8fdb6ef9576d549acf2 \
- --hash=sha256:dea9f6f8633571e18bc20cad83603072e697103a567f4b0738d52dd0211b4527 \
- --hash=sha256:e4a86a1d5eb2cce83c5972b3930c7c1eac81ab3508464345e2b8e54f119d5505 \
- --hash=sha256:e7106374d4a74ed9ff00c46cc00f0a9f06a0775f8868e423f85d4464d2333679 \
- --hash=sha256:e98a8a585b5668aa9e34d10f7785abf9545fe72663b4bfc16c99a115185ae6a5 \
- --hash=sha256:f64840e68483316eb58d82c376ad3585ca995e69e33b230436de0cdddf7363f9 \
- --hash=sha256:f8f4b0a9e6683e43889852130595c8854d8ae237f2324a053cdd884de936aa9b \
- --hash=sha256:fc45a53219ed30a7f670a6d8c98527af0020e6fd4ee4c0a8fb59f147f06d816c
-
-# Contains the requirements for the letsencrypt package.
-#
-# Since the letsencrypt package depends on certbot and using pip with hashes
-# requires that all installed packages have hashes listed, this allows
-# dependency-requirements.txt to be used without requiring a hash for a
-# (potentially unreleased) Certbot package.
-
-letsencrypt==0.7.0 \
- --hash=sha256:105a5fb107e45bcd0722eb89696986dcf5f08a86a321d6aef25a0c7c63375ade \
- --hash=sha256:c36e532c486a7e92155ee09da54b436a3c420813ec1c590b98f635d924720de9
-
-certbot==1.14.0 \
- --hash=sha256:67b4d26ceaea6c7f8325d0d45169e7a165a2cabc7122c84bc971ba068ca19cca \
- --hash=sha256:959ea90c6bb8dca38eab9772722cb940972ef6afcd5f15deef08b3c3636841eb
-acme==1.14.0 \
- --hash=sha256:4f48c41261202f1a389ec2986b2580b58f53e0d5a1ae2463b34318d78b87fc66 \
- --hash=sha256:61daccfb0343628cbbca551a7fc4c82482113952c21db3fe0c585b7c98fa1c35
-certbot-apache==1.14.0 \
- --hash=sha256:b757038db23db707c44630fecb46e99172bd791f0db5a8e623c0842613c4d3d9 \
- --hash=sha256:887fe4a21af2de1e5c2c9428bacba6eb7c1219257bc70f1a1d8447c8a321adb0
-certbot-nginx==1.14.0 \
- --hash=sha256:8916a815437988d6c192df9f035bb7a176eab20eee0956677b335d0698d243fb \
- --hash=sha256:cc2a8a0de56d9bb6b2efbda6c80c647dad8db2bb90675cac03ade94bd5fc8597
-
-UNLIKELY_EOF
- # -------------------------------------------------------------------------
- cat << "UNLIKELY_EOF" > "$TEMP_DIR/pipstrap.py"
-#!/usr/bin/env python
-"""A small script that can act as a trust root for installing pip >=8
-Embed this in your project, and your VCS checkout is all you have to trust. In
-a post-peep era, this lets you claw your way to a hash-checking version of pip,
-with which you can install the rest of your dependencies safely. All it assumes
-is Python 2.6 or better and *some* version of pip already installed. If
-anything goes wrong, it will exit with a non-zero status code.
-"""
-# This is here so embedded copies are MIT-compliant:
-# Copyright (c) 2016 Erik Rose
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to
-# deal in the Software without restriction, including without limitation the
-# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
-# sell copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-from __future__ import print_function
-from distutils.version import StrictVersion
-from hashlib import sha256
-from os import environ
-from os.path import join
-from shutil import rmtree
-try:
- from subprocess import check_output
-except ImportError:
- from subprocess import CalledProcessError, PIPE, Popen
-
- def check_output(*popenargs, **kwargs):
- if 'stdout' in kwargs:
- raise ValueError('stdout argument not allowed, it will be '
- 'overridden.')
- process = Popen(stdout=PIPE, *popenargs, **kwargs)
- output, unused_err = process.communicate()
- retcode = process.poll()
- if retcode:
- cmd = kwargs.get("args")
- if cmd is None:
- cmd = popenargs[0]
- raise CalledProcessError(retcode, cmd)
- return output
-import sys
-from tempfile import mkdtemp
-try:
- from urllib2 import build_opener, HTTPHandler, HTTPSHandler
-except ImportError:
- from urllib.request import build_opener, HTTPHandler, HTTPSHandler
-try:
- from urlparse import urlparse
-except ImportError:
- from urllib.parse import urlparse # 3.4
-
-
-__version__ = 1, 5, 1
-PIP_VERSION = '9.0.1'
-DEFAULT_INDEX_BASE = 'https://pypi.python.org'
-
-
-# wheel has a conditional dependency on argparse:
-maybe_argparse = (
- [('18/dd/e617cfc3f6210ae183374cd9f6a26b20514bbb5a792af97949c5aacddf0f/'
- 'argparse-1.4.0.tar.gz',
- '62b089a55be1d8949cd2bc7e0df0bddb9e028faefc8c32038cc84862aefdd6e4')]
- if sys.version_info < (2, 7, 0) else [])
-
-
-# Be careful when updating the pinned versions here, in particular for pip.
-# Indeed starting from 10.0, pip will build dependencies in isolation if the
-# related projects are compliant with PEP 517. This is not something we want
-# as of now, so the isolation build will need to be disabled wherever
-# pipstrap is used (see https://github.com/certbot/certbot/issues/8256).
-PACKAGES = maybe_argparse + [
- # Pip has no dependencies, as it vendors everything:
- ('11/b6/abcb525026a4be042b486df43905d6893fb04f05aac21c32c638e939e447/'
- 'pip-{0}.tar.gz'.format(PIP_VERSION),
- '09f243e1a7b461f654c26a725fa373211bb7ff17a9300058b205c61658ca940d'),
- # This version of setuptools has only optional dependencies:
- ('37/1b/b25507861991beeade31473868463dad0e58b1978c209de27384ae541b0b/'
- 'setuptools-40.6.3.zip',
- '3b474dad69c49f0d2d86696b68105f3a6f195f7ab655af12ef9a9c326d2b08f8'),
- ('c9/1d/bd19e691fd4cfe908c76c429fe6e4436c9e83583c4414b54f6c85471954a/'
- 'wheel-0.29.0.tar.gz',
- '1ebb8ad7e26b448e9caa4773d2357849bf80ff9e313964bcaf79cbf0201a1648')
-]
-
-
-class HashError(Exception):
- def __str__(self):
- url, path, actual, expected = self.args
- return ('{url} did not match the expected hash {expected}. Instead, '
- 'it was {actual}. The file (left at {path}) may have been '
- 'tampered with.'.format(**locals()))
-
-
-def hashed_download(url, temp, digest):
- """Download ``url`` to ``temp``, make sure it has the SHA-256 ``digest``,
- and return its path."""
- # Based on pip 1.4.1's URLOpener but with cert verification removed. Python
- # >=2.7.9 verifies HTTPS certs itself, and, in any case, the cert
- # authenticity has only privacy (not arbitrary code execution)
- # implications, since we're checking hashes.
- def opener(using_https=True):
- opener = build_opener(HTTPSHandler())
- if using_https:
- # Strip out HTTPHandler to prevent MITM spoof:
- for handler in opener.handlers:
- if isinstance(handler, HTTPHandler):
- opener.handlers.remove(handler)
- return opener
-
- def read_chunks(response, chunk_size):
- while True:
- chunk = response.read(chunk_size)
- if not chunk:
- break
- yield chunk
-
- parsed_url = urlparse(url)
- response = opener(using_https=parsed_url.scheme == 'https').open(url)
- path = join(temp, parsed_url.path.split('/')[-1])
- actual_hash = sha256()
- with open(path, 'wb') as file:
- for chunk in read_chunks(response, 4096):
- file.write(chunk)
- actual_hash.update(chunk)
-
- actual_digest = actual_hash.hexdigest()
- if actual_digest != digest:
- raise HashError(url, path, actual_digest, digest)
- return path
-
-
-def get_index_base():
- """Return the URL to the dir containing the "packages" folder.
- Try to wring something out of PIP_INDEX_URL, if set. Hack "/simple" off the
- end if it's there; that is likely to give us the right dir.
- """
- env_var = environ.get('PIP_INDEX_URL', '').rstrip('/')
- if env_var:
- SIMPLE = '/simple'
- if env_var.endswith(SIMPLE):
- return env_var[:-len(SIMPLE)]
- else:
- return env_var
- else:
- return DEFAULT_INDEX_BASE
-
-
-def main():
- python = sys.executable or 'python'
- pip_version = StrictVersion(check_output([python, '-m', 'pip', '--version'])
- .decode('utf-8').split()[1])
- has_pip_cache = pip_version >= StrictVersion('6.0')
- index_base = get_index_base()
- temp = mkdtemp(prefix='pipstrap-')
- try:
- downloads = [hashed_download(index_base + '/packages/' + path,
- temp,
- digest)
- for path, digest in PACKAGES]
- # Calling pip as a module is the preferred way to avoid problems about pip self-upgrade.
- command = [python, '-m', 'pip', 'install', '--no-index', '--no-deps', '-U']
- # Disable cache since it is not used and it otherwise sometimes throws permission warnings:
- command.extend(['--no-cache-dir'] if has_pip_cache else [])
- command.extend(downloads)
- check_output(command)
- except HashError as exc:
- print(exc)
- except Exception:
- rmtree(temp)
- raise
- else:
- rmtree(temp)
- return 0
- return 1
-
-
-if __name__ == '__main__':
- sys.exit(main())
-
-UNLIKELY_EOF
- # -------------------------------------------------------------------------
- # Set PATH so pipstrap upgrades the right (v)env:
- PATH="$VENV_BIN:$PATH" "$VENV_BIN/python" "$TEMP_DIR/pipstrap.py"
- set +e
- if [ "$VERBOSE" = 1 ]; then
- "$VENV_BIN/pip" install --disable-pip-version-check --no-cache-dir --require-hashes -r "$TEMP_DIR/letsencrypt-auto-requirements.txt"
- else
- PIP_OUT=`"$VENV_BIN/pip" install --disable-pip-version-check --no-cache-dir --require-hashes -r "$TEMP_DIR/letsencrypt-auto-requirements.txt" 2>&1`
- fi
- PIP_STATUS=$?
- set -e
- if [ "$PIP_STATUS" != 0 ]; then
- # Report error. (Otherwise, be quiet.)
- error "Had a problem while installing Python packages."
- if [ "$VERBOSE" != 1 ]; then
- error
- error "pip prints the following errors: "
- error "====================================================="
- error "$PIP_OUT"
- error "====================================================="
- error
- error "Certbot has problem setting up the virtual environment."
-
- if `echo $PIP_OUT | grep -q Killed` || `echo $PIP_OUT | grep -q "allocate memory"` ; then
- error
- error "Based on your pip output, the problem can likely be fixed by "
- error "increasing the available memory."
- else
- error
- error "We were not be able to guess the right solution from your pip "
- error "output."
- fi
-
- error
- error "Consult https://certbot.eff.org/docs/install.html#problems-with-python-virtual-environment"
- error "for possible solutions."
- error "You may also find some support resources at https://certbot.eff.org/support/ ."
- fi
- rm -rf "$VENV_PATH"
- exit 1
- fi
-
- if [ -d "$OLD_VENV_PATH" -a ! -L "$OLD_VENV_PATH" ]; then
- rm -rf "$OLD_VENV_PATH"
- ln -s "$VENV_PATH" "$OLD_VENV_PATH"
- fi
-
- say "Installation succeeded."
- fi
-
- # If you're modifying any of the code after this point in this current `if` block, you
- # may need to update the "$DEPRECATED_OS" = 1 case at the beginning of phase 2 as well.
-
- if [ "$INSTALL_ONLY" = 1 ]; then
- say "Certbot is installed."
- exit 0
- fi
-
- "$VENV_BIN/letsencrypt" "$@"
-
-else
- # Phase 1: Upgrade certbot-auto if necessary, then self-invoke.
- #
- # Each phase checks the version of only the thing it is responsible for
- # upgrading. Phase 1 checks the version of the latest release of
- # certbot-auto (which is always the same as that of the certbot
- # package). Phase 2 checks the version of the locally installed certbot.
- export PHASE_1_VERSION="$LE_AUTO_VERSION"
-
- if [ ! -f "$VENV_BIN/letsencrypt" ]; then
- if ! OldVenvExists; then
- if [ "$HELP" = 1 ]; then
- echo "$USAGE"
- exit 0
- fi
- # If it looks like we've never bootstrapped before, bootstrap:
- Bootstrap
- fi
- fi
- if [ "$OS_PACKAGES_ONLY" = 1 ]; then
- say "OS packages installed."
- exit 0
- fi
-
- DeterminePythonVersion "NOCRASH"
- # Don't warn about file permissions if the user disabled the check or we
- # can't find an up-to-date Python.
- if [ "$PYVER" -ge "$MIN_PYVER" -a "$NO_PERMISSIONS_CHECK" != 1 ]; then
- # If the script fails for some reason, don't break certbot-auto.
- set +e
- # Suppress unexpected error output.
- CHECK_PERM_OUT=$(CheckPathPermissions "$LE_PYTHON" "$0" 2>/dev/null)
- CHECK_PERM_STATUS="$?"
- set -e
- # Only print output if the script ran successfully and it actually produced
- # output. The latter check resolves
- # https://github.com/certbot/certbot/issues/7012.
- if [ "$CHECK_PERM_STATUS" = 0 -a -n "$CHECK_PERM_OUT" ]; then
- error "$CHECK_PERM_OUT"
- fi
- fi
-
- if [ "$NO_SELF_UPGRADE" != 1 ]; then
- TEMP_DIR=$(TempDir)
- trap 'rm -rf "$TEMP_DIR"' EXIT
- # ---------------------------------------------------------------------------
- cat << "UNLIKELY_EOF" > "$TEMP_DIR/fetch.py"
-"""Do downloading and JSON parsing without additional dependencies. ::
-
- # Print latest released version of LE to stdout:
- python fetch.py --latest-version
-
- # Download letsencrypt-auto script from git tag v1.2.3 into the folder I'm
- # in, and make sure its signature verifies:
- python fetch.py --le-auto-script v1.2.3
-
-On failure, return non-zero.
-
-"""
-
-from __future__ import print_function, unicode_literals
-
-from distutils.version import LooseVersion
-from json import loads
-from os import devnull, environ
-from os.path import dirname, join
-import re
-import ssl
-from subprocess import check_call, CalledProcessError
-from sys import argv, exit
-try:
- from urllib2 import build_opener, HTTPHandler, HTTPSHandler
- from urllib2 import HTTPError, URLError
-except ImportError:
- from urllib.request import build_opener, HTTPHandler, HTTPSHandler
- from urllib.error import HTTPError, URLError
-
-PUBLIC_KEY = environ.get('LE_AUTO_PUBLIC_KEY', """-----BEGIN PUBLIC KEY-----
-MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6MR8W/galdxnpGqBsYbq
-OzQb2eyW15YFjDDEMI0ZOzt8f504obNs920lDnpPD2/KqgsfjOgw2K7xWDJIj/18
-xUvWPk3LDkrnokNiRkA3KOx3W6fHycKL+zID7zy+xZYBuh2fLyQtWV1VGQ45iNRp
-9+Zo7rH86cdfgkdnWTlNSHyTLW9NbXvyv/E12bppPcEvgCTAQXgnDVJ0/sqmeiij
-n9tTFh03aM+R2V/21h8aTraAS24qiPCz6gkmYGC8yr6mglcnNoYbsLNYZ69zF1XH
-cXPduCPdPdfLlzVlKK1/U7hkA28eG3BIAMh6uJYBRJTpiGgaGdPd7YekUB8S6cy+
-CQIDAQAB
------END PUBLIC KEY-----
-""")
-
-class ExpectedError(Exception):
- """A novice-readable exception that also carries the original exception for
- debugging"""
-
-
-class HttpsGetter(object):
- def __init__(self):
- """Build an HTTPS opener."""
- # Based on pip 1.4.1's URLOpener
- # This verifies certs on only Python >=2.7.9, and when NO_CERT_VERIFY isn't set.
- if environ.get('NO_CERT_VERIFY') == '1' and hasattr(ssl, 'SSLContext'):
- self._opener = build_opener(HTTPSHandler(context=cert_none_context()))
- else:
- self._opener = build_opener(HTTPSHandler())
- # Strip out HTTPHandler to prevent MITM spoof:
- for handler in self._opener.handlers:
- if isinstance(handler, HTTPHandler):
- self._opener.handlers.remove(handler)
-
- def get(self, url):
- """Return the document contents pointed to by an HTTPS URL.
-
- If something goes wrong (404, timeout, etc.), raise ExpectedError.
-
- """
- try:
- # socket module docs say default timeout is None: that is, no
- # timeout
- return self._opener.open(url, timeout=30).read()
- except (HTTPError, IOError) as exc:
- raise ExpectedError("Couldn't download %s." % url, exc)
-
-
-def write(contents, dir, filename):
- """Write something to a file in a certain directory."""
- with open(join(dir, filename), 'wb') as file:
- file.write(contents)
-
-
-def latest_stable_version(get):
- """Return the latest stable release of letsencrypt."""
- metadata = loads(get(
- environ.get('LE_AUTO_JSON_URL',
- 'https://pypi.python.org/pypi/certbot/json')).decode('UTF-8'))
- # metadata['info']['version'] actually returns the latest of any kind of
- # release release, contrary to https://wiki.python.org/moin/PyPIJSON.
- # The regex is a sufficient regex for picking out prereleases for most
- # packages, LE included.
- return str(max(LooseVersion(r) for r
- in metadata['releases'].keys()
- if re.match('^[0-9.]+$', r)))
-
-
-def verified_new_le_auto(get, tag, temp_dir):
- """Return the path to a verified, up-to-date letsencrypt-auto script.
-
- If the download's signature does not verify or something else goes wrong
- with the verification process, raise ExpectedError.
-
- """
- le_auto_dir = environ.get(
- 'LE_AUTO_DIR_TEMPLATE',
- 'https://raw.githubusercontent.com/certbot/certbot/%s/'
- 'letsencrypt-auto-source/') % tag
- write(get(le_auto_dir + 'letsencrypt-auto'), temp_dir, 'letsencrypt-auto')
- write(get(le_auto_dir + 'letsencrypt-auto.sig'), temp_dir, 'letsencrypt-auto.sig')
- write(PUBLIC_KEY.encode('UTF-8'), temp_dir, 'public_key.pem')
- try:
- with open(devnull, 'w') as dev_null:
- check_call(['openssl', 'dgst', '-sha256', '-verify',
- join(temp_dir, 'public_key.pem'),
- '-signature',
- join(temp_dir, 'letsencrypt-auto.sig'),
- join(temp_dir, 'letsencrypt-auto')],
- stdout=dev_null,
- stderr=dev_null)
- except CalledProcessError as exc:
- raise ExpectedError("Couldn't verify signature of downloaded "
- "certbot-auto.", exc)
-
-
-def cert_none_context():
- """Create a SSLContext object to not check hostname."""
- # PROTOCOL_TLS isn't available before 2.7.13 but this code is for 2.7.9+, so use this.
- context = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
- context.verify_mode = ssl.CERT_NONE
- return context
-
-
-def main():
- get = HttpsGetter().get
- flag = argv[1]
- try:
- if flag == '--latest-version':
- print(latest_stable_version(get))
- elif flag == '--le-auto-script':
- tag = argv[2]
- verified_new_le_auto(get, tag, dirname(argv[0]))
- except ExpectedError as exc:
- print(exc.args[0], exc.args[1])
- return 1
- else:
- return 0
-
-
-if __name__ == '__main__':
- exit(main())
-
-UNLIKELY_EOF
- # ---------------------------------------------------------------------------
- if [ "$PYVER" -lt "$MIN_PYVER" ]; then
- error "WARNING: couldn't find Python $MIN_PYTHON_VERSION+ to check for updates."
- elif ! REMOTE_VERSION=`"$LE_PYTHON" "$TEMP_DIR/fetch.py" --latest-version` ; then
- error "WARNING: unable to check for updates."
- fi
-
- # If for any reason REMOTE_VERSION is not set, let's assume certbot-auto is up-to-date,
- # and do not go into the self-upgrading process.
- if [ -n "$REMOTE_VERSION" ]; then
- LE_VERSION_STATE=`CompareVersions "$LE_PYTHON" "$LE_AUTO_VERSION" "$REMOTE_VERSION"`
-
- if [ "$LE_VERSION_STATE" = "UNOFFICIAL" ]; then
- say "Unofficial certbot-auto version detected, self-upgrade is disabled: $LE_AUTO_VERSION"
- elif [ "$LE_VERSION_STATE" = "OUTDATED" ]; then
- say "Upgrading certbot-auto $LE_AUTO_VERSION to $REMOTE_VERSION..."
-
- # Now we drop into Python so we don't have to install even more
- # dependencies (curl, etc.), for better flow control, and for the option of
- # future Windows compatibility.
- "$LE_PYTHON" "$TEMP_DIR/fetch.py" --le-auto-script "v$REMOTE_VERSION"
-
- # Install new copy of certbot-auto.
- # TODO: Deal with quotes in pathnames.
- say "Replacing certbot-auto..."
- # Clone permissions with cp. chmod and chown don't have a --reference
- # option on macOS or BSD, and stat -c on Linux is stat -f on macOS and BSD:
- cp -p "$0" "$TEMP_DIR/letsencrypt-auto.permission-clone"
- cp "$TEMP_DIR/letsencrypt-auto" "$TEMP_DIR/letsencrypt-auto.permission-clone"
- # Using mv rather than cp leaves the old file descriptor pointing to the
- # original copy so the shell can continue to read it unmolested. mv across
- # filesystems is non-atomic, doing `rm dest, cp src dest, rm src`, but the
- # cp is unlikely to fail if the rm doesn't.
- mv -f "$TEMP_DIR/letsencrypt-auto.permission-clone" "$0"
- fi # A newer version is available.
- fi
- fi # Self-upgrading is allowed.
-
- RerunWithArgs --le-auto-phase2 "$@"
-fi
diff --git a/certbot-ci/certbot_integration_tests/certbot_tests/test_main.py b/certbot-ci/certbot_integration_tests/certbot_tests/test_main.py
index 965e4b6d8cc..18bc243e4db 100644
--- a/certbot-ci/certbot_integration_tests/certbot_tests/test_main.py
+++ b/certbot-ci/certbot_integration_tests/certbot_tests/test_main.py
@@ -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')
diff --git a/certbot-ci/certbot_integration_tests/utils/acme_server.py b/certbot-ci/certbot_integration_tests/utils/acme_server.py
index 9fd5fcb39be..65b78faad67 100755
--- a/certbot-ci/certbot_integration_tests/utils/acme_server.py
+++ b/certbot-ci/certbot_integration_tests/utils/acme_server.py
@@ -52,7 +52,7 @@ def __init__(self, acme_server, nodes, http_proxy=True, stdout=False,
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:
diff --git a/certbot-ci/certbot_integration_tests/utils/dns_server.py b/certbot-ci/certbot_integration_tests/utils/dns_server.py
index d9007ef3ab4..c4bbcaea1db 100644
--- a/certbot-ci/certbot_integration_tests/utils/dns_server.py
+++ b/certbot-ci/certbot_integration_tests/utils/dns_server.py
@@ -45,6 +45,7 @@ def __init__(self, unused_nodes, show_output=False):
# 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):
diff --git a/certbot-compatibility-test/certbot_compatibility_test/configurators/nginx/common.py b/certbot-compatibility-test/certbot_compatibility_test/configurators/nginx/common.py
index 7cba487cf33..b0a24a63c28 100644
--- a/certbot-compatibility-test/certbot_compatibility_test/configurators/nginx/common.py
+++ b/certbot-compatibility-test/certbot_compatibility_test/configurators/nginx/common.py
@@ -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
diff --git a/certbot-compatibility-test/certbot_compatibility_test/test_driver.py b/certbot-compatibility-test/certbot_compatibility_test/test_driver.py
index d7cafd23528..8c8086b37c6 100644
--- a/certbot-compatibility-test/certbot_compatibility_test/test_driver.py
+++ b/certbot-compatibility-test/certbot_compatibility_test/test_driver.py
@@ -10,7 +10,6 @@
import time
from typing import List
from typing import Tuple
-import zope.component
import OpenSSL
from urllib3.util import connection
@@ -21,6 +20,7 @@
from certbot import achallenges
from certbot import errors as le_errors
from certbot.display import util as display_util
+from certbot._internal.display import obj as display_obj
from certbot.tests import acme_util
from certbot_compatibility_test import errors
from certbot_compatibility_test import util
@@ -332,7 +332,7 @@ def setup_logging(args):
def setup_display():
""""Prepares IDisplay for the Certbot plugins """
displayer = display_util.NoninteractiveDisplay(sys.stdout)
- zope.component.provideUtility(displayer)
+ display_obj.set_display(displayer)
def main():
diff --git a/certbot-compatibility-test/setup.py b/certbot-compatibility-test/setup.py
index 879dff40c47..7bdc6a11cf1 100644
--- a/certbot-compatibility-test/setup.py
+++ b/certbot-compatibility-test/setup.py
@@ -3,7 +3,7 @@
from setuptools import find_packages
from setuptools import setup
-version = '1.17.0.dev0'
+version = '1.18.0.dev0'
install_requires = [
'certbot',
diff --git a/certbot-dns-cloudflare/certbot_dns_cloudflare/_internal/dns_cloudflare.py b/certbot-dns-cloudflare/certbot_dns_cloudflare/_internal/dns_cloudflare.py
index 8f42b3ce917..425cf511663 100644
--- a/certbot-dns-cloudflare/certbot_dns_cloudflare/_internal/dns_cloudflare.py
+++ b/certbot-dns-cloudflare/certbot_dns_cloudflare/_internal/dns_cloudflare.py
@@ -6,10 +6,8 @@
from typing import Optional
import CloudFlare
-import zope.interface
from certbot import errors
-from certbot import interfaces
from certbot.plugins import dns_common
from certbot.plugins.dns_common import CredentialsConfiguration
@@ -18,8 +16,6 @@
ACCOUNT_URL = 'https://dash.cloudflare.com/?to=/:account/profile/api-tokens'
-@zope.interface.implementer(interfaces.IAuthenticator)
-@zope.interface.provider(interfaces.IPluginFactory)
class Authenticator(dns_common.DNSAuthenticator):
"""DNS Authenticator for Cloudflare
diff --git a/certbot-dns-cloudflare/setup.py b/certbot-dns-cloudflare/setup.py
index 3e5cade987a..8642a35698d 100644
--- a/certbot-dns-cloudflare/setup.py
+++ b/certbot-dns-cloudflare/setup.py
@@ -4,7 +4,7 @@
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',
diff --git a/certbot-dns-cloudflare/tests/dns_cloudflare_test.py b/certbot-dns-cloudflare/tests/dns_cloudflare_test.py
index 94676f9d28a..2b182783103 100644
--- a/certbot-dns-cloudflare/tests/dns_cloudflare_test.py
+++ b/certbot-dns-cloudflare/tests/dns_cloudflare_test.py
@@ -41,7 +41,7 @@ def setUp(self):
# _get_cloudflare_client | pylint: disable=protected-access
self.auth._get_cloudflare_client = mock.MagicMock(return_value=self.mock_client)
- @test_util.patch_get_utility()
+ @test_util.patch_display_util()
def test_perform(self, unused_mock_get_utility):
self.auth.perform([self.achall])
@@ -56,7 +56,7 @@ def test_cleanup(self):
expected = [mock.call.del_txt_record(DOMAIN, '_acme-challenge.'+DOMAIN, mock.ANY)]
self.assertEqual(expected, self.mock_client.mock_calls)
- @test_util.patch_get_utility()
+ @test_util.patch_display_util()
def test_api_token(self, unused_mock_get_utility):
dns_test_common.write({"cloudflare_api_token": API_TOKEN},
self.config.cloudflare_credentials)
diff --git a/certbot-dns-cloudxns/certbot_dns_cloudxns/_internal/dns_cloudxns.py b/certbot-dns-cloudxns/certbot_dns_cloudxns/_internal/dns_cloudxns.py
index 1ec5cb5abab..d30e99a81b0 100644
--- a/certbot-dns-cloudxns/certbot_dns_cloudxns/_internal/dns_cloudxns.py
+++ b/certbot-dns-cloudxns/certbot_dns_cloudxns/_internal/dns_cloudxns.py
@@ -3,10 +3,8 @@
from typing import Optional
from lexicon.providers import cloudxns
-import zope.interface
from certbot import errors
-from certbot import interfaces
from certbot.plugins import dns_common
from certbot.plugins import dns_common_lexicon
from certbot.plugins.dns_common import CredentialsConfiguration
@@ -16,8 +14,6 @@
ACCOUNT_URL = 'https://www.cloudxns.net/en/AccountManage/apimanage.html'
-@zope.interface.implementer(interfaces.IAuthenticator)
-@zope.interface.provider(interfaces.IPluginFactory)
class Authenticator(dns_common.DNSAuthenticator):
"""DNS Authenticator for CloudXNS DNS
diff --git a/certbot-dns-cloudxns/setup.py b/certbot-dns-cloudxns/setup.py
index da667657a45..064b72be851 100644
--- a/certbot-dns-cloudxns/setup.py
+++ b/certbot-dns-cloudxns/setup.py
@@ -4,7 +4,7 @@
from setuptools import find_packages
from setuptools import setup
-version = '1.17.0.dev0'
+version = '1.18.0.dev0'
install_requires = [
'dns-lexicon>=3.1.0', # Changed `rtype` parameter name
diff --git a/certbot-dns-digitalocean/certbot_dns_digitalocean/_internal/dns_digitalocean.py b/certbot-dns-digitalocean/certbot_dns_digitalocean/_internal/dns_digitalocean.py
index 23b669847be..6e0e87a17e3 100644
--- a/certbot-dns-digitalocean/certbot_dns_digitalocean/_internal/dns_digitalocean.py
+++ b/certbot-dns-digitalocean/certbot_dns_digitalocean/_internal/dns_digitalocean.py
@@ -3,18 +3,14 @@
from typing import Optional
import digitalocean
-import zope.interface
from certbot import errors
-from certbot import interfaces
from certbot.plugins import dns_common
from certbot.plugins.dns_common import CredentialsConfiguration
logger = logging.getLogger(__name__)
-@zope.interface.implementer(interfaces.IAuthenticator)
-@zope.interface.provider(interfaces.IPluginFactory)
class Authenticator(dns_common.DNSAuthenticator):
"""DNS Authenticator for DigitalOcean
diff --git a/certbot-dns-digitalocean/setup.py b/certbot-dns-digitalocean/setup.py
index 5b59ab4acd2..3b80eb5918e 100644
--- a/certbot-dns-digitalocean/setup.py
+++ b/certbot-dns-digitalocean/setup.py
@@ -4,7 +4,7 @@
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
diff --git a/certbot-dns-digitalocean/tests/dns_digitalocean_test.py b/certbot-dns-digitalocean/tests/dns_digitalocean_test.py
index 07972bdde1b..4683893e80e 100644
--- a/certbot-dns-digitalocean/tests/dns_digitalocean_test.py
+++ b/certbot-dns-digitalocean/tests/dns_digitalocean_test.py
@@ -37,7 +37,7 @@ def setUp(self):
# _get_digitalocean_client | pylint: disable=protected-access
self.auth._get_digitalocean_client = mock.MagicMock(return_value=self.mock_client)
- @test_util.patch_get_utility()
+ @test_util.patch_display_util()
def test_perform(self, unused_mock_get_utility):
self.auth.perform([self.achall])
diff --git a/certbot-dns-dnsimple/certbot_dns_dnsimple/_internal/dns_dnsimple.py b/certbot-dns-dnsimple/certbot_dns_dnsimple/_internal/dns_dnsimple.py
index 858ee8925e1..1b0148137fe 100644
--- a/certbot-dns-dnsimple/certbot_dns_dnsimple/_internal/dns_dnsimple.py
+++ b/certbot-dns-dnsimple/certbot_dns_dnsimple/_internal/dns_dnsimple.py
@@ -3,10 +3,8 @@
from typing import Optional
from lexicon.providers import dnsimple
-import zope.interface
from certbot import errors
-from certbot import interfaces
from certbot.plugins import dns_common
from certbot.plugins import dns_common_lexicon
from certbot.plugins.dns_common import CredentialsConfiguration
@@ -16,8 +14,6 @@
ACCOUNT_URL = 'https://dnsimple.com/user'
-@zope.interface.implementer(interfaces.IAuthenticator)
-@zope.interface.provider(interfaces.IPluginFactory)
class Authenticator(dns_common.DNSAuthenticator):
"""DNS Authenticator for DNSimple
diff --git a/certbot-dns-dnsimple/setup.py b/certbot-dns-dnsimple/setup.py
index 5a1b13f4406..956a228bd83 100644
--- a/certbot-dns-dnsimple/setup.py
+++ b/certbot-dns-dnsimple/setup.py
@@ -4,7 +4,7 @@
from setuptools import find_packages
from setuptools import setup
-version = '1.17.0.dev0'
+version = '1.18.0.dev0'
install_requires = [
'setuptools>=39.0.1',
diff --git a/certbot-dns-dnsmadeeasy/certbot_dns_dnsmadeeasy/_internal/dns_dnsmadeeasy.py b/certbot-dns-dnsmadeeasy/certbot_dns_dnsmadeeasy/_internal/dns_dnsmadeeasy.py
index 67903e19d49..628140d0605 100644
--- a/certbot-dns-dnsmadeeasy/certbot_dns_dnsmadeeasy/_internal/dns_dnsmadeeasy.py
+++ b/certbot-dns-dnsmadeeasy/certbot_dns_dnsmadeeasy/_internal/dns_dnsmadeeasy.py
@@ -3,10 +3,8 @@
from typing import Optional
from lexicon.providers import dnsmadeeasy
-import zope.interface
from certbot import errors
-from certbot import interfaces
from certbot.plugins import dns_common
from certbot.plugins import dns_common_lexicon
from certbot.plugins.dns_common import CredentialsConfiguration
@@ -16,8 +14,6 @@
ACCOUNT_URL = 'https://cp.dnsmadeeasy.com/account/info'
-@zope.interface.implementer(interfaces.IAuthenticator)
-@zope.interface.provider(interfaces.IPluginFactory)
class Authenticator(dns_common.DNSAuthenticator):
"""DNS Authenticator for DNS Made Easy
diff --git a/certbot-dns-dnsmadeeasy/setup.py b/certbot-dns-dnsmadeeasy/setup.py
index e0ff95a3ba1..0cccf9a14ae 100644
--- a/certbot-dns-dnsmadeeasy/setup.py
+++ b/certbot-dns-dnsmadeeasy/setup.py
@@ -4,7 +4,7 @@
from setuptools import find_packages
from setuptools import setup
-version = '1.17.0.dev0'
+version = '1.18.0.dev0'
install_requires = [
'dns-lexicon>=3.1.0', # Changed `rtype` parameter name
diff --git a/certbot-dns-gehirn/certbot_dns_gehirn/_internal/dns_gehirn.py b/certbot-dns-gehirn/certbot_dns_gehirn/_internal/dns_gehirn.py
index 57ff01671d3..4fc5d8a92a1 100644
--- a/certbot-dns-gehirn/certbot_dns_gehirn/_internal/dns_gehirn.py
+++ b/certbot-dns-gehirn/certbot_dns_gehirn/_internal/dns_gehirn.py
@@ -3,10 +3,8 @@
from typing import Optional
from lexicon.providers import gehirn
-import zope.interface
from certbot import errors
-from certbot import interfaces
from certbot.plugins import dns_common
from certbot.plugins import dns_common_lexicon
from certbot.plugins.dns_common import CredentialsConfiguration
@@ -15,8 +13,7 @@
DASHBOARD_URL = "https://gis.gehirn.jp/"
-@zope.interface.implementer(interfaces.IAuthenticator)
-@zope.interface.provider(interfaces.IPluginFactory)
+
class Authenticator(dns_common.DNSAuthenticator):
"""DNS Authenticator for Gehirn Infrastructure Service DNS
diff --git a/certbot-dns-gehirn/setup.py b/certbot-dns-gehirn/setup.py
index 997cd4cca0e..1bb2afb6195 100644
--- a/certbot-dns-gehirn/setup.py
+++ b/certbot-dns-gehirn/setup.py
@@ -4,7 +4,7 @@
from setuptools import find_packages
from setuptools import setup
-version = '1.17.0.dev0'
+version = '1.18.0.dev0'
install_requires = [
'dns-lexicon>=3.1.0', # Changed `rtype` parameter name
diff --git a/certbot-dns-google/certbot_dns_google/_internal/dns_google.py b/certbot-dns-google/certbot_dns_google/_internal/dns_google.py
index 3a2686a6301..8ddbbba65e7 100644
--- a/certbot-dns-google/certbot_dns_google/_internal/dns_google.py
+++ b/certbot-dns-google/certbot_dns_google/_internal/dns_google.py
@@ -6,10 +6,8 @@
from googleapiclient import errors as googleapiclient_errors
import httplib2
from oauth2client.service_account import ServiceAccountCredentials
-import zope.interface
from certbot import errors
-from certbot import interfaces
from certbot.plugins import dns_common
logger = logging.getLogger(__name__)
@@ -20,8 +18,6 @@
METADATA_HEADERS = {'Metadata-Flavor': 'Google'}
-@zope.interface.implementer(interfaces.IAuthenticator)
-@zope.interface.provider(interfaces.IPluginFactory)
class Authenticator(dns_common.DNSAuthenticator):
"""DNS Authenticator for Google Cloud DNS
diff --git a/certbot-dns-google/setup.py b/certbot-dns-google/setup.py
index fd3bb45595a..04b1ac8de2f 100644
--- a/certbot-dns-google/setup.py
+++ b/certbot-dns-google/setup.py
@@ -4,7 +4,7 @@
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',
diff --git a/certbot-dns-google/tests/dns_google_test.py b/certbot-dns-google/tests/dns_google_test.py
index 83fa29b414b..dc5bc1bf15a 100644
--- a/certbot-dns-google/tests/dns_google_test.py
+++ b/certbot-dns-google/tests/dns_google_test.py
@@ -43,7 +43,7 @@ def setUp(self):
# _get_google_client | pylint: disable=protected-access
self.auth._get_google_client = mock.MagicMock(return_value=self.mock_client)
- @test_util.patch_get_utility()
+ @test_util.patch_display_util()
def test_perform(self, unused_mock_get_utility):
self.auth.perform([self.achall])
@@ -59,7 +59,7 @@ def test_cleanup(self):
self.assertEqual(expected, self.mock_client.mock_calls)
@mock.patch('httplib2.Http.request', side_effect=ServerNotFoundError)
- @test_util.patch_get_utility()
+ @test_util.patch_display_util()
def test_without_auth(self, unused_mock_get_utility, unused_mock):
self.config.google_credentials = None
self.assertRaises(PluginError, self.auth.perform, [self.achall])
diff --git a/certbot-dns-linode/certbot_dns_linode/_internal/dns_linode.py b/certbot-dns-linode/certbot_dns_linode/_internal/dns_linode.py
index b1649cf6153..003871a7512 100644
--- a/certbot-dns-linode/certbot_dns_linode/_internal/dns_linode.py
+++ b/certbot-dns-linode/certbot_dns_linode/_internal/dns_linode.py
@@ -5,10 +5,8 @@
from lexicon.providers import linode
from lexicon.providers import linode4
-import zope.interface
from certbot import errors
-from certbot import interfaces
from certbot.plugins import dns_common
from certbot.plugins import dns_common_lexicon
from certbot.plugins.dns_common import CredentialsConfiguration
@@ -18,8 +16,7 @@
API_KEY_URL = 'https://manager.linode.com/profile/api'
API_KEY_URL_V4 = 'https://cloud.linode.com/profile/tokens'
-@zope.interface.implementer(interfaces.IAuthenticator)
-@zope.interface.provider(interfaces.IPluginFactory)
+
class Authenticator(dns_common.DNSAuthenticator):
"""DNS Authenticator for Linode
diff --git a/certbot-dns-linode/setup.py b/certbot-dns-linode/setup.py
index 2c21ceecbf9..cf7996b0588 100644
--- a/certbot-dns-linode/setup.py
+++ b/certbot-dns-linode/setup.py
@@ -4,7 +4,7 @@
from setuptools import find_packages
from setuptools import setup
-version = '1.17.0.dev0'
+version = '1.18.0.dev0'
install_requires = [
'dns-lexicon>=3.1.0', # Changed `rtype` parameter name
diff --git a/certbot-dns-luadns/certbot_dns_luadns/_internal/dns_luadns.py b/certbot-dns-luadns/certbot_dns_luadns/_internal/dns_luadns.py
index ed90b63d9c6..bf4ddc4967a 100644
--- a/certbot-dns-luadns/certbot_dns_luadns/_internal/dns_luadns.py
+++ b/certbot-dns-luadns/certbot_dns_luadns/_internal/dns_luadns.py
@@ -3,10 +3,8 @@
from typing import Optional
from lexicon.providers import luadns
-import zope.interface
from certbot import errors
-from certbot import interfaces
from certbot.plugins import dns_common
from certbot.plugins import dns_common_lexicon
from certbot.plugins.dns_common import CredentialsConfiguration
@@ -16,8 +14,6 @@
ACCOUNT_URL = 'https://api.luadns.com/settings'
-@zope.interface.implementer(interfaces.IAuthenticator)
-@zope.interface.provider(interfaces.IPluginFactory)
class Authenticator(dns_common.DNSAuthenticator):
"""DNS Authenticator for LuaDNS
diff --git a/certbot-dns-luadns/setup.py b/certbot-dns-luadns/setup.py
index 024b1100bb9..c3791a375e7 100644
--- a/certbot-dns-luadns/setup.py
+++ b/certbot-dns-luadns/setup.py
@@ -4,7 +4,7 @@
from setuptools import find_packages
from setuptools import setup
-version = '1.17.0.dev0'
+version = '1.18.0.dev0'
install_requires = [
'dns-lexicon>=3.1.0', # Changed `rtype` parameter name
diff --git a/certbot-dns-nsone/certbot_dns_nsone/_internal/dns_nsone.py b/certbot-dns-nsone/certbot_dns_nsone/_internal/dns_nsone.py
index ce46ad83506..a66e0986870 100644
--- a/certbot-dns-nsone/certbot_dns_nsone/_internal/dns_nsone.py
+++ b/certbot-dns-nsone/certbot_dns_nsone/_internal/dns_nsone.py
@@ -3,10 +3,8 @@
from typing import Optional
from lexicon.providers import nsone
-import zope.interface
from certbot import errors
-from certbot import interfaces
from certbot.plugins import dns_common
from certbot.plugins import dns_common_lexicon
from certbot.plugins.dns_common import CredentialsConfiguration
@@ -16,8 +14,6 @@
ACCOUNT_URL = 'https://my.nsone.net/#/account/settings'
-@zope.interface.implementer(interfaces.IAuthenticator)
-@zope.interface.provider(interfaces.IPluginFactory)
class Authenticator(dns_common.DNSAuthenticator):
"""DNS Authenticator for NS1
diff --git a/certbot-dns-nsone/setup.py b/certbot-dns-nsone/setup.py
index 959c3876dd9..e59eaeb6ff2 100644
--- a/certbot-dns-nsone/setup.py
+++ b/certbot-dns-nsone/setup.py
@@ -4,7 +4,7 @@
from setuptools import find_packages
from setuptools import setup
-version = '1.17.0.dev0'
+version = '1.18.0.dev0'
install_requires = [
'dns-lexicon>=3.1.0', # Changed `rtype` parameter name
diff --git a/certbot-dns-ovh/certbot_dns_ovh/_internal/dns_ovh.py b/certbot-dns-ovh/certbot_dns_ovh/_internal/dns_ovh.py
index 54fd6d7918e..4c46da0e8f9 100644
--- a/certbot-dns-ovh/certbot_dns_ovh/_internal/dns_ovh.py
+++ b/certbot-dns-ovh/certbot_dns_ovh/_internal/dns_ovh.py
@@ -3,10 +3,8 @@
from typing import Optional
from lexicon.providers import ovh
-import zope.interface
from certbot import errors
-from certbot import interfaces
from certbot.plugins import dns_common
from certbot.plugins import dns_common_lexicon
from certbot.plugins.dns_common import CredentialsConfiguration
@@ -16,8 +14,6 @@
TOKEN_URL = 'https://eu.api.ovh.com/createToken/ or https://ca.api.ovh.com/createToken/'
-@zope.interface.implementer(interfaces.IAuthenticator)
-@zope.interface.provider(interfaces.IPluginFactory)
class Authenticator(dns_common.DNSAuthenticator):
"""DNS Authenticator for OVH
diff --git a/certbot-dns-ovh/setup.py b/certbot-dns-ovh/setup.py
index 52dca456bd1..b3eca861d40 100644
--- a/certbot-dns-ovh/setup.py
+++ b/certbot-dns-ovh/setup.py
@@ -4,7 +4,7 @@
from setuptools import find_packages
from setuptools import setup
-version = '1.17.0.dev0'
+version = '1.18.0.dev0'
install_requires = [
'dns-lexicon>=3.1.0', # Changed `rtype` parameter name
diff --git a/certbot-dns-rfc2136/certbot_dns_rfc2136/_internal/dns_rfc2136.py b/certbot-dns-rfc2136/certbot_dns_rfc2136/_internal/dns_rfc2136.py
index 28fd27eec07..77007a971f4 100644
--- a/certbot-dns-rfc2136/certbot_dns_rfc2136/_internal/dns_rfc2136.py
+++ b/certbot-dns-rfc2136/certbot_dns_rfc2136/_internal/dns_rfc2136.py
@@ -11,10 +11,8 @@
import dns.tsig
import dns.tsigkeyring
import dns.update
-import zope.interface
from certbot import errors
-from certbot import interfaces
from certbot.plugins import dns_common
from certbot.plugins.dns_common import CredentialsConfiguration
@@ -22,12 +20,11 @@
DEFAULT_NETWORK_TIMEOUT = 45
-@zope.interface.implementer(interfaces.IAuthenticator)
-@zope.interface.provider(interfaces.IPluginFactory)
+
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 = {
diff --git a/certbot-dns-rfc2136/setup.py b/certbot-dns-rfc2136/setup.py
index 25f0f2bfea2..e8bd0db6e09 100644
--- a/certbot-dns-rfc2136/setup.py
+++ b/certbot-dns-rfc2136/setup.py
@@ -4,7 +4,7 @@
from setuptools import find_packages
from setuptools import setup
-version = '1.17.0.dev0'
+version = '1.18.0.dev0'
install_requires = [
'dnspython',
diff --git a/certbot-dns-rfc2136/tests/dns_rfc2136_test.py b/certbot-dns-rfc2136/tests/dns_rfc2136_test.py
index de77c1bcc4c..ec424c6d997 100644
--- a/certbot-dns-rfc2136/tests/dns_rfc2136_test.py
+++ b/certbot-dns-rfc2136/tests/dns_rfc2136_test.py
@@ -42,7 +42,7 @@ def setUp(self):
# _get_rfc2136_client | pylint: disable=protected-access
self.auth._get_rfc2136_client = mock.MagicMock(return_value=self.mock_client)
- @test_util.patch_get_utility()
+ @test_util.patch_display_util()
def test_perform(self, unused_mock_get_utility):
self.auth.perform([self.achall])
@@ -66,7 +66,7 @@ def test_invalid_algorithm_raises(self):
self.auth.perform,
[self.achall])
- @test_util.patch_get_utility()
+ @test_util.patch_display_util()
def test_valid_algorithm_passes(self, unused_mock_get_utility):
config = VALID_CONFIG.copy()
config["rfc2136_algorithm"] = "HMAC-sha512"
diff --git a/certbot-dns-route53/certbot_dns_route53/_internal/dns_route53.py b/certbot-dns-route53/certbot_dns_route53/_internal/dns_route53.py
index 9e470bddef2..40b25c1838f 100644
--- a/certbot-dns-route53/certbot_dns_route53/_internal/dns_route53.py
+++ b/certbot-dns-route53/certbot_dns_route53/_internal/dns_route53.py
@@ -9,10 +9,8 @@
import boto3
from botocore.exceptions import ClientError
from botocore.exceptions import NoCredentialsError
-import zope.interface
from certbot import errors
-from certbot import interfaces
from certbot.plugins import dns_common
logger = logging.getLogger(__name__)
@@ -23,8 +21,6 @@
"and add the necessary permissions for Route53 access.")
-@zope.interface.implementer(interfaces.IAuthenticator)
-@zope.interface.provider(interfaces.IPluginFactory)
class Authenticator(dns_common.DNSAuthenticator):
"""Route53 Authenticator
diff --git a/certbot-dns-route53/certbot_dns_route53/authenticator.py b/certbot-dns-route53/certbot_dns_route53/authenticator.py
index 060d2fa38d0..b55e46902e8 100644
--- a/certbot-dns-route53/certbot_dns_route53/authenticator.py
+++ b/certbot-dns-route53/certbot_dns_route53/authenticator.py
@@ -1,14 +1,9 @@
"""Shim around `~certbot_dns_route53._internal.dns_route53` for backwards compatibility."""
import warnings
-import zope.interface
-
-from certbot import interfaces
from certbot_dns_route53._internal import dns_route53
-@zope.interface.implementer(interfaces.IAuthenticator)
-@zope.interface.provider(interfaces.IPluginFactory)
class Authenticator(dns_route53.Authenticator):
"""Shim around `~certbot_dns_route53._internal.dns_route53.Authenticator`
for backwards compatibility."""
diff --git a/certbot-dns-route53/setup.py b/certbot-dns-route53/setup.py
index 90069b3e237..54a46596f4e 100644
--- a/certbot-dns-route53/setup.py
+++ b/certbot-dns-route53/setup.py
@@ -4,7 +4,7 @@
from setuptools import find_packages
from setuptools import setup
-version = '1.17.0.dev0'
+version = '1.18.0.dev0'
install_requires = [
'boto3',
diff --git a/certbot-dns-sakuracloud/certbot_dns_sakuracloud/_internal/dns_sakuracloud.py b/certbot-dns-sakuracloud/certbot_dns_sakuracloud/_internal/dns_sakuracloud.py
index 668c5e1f8ab..3990ad63497 100644
--- a/certbot-dns-sakuracloud/certbot_dns_sakuracloud/_internal/dns_sakuracloud.py
+++ b/certbot-dns-sakuracloud/certbot_dns_sakuracloud/_internal/dns_sakuracloud.py
@@ -3,10 +3,8 @@
from typing import Optional
from lexicon.providers import sakuracloud
-import zope.interface
from certbot import errors
-from certbot import interfaces
from certbot.plugins import dns_common
from certbot.plugins import dns_common_lexicon
from certbot.plugins.dns_common import CredentialsConfiguration
@@ -16,8 +14,6 @@
APIKEY_URL = "https://secure.sakura.ad.jp/cloud/#!/apikey/top/"
-@zope.interface.implementer(interfaces.IAuthenticator)
-@zope.interface.provider(interfaces.IPluginFactory)
class Authenticator(dns_common.DNSAuthenticator):
"""DNS Authenticator for Sakura Cloud DNS
diff --git a/certbot-dns-sakuracloud/setup.py b/certbot-dns-sakuracloud/setup.py
index 8f98058f0cd..133a910aafc 100644
--- a/certbot-dns-sakuracloud/setup.py
+++ b/certbot-dns-sakuracloud/setup.py
@@ -4,7 +4,7 @@
from setuptools import find_packages
from setuptools import setup
-version = '1.17.0.dev0'
+version = '1.18.0.dev0'
install_requires = [
'dns-lexicon>=3.1.0', # Changed `rtype` parameter name
diff --git a/certbot-nginx/certbot_nginx/_internal/configurator.py b/certbot-nginx/certbot_nginx/_internal/configurator.py
index 07397bfe8a7..66136ba70c1 100644
--- a/certbot-nginx/certbot_nginx/_internal/configurator.py
+++ b/certbot-nginx/certbot_nginx/_internal/configurator.py
@@ -16,7 +16,6 @@
import OpenSSL
import pkg_resources
-import zope.interface
from acme import challenges
from acme import crypto_util as acme_crypto_util
@@ -44,9 +43,7 @@
logger = logging.getLogger(__name__)
-@zope.interface.implementer(interfaces.IAuthenticator, interfaces.IInstaller)
-@zope.interface.provider(interfaces.IPluginFactory)
-class NginxConfigurator(common.Installer):
+class NginxConfigurator(common.Installer, interfaces.Authenticator):
"""Nginx configurator.
.. todo:: Add proper support for comments in the config. Currently,
@@ -769,9 +766,6 @@ def enhance(self, domain, enhancement, options=None):
except (KeyError, ValueError):
raise errors.PluginError(
"Unsupported enhancement: {0}".format(enhancement))
- except errors.PluginError:
- logger.error("Failed %s for %s", enhancement, domain)
- raise
def _has_certbot_redirect(self, vhost, domain):
test_redirect_block = _test_block_from_block(_redirect_block_for_domain(domain))
@@ -791,6 +785,10 @@ def _set_http_header(self, domain, header_substring):
:raises .errors.PluginError: If no viable HTTPS host can be created or
set with header header_substring.
"""
+ if not header_substring in constants.HEADER_ARGS:
+ raise errors.NotSupportedError(
+ f"{header_substring} is not supported by the nginx plugin.")
+
vhosts = self.choose_vhosts(domain)
if not vhosts:
raise errors.PluginError(
diff --git a/certbot-nginx/certbot_nginx/_internal/display_ops.py b/certbot-nginx/certbot_nginx/_internal/display_ops.py
index 356cc506c8d..95163a81fcf 100644
--- a/certbot-nginx/certbot_nginx/_internal/display_ops.py
+++ b/certbot-nginx/certbot_nginx/_internal/display_ops.py
@@ -1,10 +1,7 @@
"""Contains UI methods for Nginx operations."""
import logging
-import zope.component
-
-from certbot import interfaces
-import certbot.display.util as display_util
+from certbot.display import util as display_util
logger = logging.getLogger(__name__)
@@ -22,7 +19,7 @@ def select_vhost_multiple(vhosts):
# Remove the extra newline from the last entry
if tags_list:
tags_list[-1] = tags_list[-1][:-1]
- code, names = zope.component.getUtility(interfaces.IDisplay).checklist(
+ code, names = display_util.checklist(
"Which server blocks would you like to modify?",
tags=tags_list, force_interactive=True)
if code == display_util.OK:
@@ -30,6 +27,7 @@ def select_vhost_multiple(vhosts):
return return_vhosts
return []
+
def _reversemap_vhosts(names, vhosts):
"""Helper function for select_vhost_multiple for mapping string
representations back to actual vhost objects"""
diff --git a/certbot-nginx/certbot_nginx/_internal/nginxparser.py b/certbot-nginx/certbot_nginx/_internal/nginxparser.py
index 787f7c363f2..2aa677c3801 100644
--- a/certbot-nginx/certbot_nginx/_internal/nginxparser.py
+++ b/certbot-nginx/certbot_nginx/_internal/nginxparser.py
@@ -9,7 +9,6 @@
from pyparsing import Forward
from pyparsing import Group
from pyparsing import Literal
-from pyparsing import OneOrMore
from pyparsing import Optional
from pyparsing import QuotedString
from pyparsing import Regex
@@ -57,7 +56,7 @@ class RawNginxParser:
block_innards = Group(ZeroOrMore(contents) + space).leaveWhitespace()
block << block_begin + left_bracket + block_innards + right_bracket
- script = OneOrMore(contents) + space + stringEnd
+ script = ZeroOrMore(contents) + space + stringEnd
script.parseWithTabs().leaveWhitespace()
def __init__(self, source):
diff --git a/certbot-nginx/certbot_nginx/_internal/parser.py b/certbot-nginx/certbot_nginx/_internal/parser.py
index a9a48407cb0..83ec4c92396 100644
--- a/certbot-nginx/certbot_nginx/_internal/parser.py
+++ b/certbot-nginx/certbot_nginx/_internal/parser.py
@@ -96,8 +96,8 @@ def _build_addr_to_ssl(self):
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 @@ def _get_raw_servers(self) -> Dict:
"""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 @@ def get_vhosts(self):
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 @@ def filedump(self, ext='tmp', lazy=True):
"""
# 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):
diff --git a/certbot-nginx/setup.py b/certbot-nginx/setup.py
index 371df2a3b9d..67e326f31d2 100644
--- a/certbot-nginx/setup.py
+++ b/certbot-nginx/setup.py
@@ -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
diff --git a/certbot-nginx/tests/configurator_test.py b/certbot-nginx/tests/configurator_test.py
index fae5d41d9fa..e667d5375d7 100644
--- a/certbot-nginx/tests/configurator_test.py
+++ b/certbot-nginx/tests/configurator_test.py
@@ -43,7 +43,7 @@ def test_prepare_no_install(self, mock_exe_exists):
def test_prepare(self):
self.assertEqual((1, 6, 2), self.config.version)
- self.assertEqual(13, len(self.config.parser.parsed))
+ self.assertEqual(14, len(self.config.parser.parsed))
@mock.patch("certbot_nginx._internal.configurator.util.exe_exists")
@mock.patch("certbot_nginx._internal.configurator.subprocess.run")
diff --git a/certbot-nginx/tests/display_ops_test.py b/certbot-nginx/tests/display_ops_test.py
index bcd455410e1..7480a1109b2 100644
--- a/certbot-nginx/tests/display_ops_test.py
+++ b/certbot-nginx/tests/display_ops_test.py
@@ -19,7 +19,7 @@ def setUp(self):
def test_select_no_input(self):
self.assertFalse(select_vhost_multiple([]))
- @certbot_util.patch_get_utility()
+ @certbot_util.patch_display_util()
def test_select_correct(self, mock_util):
mock_util().checklist.return_value = (
display_util.OK, [self.vhosts[3].display_repr(),
@@ -31,7 +31,7 @@ def test_select_correct(self, mock_util):
self.assertTrue(self.vhosts[3] in vhs)
self.assertFalse(self.vhosts[1] in vhs)
- @certbot_util.patch_get_utility()
+ @certbot_util.patch_display_util()
def test_select_cancel(self, mock_util):
mock_util().checklist.return_value = (display_util.CANCEL, "whatever")
vhs = select_vhost_multiple([self.vhosts[2], self.vhosts[3]])
diff --git a/certbot-nginx/tests/nginxparser_test.py b/certbot-nginx/tests/nginxparser_test.py
index a5212078f35..8f7d5accfe9 100644
--- a/certbot-nginx/tests/nginxparser_test.py
+++ b/certbot-nginx/tests/nginxparser_test.py
@@ -350,6 +350,10 @@ def test_edge_cases(self):
self.assertEqual(loads("blag${dfgdfg};"), [['blag${dfgdfg}']])
self.assertRaises(ParseException, loads, "blag${dfgdf{g};")
+ # empty file
+ parsed = loads("")
+ self.assertEqual(parsed, [])
+
class TestUnspacedList(unittest.TestCase):
"""Test the UnspacedList data structure"""
diff --git a/certbot-nginx/tests/parser_test.py b/certbot-nginx/tests/parser_test.py
index b062c4196c9..9047cb44615 100644
--- a/certbot-nginx/tests/parser_test.py
+++ b/certbot-nginx/tests/parser_test.py
@@ -50,7 +50,7 @@ def test_load(self):
nparser = parser.NginxParser(self.config_path)
nparser.load()
self.assertEqual({nparser.abs_path(x) for x in
- ['foo.conf', 'nginx.conf', 'server.conf',
+ ['foo.conf', 'nginx.conf', 'server.conf', 'mime.types',
'sites-enabled/default',
'sites-enabled/both.com',
'sites-enabled/example.com',
@@ -89,7 +89,7 @@ def test_filedump(self):
# pylint: disable=protected-access
parsed = nparser._parse_files(nparser.abs_path(
'sites-enabled/example.com.test'))
- self.assertEqual(3, len(glob.glob(nparser.abs_path('*.test'))))
+ self.assertEqual(4, len(glob.glob(nparser.abs_path('*.test'))))
self.assertEqual(10, len(
glob.glob(nparser.abs_path('sites-enabled/*.test'))))
self.assertEqual([[['server'], [['listen', '69.50.225.155:9000'],
diff --git a/certbot-nginx/tests/test_util.py b/certbot-nginx/tests/test_util.py
index 97fe05af0d5..6cc701f42c8 100644
--- a/certbot-nginx/tests/test_util.py
+++ b/certbot-nginx/tests/test_util.py
@@ -6,8 +6,8 @@
import josepy as jose
try:
import mock
-except ImportError: # pragma: no cover
- from unittest import mock # type: ignore
+except ImportError: # pragma: no cover
+ from unittest import mock # type: ignore
import pkg_resources
from certbot import util
diff --git a/certbot/CHANGELOG.md b/certbot/CHANGELOG.md
index 04326ef305e..c13b42d3895 100644
--- a/certbot/CHANGELOG.md
+++ b/certbot/CHANGELOG.md
@@ -2,11 +2,39 @@
Certbot adheres to [Semantic Versioning](https://semver.org/).
-## 1.17.0 - master
+## 1.18.0 - master
### Added
-*
+* New functions that Certbot plugins can use to interact with the user have
+ been added to `certbot.display.util`. We plan to deprecate using `IDisplay`
+ with `zope` in favor of these new functions in the future.
+
+### 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`.
+* The `certbot.tests.patch_get_utility*` functions have been deprecated.
+ Plugins should now patch `certbot.display.util` themselves in their tests or
+ use `certbot.tests.util.patch_display_util` as a temporary workaround.
+
+### 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
@@ -16,10 +44,16 @@ Certbot adheres to [Semantic Versioning](https://semver.org/).
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
-*
+* When we increased the logging level on our nginx "Could not parse file" message,
+ it caused a previously-existing inability to parse empty files to become more
+ visible. We have now added the ability to correctly parse empty files, so that
+ message should only show for more significant errors.
More details about these changes can be found on our GitHub repo.
diff --git a/certbot/certbot/__init__.py b/certbot/certbot/__init__.py
index 4d97c9fdab1..dbe457d0fe0 100644
--- a/certbot/certbot/__init__.py
+++ b/certbot/certbot/__init__.py
@@ -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'
diff --git a/certbot/certbot/_internal/account.py b/certbot/certbot/_internal/account.py
index c5667a865fb..a250347caa8 100644
--- a/certbot/certbot/_internal/account.py
+++ b/certbot/certbot/_internal/account.py
@@ -311,8 +311,8 @@ def _delete_links_and_find_target_dir(self, server_path, link_func):
# 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
diff --git a/certbot/certbot/_internal/auth_handler.py b/certbot/certbot/_internal/auth_handler.py
index 41ae492e249..e578ff25bdc 100644
--- a/certbot/certbot/_internal/auth_handler.py
+++ b/certbot/certbot/_internal/auth_handler.py
@@ -6,14 +6,11 @@
from typing import List
from typing import Tuple
-import zope.component
-
from acme import challenges
from acme import errors as acme_errors
from acme import messages
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
@@ -74,9 +71,9 @@ def handle_authorizations(self, orderr, config, best_effort=False, max_retries=3
# If debug is on, wait for user input before starting the verification process.
if config.debug_challenges:
- notify = zope.component.getUtility(interfaces.IDisplay).notification
- notify('Challenges loaded. Press continue to submit to CA. '
- 'Pass "-v" for more info about challenges.', pause=True)
+ display_util.notification(
+ 'Challenges loaded. Press continue to submit to CA. '
+ 'Pass "-v" for more info about challenges.', pause=True)
except errors.AuthorizationError as error:
logger.critical('Failure in setting up challenges.')
logger.info('Attempting to clean up outstanding challenges...')
diff --git a/certbot/certbot/_internal/cert_manager.py b/certbot/certbot/_internal/cert_manager.py
index b9b4ad2d7f0..d312fe8b896 100644
--- a/certbot/certbot/_internal/cert_manager.py
+++ b/certbot/certbot/_internal/cert_manager.py
@@ -6,11 +6,9 @@
from typing import List
import pytz
-import zope.component
from certbot import crypto_util
from certbot import errors
-from certbot import interfaces
from certbot import ocsp
from certbot import util
from certbot._internal import storage
@@ -23,6 +21,7 @@
# Commands
###################
+
def update_live_symlinks(config):
"""Update the certificate file family symlinks to use archive_dir.
@@ -38,6 +37,7 @@ def update_live_symlinks(config):
for renewal_file in storage.renewal_conf_files(config):
storage.RenewableCert(renewal_file, config, update_symlinks=True)
+
def rename_lineage(config):
"""Rename the specified lineage to the new name.
@@ -45,15 +45,13 @@ def rename_lineage(config):
:type config: :class:`certbot._internal.configuration.NamespaceConfig`
"""
- disp = zope.component.getUtility(interfaces.IDisplay)
-
certname = get_certnames(config, "rename")[0]
new_certname = config.new_certname
if not new_certname:
- code, new_certname = disp.input(
+ code, new_certname = display_util.input_text(
"Enter the new name for certificate {0}".format(certname),
- flag="--updated-cert-name", force_interactive=True)
+ force_interactive=True)
if code != display_util.OK or not new_certname:
raise errors.Error("User ended interaction.")
@@ -62,8 +60,8 @@ def rename_lineage(config):
raise errors.ConfigurationError("No existing certificate with name "
"{0} found.".format(certname))
storage.rename_renewal_config(certname, new_certname, config)
- disp.notification("Successfully renamed {0} to {1}."
- .format(certname, new_certname), pause=False)
+ display_util.notification("Successfully renamed {0} to {1}."
+ .format(certname, new_certname), pause=False)
def certificates(config):
@@ -92,12 +90,11 @@ def certificates(config):
def delete(config):
"""Delete Certbot files associated with a certificate lineage."""
certnames = get_certnames(config, "delete", allow_multiple=True)
- disp = zope.component.getUtility(interfaces.IDisplay)
msg = ["The following certificate(s) are selected for deletion:\n"]
for certname in certnames:
msg.append(" * " + certname)
msg.append("\nAre you sure you want to delete the above certificate(s)?")
- if not disp.yesno("\n".join(msg), default=True):
+ if not display_util.yesno("\n".join(msg), default=True):
logger.info("Deletion of certificate(s) canceled.")
return
for certname in certnames:
@@ -310,13 +307,11 @@ def human_readable_cert_info(config, cert, skip_filter_checks=False):
def get_certnames(config, verb, allow_multiple=False, custom_prompt=None):
- """Get certname from flag, interactively, or error out.
- """
+ """Get certname from flag, interactively, or error out."""
certname = config.certname
if certname:
certnames = [certname]
else:
- disp = zope.component.getUtility(interfaces.IDisplay)
filenames = storage.renewal_conf_files(config)
choices = [storage.lineagename_for_filename(name) for name in filenames]
if not choices:
@@ -326,7 +321,7 @@ def get_certnames(config, verb, allow_multiple=False, custom_prompt=None):
prompt = "Which certificate(s) would you like to {0}?".format(verb)
else:
prompt = custom_prompt
- code, certnames = disp.checklist(
+ code, certnames = display_util.checklist(
prompt, choices, cli_flag="--cert-name", force_interactive=True)
if code != display_util.OK:
raise errors.Error("User ended interaction.")
@@ -336,7 +331,7 @@ def get_certnames(config, verb, allow_multiple=False, custom_prompt=None):
else:
prompt = custom_prompt
- code, index = disp.menu(
+ code, index = display_util.menu(
prompt, choices, cli_flag="--cert-name", force_interactive=True)
if code != display_util.OK or index not in range(0, len(choices)):
@@ -382,8 +377,7 @@ def _describe_certs(config, parsed_certs, parse_failures):
"were invalid:")
notify(_report_lines(parse_failures))
- disp = zope.component.getUtility(interfaces.IDisplay)
- disp.notification("\n".join(out), pause=False, wrap=False)
+ display_util.notification("\n".join(out), pause=False, wrap=False)
def _search_lineages(cli_config, func, initial_rv, *args):
diff --git a/certbot/certbot/_internal/cli/__init__.py b/certbot/certbot/_internal/cli/__init__.py
index 7d53ad6499b..4212c353b19 100644
--- a/certbot/certbot/_internal/cli/__init__.py
+++ b/certbot/certbot/_internal/cli/__init__.py
@@ -40,9 +40,9 @@
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__)
@@ -71,6 +71,11 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False):
default=flag_default("verbose_count"), help="This flag can be used "
"multiple times to incrementally increase the verbosity of output, "
"e.g. -vvv.")
+ # This is for developers to set the level in the cli.ini, and overrides
+ # the --verbose flag
+ helpful.add(
+ None, "--verbose-level", dest="verbose_level",
+ default=flag_default("verbose_level"), help=argparse.SUPPRESS)
helpful.add(
None, "-t", "--text", dest="text_mode", action="store_true",
default=flag_default("text_mode"), help=argparse.SUPPRESS)
@@ -449,6 +454,7 @@ def set_by_cli(var):
plugins = plugins_disco.PluginsRegistry.find_all()
# reconstructed_args == sys.argv[1:], or whatever was passed to main()
reconstructed_args = helpful_parser.args + [helpful_parser.verb]
+
detector = set_by_cli.detector = prepare_and_parse_args( # type: ignore
plugins, reconstructed_args, detect_defaults=True)
# propagate plugin requests: eg --standalone modifies config.authenticator
diff --git a/certbot/certbot/_internal/cli/cli_utils.py b/certbot/certbot/_internal/cli/cli_utils.py
index 8060b5e2148..50e093310da 100644
--- a/certbot/certbot/_internal/cli/cli_utils.py
+++ b/certbot/certbot/_internal/cli/cli_utils.py
@@ -1,8 +1,7 @@
"""Certbot command line util function"""
import argparse
import copy
-
-import zope.interface.interface # pylint: disable=unused-import
+import inspect
from acme import challenges
from certbot import errors
@@ -59,11 +58,10 @@ def flag_default(name):
def config_help(name, hidden=False):
- """Extract the help message for an `.IConfig` attribute."""
+ """Extract the help message for an `.Config` property docstring."""
if hidden:
return argparse.SUPPRESS
- field: zope.interface.interface.Attribute = interfaces.IConfig.__getitem__(name)
- return field.__doc__
+ return inspect.getdoc(getattr(interfaces.Config, name))
class HelpfulArgumentGroup:
diff --git a/certbot/certbot/_internal/cli/helpful.py b/certbot/certbot/_internal/cli/helpful.py
index 73872584cd1..0848829c407 100644
--- a/certbot/certbot/_internal/cli/helpful.py
+++ b/certbot/certbot/_internal/cli/helpful.py
@@ -9,13 +9,9 @@
from typing import Dict
import configargparse
-import zope.component
-import zope.interface
-from zope.interface import interfaces as zope_interfaces
from certbot import crypto_util
from certbot import errors
-from certbot import interfaces
from certbot import util
from certbot._internal import constants
from certbot._internal import hooks
@@ -32,8 +28,8 @@
from certbot._internal.cli.cli_utils import HelpfulArgumentGroup
from certbot._internal.cli.verb_help import VERB_HELP
from certbot._internal.cli.verb_help import VERB_HELP_MAP
+from certbot._internal.display import obj as display_obj
from certbot.compat import os
-from certbot.display import util as display_util
class HelpfulArgumentParser:
@@ -66,13 +62,7 @@ def __init__(self, args, plugins, detect_defaults=False):
}
# Get notification function for printing
- try:
- self.notify = zope.component.getUtility(
- interfaces.IDisplay).notification
- except zope_interfaces.ComponentLookupError:
- self.notify = display_util.NoninteractiveDisplay(
- sys.stdout).notification
-
+ self.notify = display_obj.NoninteractiveDisplay(sys.stdout).notification
# List of topics for which additional help can be provided
HELP_TOPICS = ["all", "security", "paths", "automation", "testing"]
diff --git a/certbot/certbot/_internal/client.py b/certbot/certbot/_internal/client.py
index 5c0bed22062..7fcaf91c579 100644
--- a/certbot/certbot/_internal/client.py
+++ b/certbot/certbot/_internal/client.py
@@ -2,7 +2,8 @@
import datetime
import logging
import platform
-from typing import Optional
+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 @@
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):
@@ -598,7 +609,8 @@ def enhance_config(self, domains, chain_path, redirect_default=True):
with error_handler.ErrorHandler(self._rollback_and_restart, msg):
self.installer.restart()
- def apply_enhancement(self, domains, enhancement, options=None):
+ def apply_enhancement(self, domains: List[str], enhancement: str,
+ options: Optional[Union[List[str], str]] = None) -> None:
"""Applies an enhancement on all domains.
:param list domains: list of ssl_vhosts (as strings)
@@ -612,33 +624,28 @@ def apply_enhancement(self, domains, enhancement, options=None):
"""
- msg = f"Could not set up {enhancement} enhancement"
- with error_handler.ErrorHandler(self._recovery_routine_with_msg, msg):
+ enh_label = options if enhancement == "ensure-http-header" else enhancement
+ with error_handler.ErrorHandler(self._recovery_routine_with_msg, None):
for dom in domains:
try:
self.installer.enhance(dom, enhancement, options)
except errors.PluginEnhancementAlreadyPresent:
- if enhancement == "ensure-http-header":
- logger.info("Enhancement %s was already set.",
- options)
- else:
- logger.info("Enhancement %s was already set.",
- enhancement)
+ logger.info("Enhancement %s was already set.", enh_label)
except errors.PluginError:
- logger.error("Unable to set enhancement %s for %s",
- enhancement, dom)
+ logger.error("Unable to set the %s enhancement for %s.", enh_label, dom)
raise
- self.installer.save("Add enhancement %s" % (enhancement))
+ self.installer.save(f"Add enhancement {enh_label}")
- def _recovery_routine_with_msg(self, success_msg):
+ def _recovery_routine_with_msg(self, success_msg: Optional[str]) -> None:
"""Calls the installer's recovery routine and prints success_msg
:param str success_msg: message to show on successful recovery
"""
self.installer.recovery_routine()
- display_util.notify(success_msg)
+ if success_msg:
+ display_util.notify(success_msg)
def _rollback_and_restart(self, success_msg):
"""Rollback the most recent checkpoint and restart the webserver
diff --git a/certbot/certbot/_internal/configuration.py b/certbot/certbot/_internal/configuration.py
index aee0022b84a..989475c5fae 100644
--- a/certbot/certbot/_internal/configuration.py
+++ b/certbot/certbot/_internal/configuration.py
@@ -1,19 +1,17 @@
"""Certbot user-supplied configuration."""
import copy
+from typing import List
from urllib import parse
-import zope.interface
-
from certbot import errors
-from certbot import interfaces
from certbot import util
from certbot._internal import constants
from certbot.compat import misc
from certbot.compat import os
+from certbot.interfaces import Config
-@zope.interface.implementer(interfaces.IConfig)
-class NamespaceConfig:
+class NamespaceConfig(Config):
"""Configuration wrapper around :class:`argparse.Namespace`.
For more documentation, including available attributes, please see
@@ -52,27 +50,51 @@ def __init__(self, namespace):
# Check command line parameters sanity, and error out in case of problem.
check_config_sanity(self)
+ # Delegate any attribute not explicitly defined to the underlying namespace object.
+
def __getattr__(self, name):
return getattr(self.namespace, name)
def __setattr__(self, name, value):
setattr(self.namespace, name, value)
+ # Properties that are part of the abstract Config class contract
+
@property
- def server_path(self):
- """File path based on ``server``."""
- parsed = parse.urlparse(self.namespace.server)
- return (parsed.netloc + parsed.path).replace('/', os.path.sep)
+ def server(self) -> str:
+ return self.namespace.server
@property
- def accounts_dir(self): # pylint: disable=missing-function-docstring
- return self.accounts_dir_for_server_path(self.server_path)
+ def email(self) -> str:
+ return self.namespace.email
- def accounts_dir_for_server_path(self, server_path):
- """Path to accounts directory based on server_path"""
- server_path = misc.underscores_for_unsupported_characters_in_path(server_path)
- return os.path.join(
- self.namespace.config_dir, constants.ACCOUNTS_DIR, server_path)
+ @property
+ def rsa_key_size(self) -> int:
+ return self.namespace.rsa_key_size
+
+ @property
+ def elliptic_curve(self) -> str:
+ return self.namespace.elliptic_curve
+
+ @property
+ def key_type(self) -> str:
+ return self.namespace.key_type
+
+ @property
+ def must_staple(self) -> bool:
+ return self.namespace.must_staple
+
+ @property
+ def config_dir(self) -> str:
+ return self.namespace.config_dir
+
+ @property
+ def work_dir(self) -> str:
+ return self.namespace.work_dir
+
+ @property
+ def account_dir(self) -> str:
+ return self.namespace.account_dir
@property
def backup_dir(self): # pylint: disable=missing-function-docstring
@@ -95,11 +117,59 @@ def temp_checkpoint_dir(self): # pylint: disable=missing-function-docstring
return os.path.join(
self.namespace.work_dir, constants.TEMP_CHECKPOINT_DIR)
- def __deepcopy__(self, _memo):
- # Work around https://bugs.python.org/issue1515 for py26 tests :( :(
- # https://travis-ci.org/letsencrypt/letsencrypt/jobs/106900743#L3276
- new_ns = copy.deepcopy(self.namespace)
- return type(self)(new_ns)
+ @property
+ def no_verify_ssl(self) -> bool:
+ return self.namespace.no_verify_ssl
+
+ @property
+ def http01_port(self) -> int:
+ return self.namespace.http01_port
+
+ @property
+ def http01_address(self) -> str:
+ return self.namespace.http01_address
+
+ @property
+ def https_port(self) -> int:
+ return self.namespace.https_port
+
+ @property
+ def pref_challs(self) -> List[str]:
+ return self.namespace.pref_challs
+
+ @property
+ def allow_subset_of_names(self) -> bool:
+ return self.namespace.allow_subset_of_names
+
+ @property
+ def strict_permissions(self) -> bool:
+ return self.namespace.strict_permissions
+
+ @property
+ def disable_renew_updates(self) -> bool:
+ return self.namespace.disable_renew_updates
+
+ @property
+ def preferred_chain(self) -> str:
+ return self.namespace.preferred_chain
+
+ # Other properties, not part of the abstract class contract
+
+ @property
+ def server_path(self):
+ """File path based on ``server``."""
+ parsed = parse.urlparse(self.namespace.server)
+ return (parsed.netloc + parsed.path).replace('/', os.path.sep)
+
+ @property
+ def accounts_dir(self): # pylint: disable=missing-function-docstring
+ return self.accounts_dir_for_server_path(self.server_path)
+
+ def accounts_dir_for_server_path(self, server_path):
+ """Path to accounts directory based on server_path"""
+ server_path = misc.underscores_for_unsupported_characters_in_path(server_path)
+ return os.path.join(
+ self.namespace.config_dir, constants.ACCOUNTS_DIR, server_path)
@property
def default_archive_dir(self): # pylint: disable=missing-function-docstring
@@ -138,6 +208,14 @@ def renewal_post_hooks_dir(self):
return os.path.join(self.renewal_hooks_dir,
constants.RENEWAL_POST_HOOKS_DIR)
+ # Magic methods
+
+ def __deepcopy__(self, _memo):
+ # Work around https://bugs.python.org/issue1515 for py26 tests :( :(
+ # https://travis-ci.org/letsencrypt/letsencrypt/jobs/106900743#L3276
+ new_ns = copy.deepcopy(self.namespace)
+ return type(self)(new_ns)
+
def check_config_sanity(config):
"""Validate command line options and display error message if
diff --git a/certbot/certbot/_internal/constants.py b/certbot/certbot/_internal/constants.py
index 61895edb1c7..2e368db52f0 100644
--- a/certbot/certbot/_internal/constants.py
+++ b/certbot/certbot/_internal/constants.py
@@ -22,7 +22,8 @@
],
# Main parser
- verbose_count=-int(logging.WARNING / 10),
+ verbose_count=0,
+ verbose_level=None,
text_mode=False,
max_log_backups=1000,
preconfigured_renewal=False,
@@ -142,6 +143,9 @@
QUIET_LOGGING_LEVEL = logging.ERROR
"""Logging level to use in quiet mode."""
+DEFAULT_LOGGING_LEVEL = logging.WARNING
+"""Default logging level to use when not in quiet mode."""
+
RENEWER_DEFAULTS = dict(
renewer_enabled="yes",
renew_before_expiry="30 days",
diff --git a/certbot/certbot/_internal/display/obj.py b/certbot/certbot/_internal/display/obj.py
new file mode 100644
index 00000000000..35b76b83726
--- /dev/null
+++ b/certbot/certbot/_internal/display/obj.py
@@ -0,0 +1,579 @@
+"""This modules define the actual display implementations used in Certbot"""
+import logging
+import sys
+import textwrap
+from typing import Any
+from typing import Optional
+
+import zope.component
+import zope.interface
+
+from certbot import errors
+from certbot import interfaces
+from certbot._internal import constants
+from certbot._internal.display import completer
+from certbot.compat import os
+from certbot.display import util
+
+logger = logging.getLogger(__name__)
+
+
+# This class holds the global state of the display service. Using this class
+# eliminates potential gotchas that exist if self.display was just a global
+# variable. In particular, in functions `_DISPLAY = ` would create a
+# local variable unless the programmer remembered to use the `global` keyword.
+# Adding a level of indirection causes the lookup of the global _DisplayService
+# object to happen first avoiding this potential bug.
+class _DisplayService:
+ def __init__(self):
+ self.display: Optional[interfaces.IDisplay] = None
+
+
+_SERVICE = _DisplayService()
+
+
+@zope.interface.implementer(interfaces.IDisplay)
+class FileDisplay:
+ """File-based display."""
+ # see https://github.com/certbot/certbot/issues/3915
+
+ def __init__(self, outfile, force_interactive):
+ super().__init__()
+ self.outfile = outfile
+ self.force_interactive = force_interactive
+ self.skipped_interaction = False
+
+ def notification(self, message, pause=True,
+ wrap=True, force_interactive=False,
+ decorate=True):
+ """Displays a notification and waits for user acceptance.
+
+ :param str message: Message to display
+ :param bool pause: Whether or not the program should pause for the
+ user's confirmation
+ :param bool wrap: Whether or not the application should wrap text
+ :param bool force_interactive: True if it's safe to prompt the user
+ because it won't cause any workflow regressions
+ :param bool decorate: Whether to surround the message with a
+ decorated frame
+
+ """
+ if wrap:
+ message = _wrap_lines(message)
+
+ logger.debug("Notifying user: %s", message)
+
+ self.outfile.write(
+ (("{line}{frame}{line}" if decorate else "") +
+ "{msg}{line}" +
+ ("{frame}{line}" if decorate else ""))
+ .format(line=os.linesep, frame=util.SIDE_FRAME, msg=message)
+ )
+ self.outfile.flush()
+
+ if pause:
+ if self._can_interact(force_interactive):
+ util.input_with_timeout("Press Enter to Continue")
+ else:
+ logger.debug("Not pausing for user confirmation")
+
+ def menu(self, message, choices, ok_label=None, cancel_label=None, # pylint: disable=unused-argument
+ help_label=None, default=None, # pylint: disable=unused-argument
+ cli_flag=None, force_interactive=False, **unused_kwargs):
+ """Display a menu.
+
+ .. todo:: This doesn't enable the help label/button (I wasn't sold on
+ any interface I came up with for this). It would be a nice feature
+
+ :param str message: title of menu
+ :param choices: Menu lines, len must be > 0
+ :type choices: list of tuples (tag, item) or
+ list of descriptions (tags will be enumerated)
+ :param default: default value to return (if one exists)
+ :param str cli_flag: option used to set this value with the CLI
+ :param bool force_interactive: True if it's safe to prompt the user
+ because it won't cause any workflow regressions
+
+ :returns: tuple of (`code`, `index`) where
+ `code` - str display exit code
+ `index` - int index of the user's selection
+
+ :rtype: tuple
+
+ """
+ if self._return_default(message, default, cli_flag, force_interactive):
+ return util.OK, default
+
+ self._print_menu(message, choices)
+
+ code, selection = self._get_valid_int_ans(len(choices))
+
+ return code, selection - 1
+
+ def input(self, message, default=None,
+ cli_flag=None, force_interactive=False, **unused_kwargs):
+ """Accept input from the user.
+
+ :param str message: message to display to the user
+ :param default: default value to return (if one exists)
+ :param str cli_flag: option used to set this value with the CLI
+ :param bool force_interactive: True if it's safe to prompt the user
+ because it won't cause any workflow regressions
+
+ :returns: tuple of (`code`, `input`) where
+ `code` - str display exit code
+ `input` - str of the user's input
+ :rtype: tuple
+
+ """
+ if self._return_default(message, default, cli_flag, force_interactive):
+ return util.OK, default
+
+ # Trailing space must be added outside of _wrap_lines to be preserved
+ message = _wrap_lines("%s (Enter 'c' to cancel):" % message) + " "
+ ans = util.input_with_timeout(message)
+
+ if ans in ("c", "C"):
+ return util.CANCEL, "-1"
+ return util.OK, ans
+
+ def yesno(self, message, yes_label="Yes", no_label="No", default=None,
+ cli_flag=None, force_interactive=False, **unused_kwargs):
+ """Query the user with a yes/no question.
+
+ Yes and No label must begin with different letters, and must contain at
+ least one letter each.
+
+ :param str message: question for the user
+ :param str yes_label: Label of the "Yes" parameter
+ :param str no_label: Label of the "No" parameter
+ :param default: default value to return (if one exists)
+ :param str cli_flag: option used to set this value with the CLI
+ :param bool force_interactive: True if it's safe to prompt the user
+ because it won't cause any workflow regressions
+
+ :returns: True for "Yes", False for "No"
+ :rtype: bool
+
+ """
+ if self._return_default(message, default, cli_flag, force_interactive):
+ return default
+
+ message = _wrap_lines(message)
+
+ self.outfile.write("{0}{frame}{msg}{0}{frame}".format(
+ os.linesep, frame=util.SIDE_FRAME + os.linesep, msg=message))
+ self.outfile.flush()
+
+ while True:
+ ans = util.input_with_timeout("{yes}/{no}: ".format(
+ yes=_parens_around_char(yes_label),
+ no=_parens_around_char(no_label)))
+
+ # Couldn't get pylint indentation right with elif
+ # elif doesn't matter in this situation
+ if (ans.startswith(yes_label[0].lower()) or
+ ans.startswith(yes_label[0].upper())):
+ return True
+ if (ans.startswith(no_label[0].lower()) or
+ ans.startswith(no_label[0].upper())):
+ return False
+
+ def checklist(self, message, tags, default=None,
+ cli_flag=None, force_interactive=False, **unused_kwargs):
+ """Display a checklist.
+
+ :param str message: Message to display to user
+ :param list tags: `str` tags to select, len(tags) > 0
+ :param default: default value to return (if one exists)
+ :param str cli_flag: option used to set this value with the CLI
+ :param bool force_interactive: True if it's safe to prompt the user
+ because it won't cause any workflow regressions
+
+ :returns: tuple of (`code`, `tags`) where
+ `code` - str display exit code
+ `tags` - list of selected tags
+ :rtype: tuple
+
+ """
+ if self._return_default(message, default, cli_flag, force_interactive):
+ return util.OK, default
+
+ while True:
+ self._print_menu(message, tags)
+
+ code, ans = self.input("Select the appropriate numbers separated "
+ "by commas and/or spaces, or leave input "
+ "blank to select all options shown",
+ force_interactive=True)
+
+ if code == util.OK:
+ if not ans.strip():
+ ans = " ".join(str(x) for x in range(1, len(tags)+1))
+ indices = util.separate_list_input(ans)
+ selected_tags = self._scrub_checklist_input(indices, tags)
+ if selected_tags:
+ return code, selected_tags
+ self.outfile.write(
+ "** Error - Invalid selection **%s" % os.linesep)
+ self.outfile.flush()
+ else:
+ return code, []
+
+ def _return_default(self, prompt, default, cli_flag, force_interactive):
+ """Should we return the default instead of prompting the user?
+
+ :param str prompt: prompt for the user
+ :param default: default answer to prompt
+ :param str cli_flag: command line option for setting an answer
+ to this question
+ :param bool force_interactive: if interactivity is forced by the
+ IDisplay call
+
+ :returns: True if we should return the default without prompting
+ :rtype: bool
+
+ """
+ # assert_valid_call(prompt, default, cli_flag, force_interactive)
+ if self._can_interact(force_interactive):
+ return False
+ if default is None:
+ msg = "Unable to get an answer for the question:\n{0}".format(prompt)
+ if cli_flag:
+ msg += (
+ "\nYou can provide an answer on the "
+ "command line with the {0} flag.".format(cli_flag))
+ raise errors.Error(msg)
+ logger.debug(
+ "Falling back to default %s for the prompt:\n%s",
+ default, prompt)
+ return True
+
+ def _can_interact(self, force_interactive):
+ """Can we safely interact with the user?
+
+ :param bool force_interactive: if interactivity is forced by the
+ IDisplay call
+
+ :returns: True if the display can interact with the user
+ :rtype: bool
+
+ """
+ if (self.force_interactive or force_interactive or
+ sys.stdin.isatty() and self.outfile.isatty()):
+ return True
+ if not self.skipped_interaction:
+ logger.warning(
+ "Skipped user interaction because Certbot doesn't appear to "
+ "be running in a terminal. You should probably include "
+ "--non-interactive or %s on the command line.",
+ constants.FORCE_INTERACTIVE_FLAG)
+ self.skipped_interaction = True
+ return False
+
+ def directory_select(self, message, default=None, cli_flag=None,
+ force_interactive=False, **unused_kwargs):
+ """Display a directory selection screen.
+
+ :param str message: prompt to give the user
+ :param default: default value to return (if one exists)
+ :param str cli_flag: option used to set this value with the CLI
+ :param bool force_interactive: True if it's safe to prompt the user
+ because it won't cause any workflow regressions
+
+ :returns: tuple of the form (`code`, `string`) where
+ `code` - display exit code
+ `string` - input entered by the user
+
+ """
+ with completer.Completer():
+ return self.input(message, default, cli_flag, force_interactive)
+
+ def _scrub_checklist_input(self, indices, tags):
+ """Validate input and transform indices to appropriate tags.
+
+ :param list indices: input
+ :param list tags: Original tags of the checklist
+
+ :returns: valid tags the user selected
+ :rtype: :class:`list` of :class:`str`
+
+ """
+ # They should all be of type int
+ try:
+ indices = [int(index) for index in indices]
+ except ValueError:
+ return []
+
+ # Remove duplicates
+ indices = list(set(indices))
+
+ # Check all input is within range
+ for index in indices:
+ if index < 1 or index > len(tags):
+ return []
+ # Transform indices to appropriate tags
+ return [tags[index - 1] for index in indices]
+
+ def _print_menu(self, message, choices):
+ """Print a menu on the screen.
+
+ :param str message: title of menu
+ :param choices: Menu lines
+ :type choices: list of tuples (tag, item) or
+ list of descriptions (tags will be enumerated)
+
+ """
+ # Can take either tuples or single items in choices list
+ if choices and isinstance(choices[0], tuple):
+ choices = ["%s - %s" % (c[0], c[1]) for c in choices]
+
+ # Write out the message to the user
+ self.outfile.write(
+ "{new}{msg}{new}".format(new=os.linesep, msg=message))
+ self.outfile.write(util.SIDE_FRAME + os.linesep)
+
+ # Write out the menu choices
+ for i, desc in enumerate(choices, 1):
+ msg = "{num}: {desc}".format(num=i, desc=desc)
+ self.outfile.write(_wrap_lines(msg))
+
+ # Keep this outside of the textwrap
+ self.outfile.write(os.linesep)
+
+ self.outfile.write(util.SIDE_FRAME + os.linesep)
+ self.outfile.flush()
+
+ def _get_valid_int_ans(self, max_):
+ """Get a numerical selection.
+
+ :param int max: The maximum entry (len of choices), must be positive
+
+ :returns: tuple of the form (`code`, `selection`) where
+ `code` - str display exit code ('ok' or cancel')
+ `selection` - int user's selection
+ :rtype: tuple
+
+ """
+ selection = -1
+ if max_ > 1:
+ input_msg = ("Select the appropriate number "
+ "[1-{max_}] then [enter] (press 'c' to "
+ "cancel): ".format(max_=max_))
+ else:
+ input_msg = ("Press 1 [enter] to confirm the selection "
+ "(press 'c' to cancel): ")
+ while selection < 1:
+ ans = util.input_with_timeout(input_msg)
+ if ans.startswith("c") or ans.startswith("C"):
+ return util.CANCEL, -1
+ try:
+ selection = int(ans)
+ if selection < 1 or selection > max_:
+ selection = -1
+ raise ValueError
+
+ except ValueError:
+ self.outfile.write(
+ "{0}** Invalid input **{0}".format(os.linesep))
+ self.outfile.flush()
+
+ return util.OK, selection
+
+
+@zope.interface.implementer(interfaces.IDisplay)
+class NoninteractiveDisplay:
+ """An iDisplay implementation that never asks for interactive user input"""
+
+ def __init__(self, outfile, *unused_args, **unused_kwargs):
+ super().__init__()
+ self.outfile = outfile
+
+ def _interaction_fail(self, message, cli_flag, extra=""):
+ "Error out in case of an attempt to interact in noninteractive mode"
+ msg = "Missing command line flag or config entry for this setting:\n"
+ msg += message
+ if extra:
+ msg += "\n" + extra
+ if cli_flag:
+ msg += "\n\n(You can set this with the {0} flag)".format(cli_flag)
+ raise errors.MissingCommandlineFlag(msg)
+
+ def notification(self, message, pause=False, wrap=True, decorate=True, **unused_kwargs): # pylint: disable=unused-argument
+ """Displays a notification without waiting for user acceptance.
+
+ :param str message: Message to display to stdout
+ :param bool pause: The NoninteractiveDisplay waits for no keyboard
+ :param bool wrap: Whether or not the application should wrap text
+ :param bool decorate: Whether to apply a decorated frame to the message
+
+ """
+ if wrap:
+ message = _wrap_lines(message)
+
+ logger.debug("Notifying user: %s", message)
+
+ self.outfile.write(
+ (("{line}{frame}{line}" if decorate else "") +
+ "{msg}{line}" +
+ ("{frame}{line}" if decorate else ""))
+ .format(line=os.linesep, frame=util.SIDE_FRAME, msg=message)
+ )
+ self.outfile.flush()
+
+ def menu(self, message, choices, ok_label=None, cancel_label=None,
+ help_label=None, default=None, cli_flag=None, **unused_kwargs):
+ # pylint: disable=unused-argument
+ """Avoid displaying a menu.
+
+ :param str message: title of menu
+ :param choices: Menu lines, len must be > 0
+ :type choices: list of tuples (tag, item) or
+ list of descriptions (tags will be enumerated)
+ :param int default: the default choice
+ :param dict kwargs: absorbs various irrelevant labelling arguments
+
+ :returns: tuple of (`code`, `index`) where
+ `code` - str display exit code
+ `index` - int index of the user's selection
+ :rtype: tuple
+ :raises errors.MissingCommandlineFlag: if there was no default
+
+ """
+ if default is None:
+ self._interaction_fail(message, cli_flag, "Choices: " + repr(choices))
+
+ return util.OK, default
+
+ def input(self, message, default=None, cli_flag=None, **unused_kwargs):
+ """Accept input from the user.
+
+ :param str message: message to display to the user
+
+ :returns: tuple of (`code`, `input`) where
+ `code` - str display exit code
+ `input` - str of the user's input
+ :rtype: tuple
+ :raises errors.MissingCommandlineFlag: if there was no default
+
+ """
+ if default is None:
+ self._interaction_fail(message, cli_flag)
+ return util.OK, default
+
+ def yesno(self, message, yes_label=None, no_label=None, # pylint: disable=unused-argument
+ default=None, cli_flag=None, **unused_kwargs):
+ """Decide Yes or No, without asking anybody
+
+ :param str message: question for the user
+ :param dict kwargs: absorbs yes_label, no_label
+
+ :raises errors.MissingCommandlineFlag: if there was no default
+ :returns: True for "Yes", False for "No"
+ :rtype: bool
+
+ """
+ if default is None:
+ self._interaction_fail(message, cli_flag)
+ return default
+
+ def checklist(self, message, tags, default=None,
+ cli_flag=None, **unused_kwargs):
+ """Display a checklist.
+
+ :param str message: Message to display to user
+ :param list tags: `str` tags to select, len(tags) > 0
+ :param dict kwargs: absorbs default_status arg
+
+ :returns: tuple of (`code`, `tags`) where
+ `code` - str display exit code
+ `tags` - list of selected tags
+ :rtype: tuple
+
+ """
+ if default is None:
+ self._interaction_fail(message, cli_flag, "? ".join(tags))
+ return util.OK, default
+
+ def directory_select(self, message, default=None,
+ cli_flag=None, **unused_kwargs):
+ """Simulate prompting the user for a directory.
+
+ This function returns default if it is not ``None``, otherwise,
+ an exception is raised explaining the problem. If cli_flag is
+ not ``None``, the error message will include the flag that can
+ be used to set this value with the CLI.
+
+ :param str message: prompt to give the user
+ :param default: default value to return (if one exists)
+ :param str cli_flag: option used to set this value with the CLI
+
+ :returns: tuple of the form (`code`, `string`) where
+ `code` - int display exit code
+ `string` - input entered by the user
+
+ """
+ return self.input(message, default, cli_flag)
+
+
+# The two following functions use "Any" for their parameter/output types. Normally interfaces from
+# certbot.interfaces would be used, but MyPy will not understand their semantic. These interfaces
+# will be removed soon and replaced by ABC classes that will be used also here for type checking.
+# TODO: replace Any by actual ABC classes once available
+
+def get_display() -> Any:
+ """Get the display utility.
+
+ :return: the display utility
+ :rtype: IDisplay
+ :raise: ValueError if the display utility is not configured yet.
+
+ """
+ if not _SERVICE.display:
+ raise ValueError("This function was called too early in Certbot's execution "
+ "as the display utility hasn't been configured yet.")
+ return _SERVICE.display
+
+
+def set_display(display: Any) -> None:
+ """Set the display service.
+
+ :param IDisplay display: the display service
+
+ """
+ # This call is done only for retro-compatibility purposes.
+ # TODO: Remove this call once zope dependencies are removed from Certbot.
+ zope.component.provideUtility(display)
+
+ _SERVICE.display = display
+
+
+def _wrap_lines(msg):
+ """Format lines nicely to 80 chars.
+
+ :param str msg: Original message
+
+ :returns: Formatted message respecting newlines in message
+ :rtype: str
+
+ """
+ lines = msg.splitlines()
+ fixed_l = []
+
+ for line in lines:
+ fixed_l.append(textwrap.fill(
+ line,
+ 80,
+ break_long_words=False,
+ break_on_hyphens=False))
+
+ return '\n'.join(fixed_l)
+
+
+def _parens_around_char(label):
+ """Place parens around first character of label.
+
+ :param str label: Must contain at least one character
+
+ """
+ return "({first}){rest}".format(first=label[0], rest=label[1:])
diff --git a/certbot/certbot/_internal/eff.py b/certbot/certbot/_internal/eff.py
index b01e2dd61d1..8b67ff00def 100644
--- a/certbot/certbot/_internal/eff.py
+++ b/certbot/certbot/_internal/eff.py
@@ -3,9 +3,7 @@
from typing import Optional
import requests
-import zope.component
-from certbot import interfaces
from certbot._internal import constants
from certbot._internal.account import Account
from certbot._internal.account import AccountFileStorage
@@ -75,8 +73,7 @@ def _want_subscription() -> bool:
"founding partner of the Let's Encrypt project and the non-profit organization "
"that develops Certbot? We'd like to send you email about our work encrypting "
"the web, EFF news, campaigns, and ways to support digital freedom. ")
- display = zope.component.getUtility(interfaces.IDisplay)
- return display.yesno(prompt, default=False)
+ return display_util.yesno(prompt, default=False)
def subscribe(email: str) -> None:
diff --git a/certbot/certbot/_internal/error_handler.py b/certbot/certbot/_internal/error_handler.py
index 01cc92b42be..64aad155ed5 100644
--- a/certbot/certbot/_internal/error_handler.py
+++ b/certbot/certbot/_internal/error_handler.py
@@ -139,8 +139,8 @@ def _set_signal_handlers(self):
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):
diff --git a/certbot/certbot/_internal/log.py b/certbot/certbot/_internal/log.py
index 835ec77f9c0..fd665c6885f 100644
--- a/certbot/certbot/_internal/log.py
+++ b/certbot/certbot/_internal/log.py
@@ -120,8 +120,11 @@ def post_arg_parse_setup(config):
if config.quiet:
level = constants.QUIET_LOGGING_LEVEL
+ elif config.verbose_level is not None:
+ level = constants.DEFAULT_LOGGING_LEVEL - int(config.verbose_level) * 10
else:
- level = -config.verbose_count * 10
+ level = constants.DEFAULT_LOGGING_LEVEL - config.verbose_count * 10
+
stderr_handler.setLevel(level)
logger.debug('Root logging level set at %d', level)
diff --git a/certbot/certbot/_internal/main.py b/certbot/certbot/_internal/main.py
index 2bf6570f5bd..47d7e1f03ed 100644
--- a/certbot/certbot/_internal/main.py
+++ b/certbot/certbot/_internal/main.py
@@ -1,10 +1,10 @@
"""Certbot main entry point."""
# pylint: disable=too-many-lines
+from contextlib import contextmanager
import functools
import logging.handlers
import sys
-from contextlib import contextmanager
from typing import Generator
from typing import IO
from typing import Iterable
@@ -37,6 +37,7 @@
from certbot._internal import snap_config
from certbot._internal import storage
from certbot._internal import updater
+from certbot._internal.display import obj as display_obj
from certbot._internal.plugins import disco as plugins_disco
from certbot._internal.plugins import selection as plug_sel
from certbot.compat import filesystem
@@ -67,9 +68,8 @@ def _suggest_donation_if_appropriate(config):
if config.staging:
# --dry-run implies --staging
return
- disp = zope.component.getUtility(interfaces.IDisplay)
util.atexit_register(
- disp.notification,
+ display_util.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",
@@ -191,10 +191,8 @@ def _handle_subset_cert_request(config: configuration.NamespaceConfig,
existing,
", ".join(domains),
br=os.linesep)
- if config.expand or config.renew_by_default or zope.component.getUtility(
- interfaces.IDisplay).yesno(question, "Expand", "Cancel",
- cli_flag="--expand",
- force_interactive=True):
+ if config.expand or config.renew_by_default or display_util.yesno(
+ question, "Expand", "Cancel", cli_flag="--expand", force_interactive=True):
return "renew", cert
display_util.notify(
"To obtain a new certificate that contains these names without "
@@ -202,7 +200,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)
@@ -247,9 +245,8 @@ def _handle_identical_cert_request(config: configuration.NamespaceConfig,
choices = [keep_opt,
"Renew & replace the certificate (may be subject to CA rate limits)"]
- display = zope.component.getUtility(interfaces.IDisplay)
- response = display.menu(question, choices,
- default=0, force_interactive=True)
+ response = display_util.menu(question, choices,
+ default=0, force_interactive=True)
if response[0] == display_util.CANCEL:
# TODO: Add notification related to command-line options for
# skipping the menu for this case.
@@ -423,8 +420,7 @@ def _ask_user_to_confirm_new_names(config, new_domains, certname, old_domains):
_format_list("+", added),
_format_list("-", removed),
br=os.linesep))
- obj = zope.component.getUtility(interfaces.IDisplay)
- if not obj.yesno(msg, "Update certificate", "Cancel", default=True):
+ if not display_util.yesno(msg, "Update certificate", "Cancel", default=True):
raise errors.ConfigurationError("Specified mismatched certificate name and domains.")
@@ -507,6 +503,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 +559,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 +572,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.
@@ -631,8 +648,7 @@ def _tos_cb(terms_of_service):
msg = ("Please read the Terms of Service at {0}. You "
"must agree in order to register with the ACME "
"server. Do you agree?".format(terms_of_service))
- obj = zope.component.getUtility(interfaces.IDisplay)
- result = obj.yesno(msg, cli_flag="--agree-tos", force_interactive=True)
+ result = display_util.yesno(msg, cli_flag="--agree-tos", force_interactive=True)
if not result:
raise errors.Error(
"Registration cannot proceed without accepting "
@@ -681,14 +697,12 @@ def _delete_if_appropriate(config):
:raises errors.Error: If anything goes wrong, including bad user input, if an overlapping
archive dir is found for the specified lineage, etc ...
"""
- display = zope.component.getUtility(interfaces.IDisplay)
-
attempt_deletion = config.delete_after_revoke
if attempt_deletion is None:
msg = ("Would you like to delete the certificate(s) you just revoked, "
"along with all earlier and later versions of the certificate?")
- attempt_deletion = display.yesno(msg, yes_label="Yes (recommended)", no_label="No",
- force_interactive=True, default=True)
+ attempt_deletion = display_util.yesno(msg, yes_label="Yes (recommended)", no_label="No",
+ force_interactive=True, default=True)
if not attempt_deletion:
return
@@ -767,11 +781,10 @@ def unregister(config, unused_plugins):
if not accounts:
return "Could not find existing account to deactivate."
- yesno = zope.component.getUtility(interfaces.IDisplay).yesno
prompt = ("Are you sure you would like to irrevocably deactivate "
"your account?")
- wants_deactivate = yesno(prompt, yes_label='Deactivate', no_label='Abort',
- default=True)
+ wants_deactivate = display_util.yesno(prompt, yes_label='Deactivate', no_label='Abort',
+ default=True)
if not wants_deactivate:
return "Deactivation aborted."
@@ -1013,8 +1026,7 @@ def plugins_cmd(config, plugins):
filtered = plugins.visible().ifaces(ifaces)
logger.debug("Filtered plugins: %r", filtered)
- notify = functools.partial(zope.component.getUtility(
- interfaces.IDisplay).notification, pause=False)
+ notify = functools.partial(display_util.notification, pause=False)
if not config.init and not config.prepare:
notify(str(filtered))
return
@@ -1052,7 +1064,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.error(msg, sys.argv[0])
+ logger.error(msg, cli.cli_command)
raise errors.MisconfigurationError("No enhancements requested, exiting.")
try:
@@ -1398,7 +1410,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
@@ -1407,8 +1419,8 @@ def certonly(config, plugins):
should_get_cert, lineage = _find_cert(config, domains, certname)
if not should_get_cert:
- notify = zope.component.getUtility(interfaces.IDisplay).notification
- notify("Certificate not yet due for renewal; no action taken.", pause=False)
+ display_util.notification("Certificate not yet due for renewal; no action taken.",
+ pause=False)
return
lineage = _get_and_save_cert(le_client, config, domains, certname, lineage)
@@ -1417,7 +1429,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)
@@ -1541,12 +1554,13 @@ def main(cli_args=None):
if config.func != plugins_cmd: # pylint: disable=comparison-with-callable
raise
- # Reporter
+ # These calls are done only for retro-compatibility purposes.
+ # TODO: Remove these calls once zope dependencies are removed from Certbot.
report = reporter.Reporter(config)
zope.component.provideUtility(report)
util.atexit_register(report.print_messages)
with make_displayer(config) as displayer:
- zope.component.provideUtility(displayer)
+ display_obj.set_display(displayer)
return config.func(config, plugins)
diff --git a/certbot/certbot/_internal/plugins/disco.py b/certbot/certbot/_internal/plugins/disco.py
index 74406296817..511ddb7919a 100644
--- a/certbot/certbot/_internal/plugins/disco.py
+++ b/certbot/certbot/_internal/plugins/disco.py
@@ -1,15 +1,18 @@
"""Utilities for plugins discovery and selection."""
+from collections.abc import Mapping
import itertools
import logging
import sys
-from collections.abc import Mapping
from typing import Dict
from typing import Optional
+from typing import Type
from typing import Union
+import warnings
import pkg_resources
import zope.interface
import zope.interface.verify
+
from certbot import errors
from certbot import interfaces
from certbot._internal import constants
@@ -48,10 +51,10 @@ class PluginEntryPoint:
def __init__(self, entry_point: pkg_resources.EntryPoint, with_prefix=False):
self.name = self.entry_point_to_plugin_name(entry_point, with_prefix)
- self.plugin_cls: interfaces.IPluginFactory = entry_point.load()
+ self.plugin_cls: Type[interfaces.Plugin] = entry_point.load()
self.entry_point = entry_point
self.warning_message: Optional[str] = None
- self._initialized: Optional[interfaces.IPlugin] = None
+ self._initialized: Optional[interfaces.Plugin] = None
self._prepared: Optional[Union[bool, Error]] = None
self._hidden = False
self._long_description: Optional[str] = None
@@ -86,10 +89,7 @@ def long_description(self):
"""Long description of the plugin."""
if self._long_description:
return self._long_description
- try:
- return self.plugin_cls.long_description
- except AttributeError:
- return self.description
+ return getattr(self.plugin_cls, "long_description", self.description)
@long_description.setter
def long_description(self, description):
@@ -107,7 +107,7 @@ def hidden(self, hide):
def ifaces(self, *ifaces_groups):
"""Does plugin implements specified interface groups?"""
return not ifaces_groups or any(
- all(iface.implementedBy(self.plugin_cls)
+ all(_implements(self.plugin_cls, iface)
for iface in ifaces)
for ifaces in ifaces_groups)
@@ -120,9 +120,9 @@ def init(self, config=None):
"""Memoized plugin initialization."""
if not self.initialized:
self.entry_point.require() # fetch extras!
- # TODO: remove type ignore once the interface becomes a proper
- # abstract class (using abc) that mypy understands.
- self._initialized = self.plugin_cls(config, self.name) # type: ignore
+ # For plugins implementing ABCs Plugin, Authenticator or Installer, the following
+ # line will raise an exception if some implementations of abstract methods are missing.
+ self._initialized = self.plugin_cls(config, self.name)
return self._initialized
def verify(self, ifaces):
@@ -130,14 +130,9 @@ def verify(self, ifaces):
if not self.initialized:
raise ValueError("Plugin is not initialized.")
for iface in ifaces: # zope.interface.providedBy(plugin)
- try:
- zope.interface.verify.verifyObject(iface, self.init())
- except zope.interface.exceptions.BrokenImplementation as error:
- if iface.implementedBy(self.plugin_cls):
- logger.debug(
- "%s implements %s but object does not verify: %s",
- self.plugin_cls, iface.__name__, error, exc_info=True)
+ if not _verify(self.init(), self.plugin_cls, iface):
return False
+
return True
@property
@@ -195,8 +190,9 @@ def __str__(self):
"* {0}".format(self.name),
"Description: {0}".format(self.plugin_cls.description),
"Interfaces: {0}".format(", ".join(
- iface.__name__ for iface in zope.interface.implementedBy(
- self.plugin_cls))),
+ cls.__name__ for cls in self.plugin_cls.mro()
+ if cls.__module__ == 'certbot.interfaces'
+ )),
"Entry point: {0}".format(self.entry_point),
]
@@ -259,11 +255,11 @@ def _load_entry_point(cls, entry_point, plugins, with_prefix):
plugin2 = other_ep.entry_point.dist.key if other_ep.entry_point.dist else "unknown"
raise Exception("Duplicate plugin name {0} from {1} and {2}.".format(
plugin_ep.name, plugin1, plugin2))
- if interfaces.IPluginFactory.providedBy(plugin_ep.plugin_cls):
+ if _provides(plugin_ep.plugin_cls, interfaces.Plugin):
plugins[plugin_ep.name] = plugin_ep
else: # pragma: no cover
logger.warning(
- "%r does not provide IPluginFactory, skipping", plugin_ep)
+ "%r does not inherit from Plugin, skipping", plugin_ep)
return plugin_ep
@@ -338,3 +334,69 @@ def __str__(self):
if not self._plugins:
return "No plugins"
return "\n\n".join(str(p_ep) for p_ep in self._plugins.values())
+
+
+def _provides(target_class: Type[interfaces.Plugin], iface: Type) -> bool:
+ if issubclass(target_class, iface):
+ return True
+
+ if iface == interfaces.Plugin and interfaces.IPluginFactory.providedBy(target_class):
+ warnings.warn("Zope interface certbot.interfaces.IPluginFactory is deprecated, "
+ "use ABC certbot.interface.Plugin instead.")
+ return True
+
+ return False
+
+
+def _implements(target_class: Type[interfaces.Plugin], iface: Type) -> bool:
+ if issubclass(target_class, iface):
+ return True
+
+ if iface == interfaces.Plugin and interfaces.IPlugin.implementedBy(target_class):
+ warnings.warn("Zope interface certbot.interfaces.IPlugin is deprecated, "
+ "use ABC certbot.interface.Plugin instead.")
+ return True
+
+ if iface == interfaces.Authenticator and interfaces.IAuthenticator.implementedBy(target_class):
+ warnings.warn("Zope interface certbot.interfaces.IAuthenticator is deprecated, "
+ "use ABC certbot.interface.Authenticator instead.")
+ return True
+
+ if iface == interfaces.Installer and interfaces.IInstaller.implementedBy(target_class):
+ warnings.warn("Zope interface certbot.interfaces.IInstaller is deprecated, "
+ "use ABC certbot.interface.Installer instead.")
+ return True
+
+ return False
+
+
+def _verify(target_instance: interfaces.Plugin, target_class: Type[interfaces.Plugin],
+ iface: Type) -> bool:
+ if issubclass(target_class, iface):
+ # No need to trigger some verify logic for ABCs: when the object is instantiated,
+ # an error would be raised if implementation is not done properly.
+ # So effectively the checks have been done when the plugin has been initialized.
+ return True
+
+ zope_iface: Optional[Type[zope.interface.Interface]] = None
+
+ if iface == interfaces.Plugin:
+ zope_iface = interfaces.IPlugin
+ if iface == interfaces.Authenticator:
+ zope_iface = interfaces.IAuthenticator
+ if iface == interfaces.Installer:
+ zope_iface = interfaces.IInstaller
+
+ if not zope_iface:
+ raise ValueError(f"Unexpected type: {iface.__name__}")
+
+ try:
+ zope.interface.verify.verifyObject(iface, target_instance)
+ return True
+ except zope.interface.exceptions.BrokenImplementation as error:
+ if zope_iface.implementedBy(target_class):
+ logger.debug(
+ "%s implements %s but object does not verify: %s",
+ target_class, zope_iface.__name__, error, exc_info=True)
+
+ return False
diff --git a/certbot/certbot/_internal/plugins/manual.py b/certbot/certbot/_internal/plugins/manual.py
index 3f41024fba7..2901213d59c 100644
--- a/certbot/certbot/_internal/plugins/manual.py
+++ b/certbot/certbot/_internal/plugins/manual.py
@@ -2,27 +2,24 @@
import logging
from typing import Dict
-import zope.component
-import zope.interface
-
from acme import challenges
from certbot import achallenges
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._internal.cli import cli_constants
from certbot.compat import misc
from certbot.compat import os
from certbot.display import ops as display_ops
+from certbot.display import util as display_util
from certbot.plugins import common
logger = logging.getLogger(__name__)
-@zope.interface.implementer(interfaces.IAuthenticator)
-@zope.interface.provider(interfaces.IPluginFactory)
-class Authenticator(common.Plugin):
+
+class Authenticator(common.Plugin, interfaces.Authenticator):
"""Manual authenticator
This plugin allows the user to perform the domain validation
@@ -108,7 +105,7 @@ def add_parser_arguments(cls, add):
util.add_deprecated_argument(add, 'public-ip-logging-ok', 0)
def prepare(self): # pylint: disable=missing-function-docstring
- if self.config.noninteractive_mode and not self.conf('auth-hook'):
+ if getattr(self.config, 'noninteractive_mode', False) and not self.conf('auth-hook'):
raise errors.PluginError(
'An authentication script must be provided with --{0} when '
'using the manual plugin non-interactively.'.format(
@@ -116,7 +113,7 @@ def prepare(self): # pylint: disable=missing-function-docstring
self._validate_hooks()
def _validate_hooks(self):
- if self.config.validate_hooks:
+ if getattr(self.config, 'validate_hooks', False):
for name in ('auth-hook', 'cleanup-hook'):
hook = self.conf(name)
if hook is not None:
@@ -225,8 +222,7 @@ def _perform_achall_manually(self, achall, last_dns_achall=False):
elif self.subsequent_any_challenge:
# 2nd or later challenge of another type
msg += self._SUBSEQUENT_CHALLENGE_INSTRUCTIONS
- display = zope.component.getUtility(interfaces.IDisplay)
- display.notification(msg, wrap=False, force_interactive=True)
+ display_util.notification(msg, wrap=False, force_interactive=True)
self.subsequent_any_challenge = True
def cleanup(self, achalls): # pylint: disable=missing-function-docstring
diff --git a/certbot/certbot/_internal/plugins/null.py b/certbot/certbot/_internal/plugins/null.py
index 5ab2f4a04df..b800c5c39de 100644
--- a/certbot/certbot/_internal/plugins/null.py
+++ b/certbot/certbot/_internal/plugins/null.py
@@ -1,22 +1,22 @@
"""Null plugin."""
import logging
-import zope.interface
-
from certbot import interfaces
from certbot.plugins import common
logger = logging.getLogger(__name__)
-@zope.interface.implementer(interfaces.IInstaller)
-@zope.interface.provider(interfaces.IPluginFactory)
-class Installer(common.Plugin):
+class Installer(common.Plugin, interfaces.Installer):
"""Null installer."""
description = "Null Installer"
hidden = True
+ @classmethod
+ def add_parser_arguments(cls, add):
+ pass
+
# pylint: disable=missing-function-docstring
def prepare(self):
diff --git a/certbot/certbot/_internal/plugins/selection.py b/certbot/certbot/_internal/plugins/selection.py
index cdf2c235582..1d48dc8a70b 100644
--- a/certbot/certbot/_internal/plugins/selection.py
+++ b/certbot/certbot/_internal/plugins/selection.py
@@ -1,9 +1,8 @@
"""Decide which plugins to use for authentication & installation"""
import logging
-
-from typing import Optional, Tuple
-import zope.component
+from typing import Optional
+from typing import Tuple
from certbot import errors
from certbot import interfaces
@@ -12,31 +11,31 @@
from certbot.display import util as display_util
logger = logging.getLogger(__name__)
-z_util = zope.component.getUtility
+
def pick_configurator(
- config, default, plugins,
- question="How would you like to authenticate and install "
- "certificates?"):
+ config, default, plugins,
+ question="How would you like to authenticate and install "
+ "certificates?"):
"""Pick configurator plugin."""
return pick_plugin(
config, default, plugins, question,
- (interfaces.IAuthenticator, interfaces.IInstaller))
+ (interfaces.Authenticator, interfaces.Installer))
def pick_installer(config, default, plugins,
question="How would you like to install certificates?"):
"""Pick installer plugin."""
return pick_plugin(
- config, default, plugins, question, (interfaces.IInstaller,))
+ config, default, plugins, question, (interfaces.Installer,))
def pick_authenticator(
- config, default, plugins, question="How would you "
- "like to authenticate with the ACME CA?"):
+ config, default, plugins, question="How would you "
+ "like to authenticate with the ACME CA?"):
"""Pick authentication plugin."""
return pick_plugin(
- config, default, plugins, question, (interfaces.IAuthenticator,))
+ config, default, plugins, question, (interfaces.Authenticator,))
def get_unprepared_installer(config, plugins):
@@ -56,7 +55,7 @@ def get_unprepared_installer(config, plugins):
return None
installers = plugins.filter(lambda p_ep: p_ep.check_name(req_inst))
installers.init(config)
- installers = installers.verify((interfaces.IInstaller,))
+ installers = installers.verify((interfaces.Installer,))
if len(installers) > 1:
raise errors.PluginSelectionError(
"Found multiple installers with the name %s, Certbot is unable to "
@@ -138,13 +137,12 @@ def choose_plugin(prepared, question):
for plugin_ep in prepared]
while True:
- disp = z_util(interfaces.IDisplay)
- code, index = disp.menu(question, opts, force_interactive=True)
+ code, index = display_util.menu(question, opts, force_interactive=True)
if code == display_util.OK:
plugin_ep = prepared[index]
if plugin_ep.misconfigured:
- z_util(interfaces.IDisplay).notification(
+ display_util.notification(
"The selected plugin encountered an error while parsing "
"your server configuration and cannot be used. The error "
"was:\n\n{0}".format(plugin_ep.prepare()), pause=False)
diff --git a/certbot/certbot/_internal/plugins/standalone.py b/certbot/certbot/_internal/plugins/standalone.py
index 5fb29671f71..45c801256db 100644
--- a/certbot/certbot/_internal/plugins/standalone.py
+++ b/certbot/certbot/_internal/plugins/standalone.py
@@ -5,18 +5,19 @@
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
import OpenSSL
-import zope.interface
from acme import challenges
from acme import standalone as acme_standalone
from certbot import achallenges
from certbot import errors
from certbot import interfaces
+from certbot.display import util as display_util
from certbot.plugins import common
logger = logging.getLogger(__name__)
@@ -27,6 +28,7 @@
Set[achallenges.KeyAuthorizationAnnotatedChallenge]
]
+
class ServerManager:
"""Standalone servers manager.
@@ -105,9 +107,7 @@ def running(self):
return self._instances.copy()
-@zope.interface.implementer(interfaces.IAuthenticator)
-@zope.interface.provider(interfaces.IPluginFactory)
-class Authenticator(common.Plugin):
+class Authenticator(common.Plugin, interfaces.Authenticator):
"""Standalone Authenticator.
This authenticator creates its own ephemeral TCP listener on the
@@ -184,6 +184,14 @@ def cleanup(self, achalls): # pylint: disable=missing-function-docstring
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:
@@ -193,14 +201,12 @@ def _handle_perform_error(error):
"aren't running this program as "
"root).".format(error.port))
if error.socket_error.errno == errno.EADDRINUSE:
- display = zope.component.getUtility(interfaces.IDisplay)
msg = (
"Could not bind TCP port {0} because it is already in "
"use by another process on this system (such as a web "
"server). Please stop the program in question and "
"then try again.".format(error.port))
- should_retry = display.yesno(msg, "Retry",
- "Cancel", default=False)
+ should_retry = display_util.yesno(msg, "Retry", "Cancel", default=False)
if not should_retry:
raise errors.PluginError(msg)
else:
diff --git a/certbot/certbot/_internal/plugins/webroot.py b/certbot/certbot/_internal/plugins/webroot.py
index f9d6032bb01..8d8cf0e4561 100644
--- a/certbot/certbot/_internal/plugins/webroot.py
+++ b/certbot/certbot/_internal/plugins/webroot.py
@@ -8,9 +8,6 @@
from typing import List
from typing import Set
-import zope.component
-import zope.interface
-
from acme import challenges
from certbot import errors
from certbot import interfaces
@@ -27,9 +24,7 @@
logger = logging.getLogger(__name__)
-@zope.interface.implementer(interfaces.IAuthenticator)
-@zope.interface.provider(interfaces.IPluginFactory)
-class Authenticator(common.Plugin):
+class Authenticator(common.Plugin, interfaces.Authenticator):
"""Webroot Authenticator."""
description = "Place files in webroot directory"
@@ -126,11 +121,10 @@ def _prompt_for_webroot(self, domain, known_webroots):
return webroot
def _prompt_with_webroot_list(self, domain, known_webroots):
- display = zope.component.getUtility(interfaces.IDisplay)
path_flag = "--" + self.option_name("path")
while True:
- code, index = display.menu(
+ code, index = display_util.menu(
"Select the webroot for {0}:".format(domain),
["Enter a new webroot"] + known_webroots,
cli_flag=path_flag, force_interactive=True)
diff --git a/certbot/certbot/_internal/renewal.py b/certbot/certbot/_internal/renewal.py
index b4fa6aa0185..f759b9cf5f5 100644
--- a/certbot/certbot/_internal/renewal.py
+++ b/certbot/certbot/_internal/renewal.py
@@ -184,7 +184,7 @@ def restore_required_config_elements(config, renewalparams):
for item_name, restore_func in required_items:
if item_name in renewalparams and not cli.set_by_cli(item_name):
value = restore_func(item_name, renewalparams[item_name])
- setattr(config, item_name, value)
+ setattr(config.namespace, item_name, value)
def _remove_deprecated_config_elements(renewalparams):
@@ -429,8 +429,7 @@ def handle_renewal_request(config):
apply_random_sleep = not sys.stdin.isatty() and config.random_sleep_on_renew
for renewal_file in conf_files:
- disp = zope.component.getUtility(interfaces.IDisplay)
- disp.notification("Processing " + renewal_file, pause=False)
+ display_util.notification("Processing " + renewal_file, pause=False)
lineage_config = copy.deepcopy(config)
lineagename = storage.lineagename_for_filename(renewal_file)
diff --git a/certbot/certbot/_internal/reporter.py b/certbot/certbot/_internal/reporter.py
index 295a2d4c5a8..ffea53c70fa 100644
--- a/certbot/certbot/_internal/reporter.py
+++ b/certbot/certbot/_internal/reporter.py
@@ -5,16 +5,13 @@
import sys
import textwrap
-import zope.interface
-
-from certbot import interfaces
from certbot import util
+from certbot.interfaces import Reporter as BaseReporter
logger = logging.getLogger(__name__)
-@zope.interface.implementer(interfaces.IReporter)
-class Reporter:
+class Reporter(BaseReporter):
"""Collects and displays information to the user.
:ivar `queue.PriorityQueue` messages: Messages to be displayed to
diff --git a/certbot/certbot/_internal/storage.py b/certbot/certbot/_internal/storage.py
index 788c8a2c459..19992cc65df 100644
--- a/certbot/certbot/_internal/storage.py
+++ b/certbot/certbot/_internal/storage.py
@@ -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):
diff --git a/certbot/certbot/_internal/updater.py b/certbot/certbot/_internal/updater.py
index f38fadfbf63..65961df2ef1 100644
--- a/certbot/certbot/_internal/updater.py
+++ b/certbot/certbot/_internal/updater.py
@@ -4,7 +4,7 @@
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__)
diff --git a/certbot/certbot/crypto_util.py b/certbot/certbot/crypto_util.py
index 5cb21b5102c..e620d43e039 100644
--- a/certbot/certbot/crypto_util.py
+++ b/certbot/certbot/crypto_util.py
@@ -7,9 +7,10 @@
import hashlib
import logging
import re
+from typing import List
+from typing import Set
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
diff --git a/certbot/certbot/display/obj.py b/certbot/certbot/display/obj.py
new file mode 100644
index 00000000000..804ee606a46
--- /dev/null
+++ b/certbot/certbot/display/obj.py
@@ -0,0 +1,544 @@
+"""This modules define the actual display implementations used in Certbot"""
+import logging
+import sys
+import textwrap
+
+from certbot import errors
+from certbot import interfaces
+from certbot._internal import constants
+from certbot._internal.display import completer
+from certbot.compat import os
+from certbot.display import util
+
+logger = logging.getLogger(__name__)
+
+
+class FileDisplay(interfaces.Display):
+ """File-based display."""
+ # see https://github.com/certbot/certbot/issues/3915
+
+ def __init__(self, outfile, force_interactive):
+ super().__init__()
+ self.outfile = outfile
+ self.force_interactive = force_interactive
+ self.skipped_interaction = False
+
+ def notification(self, message, pause=True,
+ wrap=True, force_interactive=False,
+ decorate=True):
+ """Displays a notification and waits for user acceptance.
+
+ :param str message: Message to display
+ :param bool pause: Whether or not the program should pause for the
+ user's confirmation
+ :param bool wrap: Whether or not the application should wrap text
+ :param bool force_interactive: True if it's safe to prompt the user
+ because it won't cause any workflow regressions
+ :param bool decorate: Whether to surround the message with a
+ decorated frame
+
+ """
+ if wrap:
+ message = _wrap_lines(message)
+
+ logger.debug("Notifying user: %s", message)
+
+ self.outfile.write(
+ (("{line}{frame}{line}" if decorate else "") +
+ "{msg}{line}" +
+ ("{frame}{line}" if decorate else ""))
+ .format(line=os.linesep, frame=util.SIDE_FRAME, msg=message)
+ )
+ self.outfile.flush()
+
+ if pause:
+ if self._can_interact(force_interactive):
+ util.input_with_timeout("Press Enter to Continue")
+ else:
+ logger.debug("Not pausing for user confirmation")
+
+ def menu(self, message, choices, ok_label=None, cancel_label=None,
+ help_label=None, default=None, cli_flag=None, force_interactive=False):
+ """Display a menu.
+
+ .. todo:: This doesn't enable the help label/button (I wasn't sold on
+ any interface I came up with for this). It would be a nice feature
+
+ :param str message: title of menu
+ :param choices: Menu lines, len must be > 0
+ :type choices: list of tuples (tag, item) or
+ list of descriptions (tags will be enumerated)
+ :param str ok_label: (UNUSED)
+ :param str cancel_label: (UNUSED)
+ :param str help_label: (UNUSED)
+ :param default: default value to return (if one exists)
+ :param str cli_flag: option used to set this value with the CLI
+ :param bool force_interactive: True if it's safe to prompt the user
+ because it won't cause any workflow regressions
+
+ :returns: tuple of (`code`, `index`) where
+ `code` - str display exit code
+ `index` - int index of the user's selection
+
+ :rtype: tuple
+
+ """
+ if self._return_default(message, default, cli_flag, force_interactive):
+ return util.OK, default
+
+ self._print_menu(message, choices)
+
+ code, selection = self._get_valid_int_ans(len(choices))
+
+ return code, selection - 1
+
+ def input(self, message, default=None,
+ cli_flag=None, force_interactive=False):
+ """Accept input from the user.
+
+ :param str message: message to display to the user
+ :param default: default value to return (if one exists)
+ :param str cli_flag: option used to set this value with the CLI
+ :param bool force_interactive: True if it's safe to prompt the user
+ because it won't cause any workflow regressions
+
+ :returns: tuple of (`code`, `input`) where
+ `code` - str display exit code
+ `input` - str of the user's input
+ :rtype: tuple
+
+ """
+ if self._return_default(message, default, cli_flag, force_interactive):
+ return util.OK, default
+
+ # Trailing space must be added outside of _wrap_lines to be preserved
+ message = _wrap_lines("%s (Enter 'c' to cancel):" % message) + " "
+ ans = util.input_with_timeout(message)
+
+ if ans in ("c", "C"):
+ return util.CANCEL, "-1"
+ return util.OK, ans
+
+ def yesno(self, message, yes_label="Yes", no_label="No", default=None,
+ cli_flag=None, force_interactive=False):
+ """Query the user with a yes/no question.
+
+ Yes and No label must begin with different letters, and must contain at
+ least one letter each.
+
+ :param str message: question for the user
+ :param str yes_label: Label of the "Yes" parameter
+ :param str no_label: Label of the "No" parameter
+ :param default: default value to return (if one exists)
+ :param str cli_flag: option used to set this value with the CLI
+ :param bool force_interactive: True if it's safe to prompt the user
+ because it won't cause any workflow regressions
+
+ :returns: True for "Yes", False for "No"
+ :rtype: bool
+
+ """
+ if self._return_default(message, default, cli_flag, force_interactive):
+ return default
+
+ message = _wrap_lines(message)
+
+ self.outfile.write("{0}{frame}{msg}{0}{frame}".format(
+ os.linesep, frame=util.SIDE_FRAME + os.linesep, msg=message))
+ self.outfile.flush()
+
+ while True:
+ ans = util.input_with_timeout("{yes}/{no}: ".format(
+ yes=_parens_around_char(yes_label),
+ no=_parens_around_char(no_label)))
+
+ # Couldn't get pylint indentation right with elif
+ # elif doesn't matter in this situation
+ if (ans.startswith(yes_label[0].lower()) or
+ ans.startswith(yes_label[0].upper())):
+ return True
+ if (ans.startswith(no_label[0].lower()) or
+ ans.startswith(no_label[0].upper())):
+ return False
+
+ def checklist(self, message, tags, default=None,
+ cli_flag=None, force_interactive=False):
+ """Display a checklist.
+
+ :param str message: Message to display to user
+ :param list tags: `str` tags to select, len(tags) > 0
+ :param default: default value to return (if one exists)
+ :param str cli_flag: option used to set this value with the CLI
+ :param bool force_interactive: True if it's safe to prompt the user
+ because it won't cause any workflow regressions
+
+ :returns: tuple of (`code`, `tags`) where
+ `code` - str display exit code
+ `tags` - list of selected tags
+ :rtype: tuple
+
+ """
+ if self._return_default(message, default, cli_flag, force_interactive):
+ return util.OK, default
+
+ while True:
+ self._print_menu(message, tags)
+
+ code, ans = self.input("Select the appropriate numbers separated "
+ "by commas and/or spaces, or leave input "
+ "blank to select all options shown",
+ force_interactive=True)
+
+ if code == util.OK:
+ if not ans.strip():
+ ans = " ".join(str(x) for x in range(1, len(tags)+1))
+ indices = util.separate_list_input(ans)
+ selected_tags = self._scrub_checklist_input(indices, tags)
+ if selected_tags:
+ return code, selected_tags
+ self.outfile.write(
+ "** Error - Invalid selection **%s" % os.linesep)
+ self.outfile.flush()
+ else:
+ return code, []
+
+ def _return_default(self, prompt, default, cli_flag, force_interactive):
+ """Should we return the default instead of prompting the user?
+
+ :param str prompt: prompt for the user
+ :param default: default answer to prompt
+ :param str cli_flag: command line option for setting an answer
+ to this question
+ :param bool force_interactive: if interactivity is forced by the
+ IDisplay call
+
+ :returns: True if we should return the default without prompting
+ :rtype: bool
+
+ """
+ # assert_valid_call(prompt, default, cli_flag, force_interactive)
+ if self._can_interact(force_interactive):
+ return False
+ if default is None:
+ msg = "Unable to get an answer for the question:\n{0}".format(prompt)
+ if cli_flag:
+ msg += (
+ "\nYou can provide an answer on the "
+ "command line with the {0} flag.".format(cli_flag))
+ raise errors.Error(msg)
+ logger.debug(
+ "Falling back to default %s for the prompt:\n%s",
+ default, prompt)
+ return True
+
+ def _can_interact(self, force_interactive):
+ """Can we safely interact with the user?
+
+ :param bool force_interactive: if interactivity is forced by the
+ IDisplay call
+
+ :returns: True if the display can interact with the user
+ :rtype: bool
+
+ """
+ if (self.force_interactive or force_interactive or
+ sys.stdin.isatty() and self.outfile.isatty()):
+ return True
+ if not self.skipped_interaction:
+ logger.warning(
+ "Skipped user interaction because Certbot doesn't appear to "
+ "be running in a terminal. You should probably include "
+ "--non-interactive or %s on the command line.",
+ constants.FORCE_INTERACTIVE_FLAG)
+ self.skipped_interaction = True
+ return False
+
+ def directory_select(self, message, default=None, cli_flag=None,
+ force_interactive=False):
+ """Display a directory selection screen.
+
+ :param str message: prompt to give the user
+ :param default: default value to return (if one exists)
+ :param str cli_flag: option used to set this value with the CLI
+ :param bool force_interactive: True if it's safe to prompt the user
+ because it won't cause any workflow regressions
+
+ :returns: tuple of the form (`code`, `string`) where
+ `code` - display exit code
+ `string` - input entered by the user
+
+ """
+ with completer.Completer():
+ return self.input(message, default, cli_flag, force_interactive)
+
+ def _scrub_checklist_input(self, indices, tags):
+ """Validate input and transform indices to appropriate tags.
+
+ :param list indices: input
+ :param list tags: Original tags of the checklist
+
+ :returns: valid tags the user selected
+ :rtype: :class:`list` of :class:`str`
+
+ """
+ # They should all be of type int
+ try:
+ indices = [int(index) for index in indices]
+ except ValueError:
+ return []
+
+ # Remove duplicates
+ indices = list(set(indices))
+
+ # Check all input is within range
+ for index in indices:
+ if index < 1 or index > len(tags):
+ return []
+ # Transform indices to appropriate tags
+ return [tags[index - 1] for index in indices]
+
+ def _print_menu(self, message, choices):
+ """Print a menu on the screen.
+
+ :param str message: title of menu
+ :param choices: Menu lines
+ :type choices: list of tuples (tag, item) or
+ list of descriptions (tags will be enumerated)
+
+ """
+ # Can take either tuples or single items in choices list
+ if choices and isinstance(choices[0], tuple):
+ choices = ["%s - %s" % (c[0], c[1]) for c in choices]
+
+ # Write out the message to the user
+ self.outfile.write(
+ "{new}{msg}{new}".format(new=os.linesep, msg=message))
+ self.outfile.write(util.SIDE_FRAME + os.linesep)
+
+ # Write out the menu choices
+ for i, desc in enumerate(choices, 1):
+ msg = "{num}: {desc}".format(num=i, desc=desc)
+ self.outfile.write(_wrap_lines(msg))
+
+ # Keep this outside of the textwrap
+ self.outfile.write(os.linesep)
+
+ self.outfile.write(util.SIDE_FRAME + os.linesep)
+ self.outfile.flush()
+
+ def _get_valid_int_ans(self, max_):
+ """Get a numerical selection.
+
+ :param int max_: The maximum entry (len of choices), must be positive
+
+ :returns: tuple of the form (`code`, `selection`) where
+ `code` - str display exit code ('ok' or cancel')
+ `selection` - int user's selection
+ :rtype: tuple
+
+ """
+ selection = -1
+ if max_ > 1:
+ input_msg = ("Select the appropriate number "
+ "[1-{max_}] then [enter] (press 'c' to "
+ "cancel): ".format(max_=max_))
+ else:
+ input_msg = ("Press 1 [enter] to confirm the selection "
+ "(press 'c' to cancel): ")
+ while selection < 1:
+ ans = util.input_with_timeout(input_msg)
+ if ans.startswith("c") or ans.startswith("C"):
+ return util.CANCEL, -1
+ try:
+ selection = int(ans)
+ if selection < 1 or selection > max_:
+ selection = -1
+ raise ValueError
+
+ except ValueError:
+ self.outfile.write(
+ "{0}** Invalid input **{0}".format(os.linesep))
+ self.outfile.flush()
+
+ return util.OK, selection
+
+
+class NoninteractiveDisplay(interfaces.Display):
+ """An iDisplay implementation that never asks for interactive user input"""
+
+ def __init__(self, outfile, *unused_args, **unused_kwargs):
+ super().__init__()
+ self.outfile = outfile
+
+ def _interaction_fail(self, message, cli_flag, extra=""):
+ """Error out in case of an attempt to interact in noninteractive mode"""
+ msg = "Missing command line flag or config entry for this setting:\n"
+ msg += message
+ if extra:
+ msg += "\n" + extra
+ if cli_flag:
+ msg += "\n\n(You can set this with the {0} flag)".format(cli_flag)
+ raise errors.MissingCommandlineFlag(msg)
+
+ def notification(self, message, pause=False, wrap=True,
+ force_interactive=False, decorate=True):
+ """Displays a notification without waiting for user acceptance.
+
+ :param str message: Message to display to stdout
+ :param bool pause: (UNUSED)
+ :param bool wrap: Whether or not the application should wrap text
+ :param bool force_interactive: (UNUSED)
+ :param bool decorate: Whether to apply a decorated frame to the message
+
+ """
+ if wrap:
+ message = _wrap_lines(message)
+
+ logger.debug("Notifying user: %s", message)
+
+ self.outfile.write(
+ (("{line}{frame}{line}" if decorate else "") +
+ "{msg}{line}" +
+ ("{frame}{line}" if decorate else ""))
+ .format(line=os.linesep, frame=util.SIDE_FRAME, msg=message)
+ )
+ self.outfile.flush()
+
+ def menu(self, message, choices, ok_label=None, cancel_label=None,
+ help_label=None, default=None, cli_flag=None, force_interactive=False):
+ # pylint: disable=unused-argument
+ """Avoid displaying a menu.
+
+ :param str message: title of menu
+ :param choices: Menu lines, len must be > 0
+ :type choices: list of tuples (tag, item) or
+ list of descriptions (tags will be enumerated)
+ :param str ok_label: (UNUSED)
+ :param str cancel_label: (UNUSED)
+ :param str help_label: (UNUSED)
+ :param int default: the default choice
+ :param str cli_flag: to automate choice from the menu, (UNUSED)
+ :param bool force_interactive: (UNUSED)
+
+ :returns: tuple of (`code`, `index`) where
+ `code` - str display exit code
+ `index` - int index of the user's selection
+ :rtype: tuple
+ :raises errors.MissingCommandlineFlag: if there was no default
+
+ """
+ if default is None:
+ self._interaction_fail(message, cli_flag, "Choices: " + repr(choices))
+
+ return util.OK, default
+
+ def input(self, message, default=None, cli_flag=None, force_interactive=False):
+ """Accept input from the user.
+
+ :param str message: message to display to the user
+ :param str default: default (non-interactive) response to prompt
+ :param str cli_flag: to automate choice from the menu, eg "--redirect / --no-redirect"
+ :param bool force_interactive: (UNUSED)
+
+ :returns: tuple of (`code`, `input`) where
+ `code` - str display exit code
+ `input` - str of the user's input
+ :rtype: tuple
+ :raises errors.MissingCommandlineFlag: if there was no default
+
+ """
+ if default is None:
+ self._interaction_fail(message, cli_flag)
+ return util.OK, default
+
+ def yesno(self, message, yes_label=None, no_label=None,
+ default=None, cli_flag=None, force_interactive=False):
+ """Decide Yes or No, without asking anybody
+
+ :param str message: question for the user
+ :param str yes_label: (UNUSED)
+ :param str no_label: (UNUSED)
+ :param str default: default (non-interactive) choice from the menu
+ :param str cli_flag: to automate choice from the menu, eg "--agree-tos"
+ :param bool force_interactive: (UNUSED)
+
+ :raises errors.MissingCommandlineFlag: if there was no default
+ :returns: True for "Yes", False for "No"
+ :rtype: bool
+
+ """
+ if default is None:
+ self._interaction_fail(message, cli_flag)
+ return default
+
+ def checklist(self, message, tags, default=None,
+ cli_flag=None, force_interactive=False):
+ """Display a checklist.
+
+ :param str message: message to display to the user
+ :param list tags: where each is of type :class:`str` len(tags) > 0
+ :param str default: default (non-interactive) state of the checklist
+ :param str cli_flag: to automate choice from the menu, eg "--domains"
+ :param bool force_interactive: (UNUSED)
+
+ :returns: tuple of (`code`, `tags`) where
+ `code` - str display exit code
+ `tags` - list of selected tags
+ :rtype: tuple
+
+ """
+ if default is None:
+ self._interaction_fail(message, cli_flag, "? ".join(tags))
+ return util.OK, default
+
+ def directory_select(self, message, default=None,
+ cli_flag=None, force_interactive=False):
+ """Simulate prompting the user for a directory.
+
+ This function returns default if it is not ``None``, otherwise,
+ an exception is raised explaining the problem. If cli_flag is
+ not ``None``, the error message will include the flag that can
+ be used to set this value with the CLI.
+
+ :param str message: prompt to give the user
+ :param default: default value to return (if one exists)
+ :param str cli_flag: option used to set this value with the CLI
+ :param bool force_interactive: (UNUSED)
+
+ :returns: tuple of the form (`code`, `string`) where
+ `code` - int display exit code
+ `string` - input entered by the user
+
+ """
+ return self.input(message, default, cli_flag)
+
+
+def _wrap_lines(msg):
+ """Format lines nicely to 80 chars.
+
+ :param str msg: Original message
+
+ :returns: Formatted message respecting newlines in message
+ :rtype: str
+
+ """
+ lines = msg.splitlines()
+ fixed_l = []
+
+ for line in lines:
+ fixed_l.append(textwrap.fill(
+ line,
+ 80,
+ break_long_words=False,
+ break_on_hyphens=False))
+
+ return '\n'.join(fixed_l)
+
+
+def _parens_around_char(label):
+ """Place parens around first character of label.
+
+ :param str label: Must contain at least one character
+
+ """
+ return "({first}){rest}".format(first=label[0], rest=label[1:])
diff --git a/certbot/certbot/display/ops.py b/certbot/certbot/display/ops.py
index c2051d3d215..4eb01b4dcd3 100644
--- a/certbot/certbot/display/ops.py
+++ b/certbot/certbot/display/ops.py
@@ -2,19 +2,13 @@
import logging
from textwrap import indent
-import zope.component
-
from certbot import errors
-from certbot import interfaces
from certbot import util
from certbot.compat import os
from certbot.display import util as display_util
logger = logging.getLogger(__name__)
-# Define a helper function to avoid verbose code
-z_util = zope.component.getUtility
-
def get_email(invalid=False, optional=True):
"""Prompt for valid email address.
@@ -48,9 +42,8 @@ def get_email(invalid=False, optional=True):
while True:
try:
- code, email = z_util(interfaces.IDisplay).input(
- invalid_prefix + msg if invalid else msg,
- force_interactive=True)
+ code, email = display_util.input_text(invalid_prefix + msg if invalid else msg,
+ force_interactive=True)
except errors.MissingCommandlineFlag:
msg = ("You should register before running non-interactively, "
"or provide --agree-tos and --email flags.")
@@ -81,12 +74,12 @@ def choose_account(accounts):
# Note this will get more complicated once we start recording authorizations
labels = [acc.slug for acc in accounts]
- code, index = z_util(interfaces.IDisplay).menu(
- "Please choose an account", labels, force_interactive=True)
+ code, index = display_util.menu("Please choose an account", labels, force_interactive=True)
if code == display_util.OK:
return accounts[index]
return None
+
def choose_values(values, question=None):
"""Display screen to let user pick one or multiple values from the provided
list.
@@ -96,12 +89,12 @@ def choose_values(values, question=None):
:returns: List of selected values
:rtype: list
"""
- code, items = z_util(interfaces.IDisplay).checklist(
- question, tags=values, force_interactive=True)
+ code, items = display_util.checklist(question, tags=values, force_interactive=True)
if code == display_util.OK and items:
return items
return []
+
def choose_names(installer, question=None):
"""Display screen to select domains to validate.
@@ -147,6 +140,7 @@ def get_valid_domains(domains):
continue
return valid_domains
+
def _sort_names(FQDNs):
"""Sort FQDNs by SLD (and if many, by their subdomains)
@@ -169,13 +163,13 @@ def _filter_names(names, override_question=None):
:rtype: tuple
"""
- #Sort by domain first, and then by subdomain
+ # Sort by domain first, and then by subdomain
sorted_names = _sort_names(names)
if override_question:
question = override_question
else:
question = "Which names would you like to activate HTTPS for?"
- code, names = z_util(interfaces.IDisplay).checklist(
+ code, names = display_util.checklist(
question, tags=sorted_names, cli_flag="--domains", force_interactive=True)
return code, [str(s) for s in names]
@@ -189,7 +183,7 @@ def _choose_names_manually(prompt_prefix=""):
:rtype: `list` of `str`
"""
- code, input_ = z_util(interfaces.IDisplay).input(
+ code, input_ = display_util.input_text(
prompt_prefix +
"Please enter the domain name(s) you would like on your certificate "
"(comma and/or space separated)",
@@ -217,17 +211,16 @@ 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)
if retry_message:
# We had error in input
- retry = z_util(interfaces.IDisplay).yesno(retry_message,
- force_interactive=True)
+ retry = display_util.yesno(retry_message, force_interactive=True)
if retry:
return _choose_names_manually()
else:
@@ -332,7 +325,7 @@ def _get_validated(method, validator, message, default=None, **kwargs):
raw,
message,
exc_info=True)
- zope.component.getUtility(interfaces.IDisplay).notification(str(error), pause=False)
+ display_util.notification(str(error), pause=False)
else:
return code, raw
@@ -348,8 +341,7 @@ def validated_input(validator, *args, **kwargs):
:return: as `~certbot.interfaces.IDisplay.input`
:rtype: tuple
"""
- return _get_validated(zope.component.getUtility(interfaces.IDisplay).input,
- validator, *args, **kwargs)
+ return _get_validated(display_util.input_text, validator, *args, **kwargs)
def validated_directory(validator, *args, **kwargs):
@@ -364,5 +356,4 @@ def validated_directory(validator, *args, **kwargs):
:return: as `~certbot.interfaces.IDisplay.directory_select`
:rtype: tuple
"""
- return _get_validated(zope.component.getUtility(interfaces.IDisplay).directory_select,
- validator, *args, **kwargs)
+ return _get_validated(display_util.directory_select, validator, *args, **kwargs)
diff --git a/certbot/certbot/display/util.py b/certbot/certbot/display/util.py
index dc642586c94..bcbd46643b5 100644
--- a/certbot/certbot/display/util.py
+++ b/certbot/certbot/display/util.py
@@ -11,18 +11,17 @@
"""
import logging
import sys
-import textwrap
from typing import List
+from typing import Optional
+from typing import Tuple
+from typing import Union
-import zope.component
-import zope.interface
-from certbot import errors
-from certbot import interfaces
-from certbot._internal import constants
-from certbot._internal.display import completer
from certbot.compat import misc
-from certbot.compat import os
+# These imports are done to not break the public API of the module.
+from certbot._internal.display.obj import FileDisplay # pylint: disable=unused-import
+from certbot._internal.display.obj import NoninteractiveDisplay # pylint: disable=unused-import
+from certbot._internal.display import obj
logger = logging.getLogger(__name__)
@@ -46,26 +45,145 @@
"""Display boundary (alternates spaces, so when copy-pasted, markdown doesn't interpret
it as a heading)"""
-def _wrap_lines(msg):
- """Format lines nicely to 80 chars.
- :param str msg: Original message
+def notify(msg: str) -> None:
+ """Display a basic status message.
- :returns: Formatted message respecting newlines in message
- :rtype: str
+ :param str msg: message to display
+
+ """
+ obj.get_display().notification(msg, pause=False, decorate=False, wrap=False)
+
+
+def notification(message: str, pause: bool = True, wrap: bool = True,
+ force_interactive: bool = False, decorate: bool = True) -> None:
+ """Displays a notification and waits for user acceptance.
+
+ :param str message: Message to display
+ :param bool pause: Whether or not the program should pause for the
+ user's confirmation
+ :param bool wrap: Whether or not the application should wrap text
+ :param bool force_interactive: True if it's safe to prompt the user
+ because it won't cause any workflow regressions
+ :param bool decorate: Whether to surround the message with a
+ decorated frame
+
+ """
+ obj.get_display().notification(message, pause=pause, wrap=wrap,
+ force_interactive=force_interactive, decorate=decorate)
+
+
+def menu(message: str, choices: Union[List[str], Tuple[str, str]],
+ default: Optional[int] = None, cli_flag: Optional[str] = None,
+ force_interactive: bool = False) -> Tuple[str, int]:
+ """Display a menu.
+
+ .. todo:: This doesn't enable the help label/button (I wasn't sold on
+ any interface I came up with for this). It would be a nice feature.
+
+ :param str message: title of menu
+ :param choices: Menu lines, len must be > 0
+ :type choices: list of tuples (tag, item) or
+ list of descriptions (tags will be enumerated)
+ :param default: default value to return (if one exists)
+ :param str cli_flag: option used to set this value with the CLI
+ :param bool force_interactive: True if it's safe to prompt the user
+ because it won't cause any workflow regressions
+
+ :returns: tuple of (`code`, `index`) where
+ `code` - str display exit code
+ `index` - int index of the user's selection
+
+ :rtype: tuple
+
+ """
+ return obj.get_display().menu(message, choices, default=default, cli_flag=cli_flag,
+ force_interactive=force_interactive)
+
+
+def input_text(message: str, default: Optional[str] = None, cli_flag: Optional[str] = None,
+ force_interactive: bool = False) -> Tuple[str, str]:
+ """Accept input from the user.
+
+ :param str message: message to display to the user
+ :param default: default value to return (if one exists)
+ :param str cli_flag: option used to set this value with the CLI
+ :param bool force_interactive: True if it's safe to prompt the user
+ because it won't cause any workflow regressions
+
+ :returns: tuple of (`code`, `input`) where
+ `code` - str display exit code
+ `input` - str of the user's input
+ :rtype: tuple
+
+ """
+ return obj.get_display().input(message, default=default, cli_flag=cli_flag,
+ force_interactive=force_interactive)
+
+
+def yesno(message: str, yes_label: str = "Yes", no_label: str = "No",
+ default: Optional[bool] = None, cli_flag: Optional[str] = None,
+ force_interactive: bool = False) -> bool:
+ """Query the user with a yes/no question.
+
+ Yes and No label must begin with different letters, and must contain at
+ least one letter each.
+
+ :param str message: question for the user
+ :param str yes_label: Label of the "Yes" parameter
+ :param str no_label: Label of the "No" parameter
+ :param default: default value to return (if one exists)
+ :param str cli_flag: option used to set this value with the CLI
+ :param bool force_interactive: True if it's safe to prompt the user
+ because it won't cause any workflow regressions
+
+ :returns: True for "Yes", False for "No"
+ :rtype: bool
+
+ """
+ return obj.get_display().yesno(message, yes_label=yes_label, no_label=no_label, default=default,
+ cli_flag=cli_flag, force_interactive=force_interactive)
+
+
+def checklist(message: str, tags: List[str], default: Optional[str] = None,
+ cli_flag: Optional[str] = None,
+ force_interactive: bool = False) -> Tuple[str, List[str]]:
+ """Display a checklist.
+
+ :param str message: Message to display to user
+ :param list tags: `str` tags to select, len(tags) > 0
+ :param default: default value to return (if one exists)
+ :param str cli_flag: option used to set this value with the CLI
+ :param bool force_interactive: True if it's safe to prompt the user
+ because it won't cause any workflow regressions
+
+ :returns: tuple of (`code`, `tags`) where
+ `code` - str display exit code
+ `tags` - list of selected tags
+ :rtype: tuple
"""
- lines = msg.splitlines()
- fixed_l = []
+ return obj.get_display().checklist(message, tags, default=default, cli_flag=cli_flag,
+ force_interactive=force_interactive)
+
- for line in lines:
- fixed_l.append(textwrap.fill(
- line,
- 80,
- break_long_words=False,
- break_on_hyphens=False))
+def directory_select(message: str, default: Optional[str] = None, cli_flag: Optional[str] = None,
+ force_interactive: bool = False) -> Tuple[int, str]:
+ """Display a directory selection screen.
- return '\n'.join(fixed_l)
+ :param str message: prompt to give the user
+ :param default: default value to return (if one exists)
+ :param str cli_flag: option used to set this value with the CLI
+ :param bool force_interactive: True if it's safe to prompt the user
+ because it won't cause any workflow regressions
+
+ :returns: tuple of the form (`code`, `string`) where
+ `code` - display exit code
+ `string` - input entered by the user
+
+ """
+ return obj.get_display().directory_select(message, default=default, cli_flag=cli_flag,
+ force_interactive=force_interactive)
def input_with_timeout(prompt=None, timeout=36000.0):
@@ -98,366 +216,6 @@ def input_with_timeout(prompt=None, timeout=36000.0):
return line.rstrip('\n')
-def notify(msg: str) -> None:
- """Display a basic status message.
-
- :param str msg: message to display
-
- """
- zope.component.getUtility(interfaces.IDisplay).notification(
- msg, pause=False, decorate=False, wrap=False
- )
-
-
-@zope.interface.implementer(interfaces.IDisplay)
-class FileDisplay:
- """File-based display."""
- # see https://github.com/certbot/certbot/issues/3915
-
- def __init__(self, outfile, force_interactive):
- super().__init__()
- self.outfile = outfile
- self.force_interactive = force_interactive
- self.skipped_interaction = False
-
- def notification(self, message, pause=True,
- wrap=True, force_interactive=False,
- decorate=True):
- """Displays a notification and waits for user acceptance.
-
- :param str message: Message to display
- :param bool pause: Whether or not the program should pause for the
- user's confirmation
- :param bool wrap: Whether or not the application should wrap text
- :param bool force_interactive: True if it's safe to prompt the user
- because it won't cause any workflow regressions
- :param bool decorate: Whether to surround the message with a
- decorated frame
-
- """
- if wrap:
- message = _wrap_lines(message)
-
- logger.debug("Notifying user: %s", message)
-
- self.outfile.write(
- (("{line}{frame}{line}" if decorate else "") +
- "{msg}{line}" +
- ("{frame}{line}" if decorate else ""))
- .format(line=os.linesep, frame=SIDE_FRAME, msg=message)
- )
- self.outfile.flush()
-
- if pause:
- if self._can_interact(force_interactive):
- input_with_timeout("Press Enter to Continue")
- else:
- logger.debug("Not pausing for user confirmation")
-
- def menu(self, message, choices, ok_label=None, cancel_label=None, # pylint: disable=unused-argument
- help_label=None, default=None, # pylint: disable=unused-argument
- cli_flag=None, force_interactive=False, **unused_kwargs):
- """Display a menu.
-
- .. todo:: This doesn't enable the help label/button (I wasn't sold on
- any interface I came up with for this). It would be a nice feature
-
- :param str message: title of menu
- :param choices: Menu lines, len must be > 0
- :type choices: list of tuples (tag, item) or
- list of descriptions (tags will be enumerated)
- :param default: default value to return (if one exists)
- :param str cli_flag: option used to set this value with the CLI
- :param bool force_interactive: True if it's safe to prompt the user
- because it won't cause any workflow regressions
-
- :returns: tuple of (`code`, `index`) where
- `code` - str display exit code
- `index` - int index of the user's selection
-
- :rtype: tuple
-
- """
- if self._return_default(message, default, cli_flag, force_interactive):
- return OK, default
-
- self._print_menu(message, choices)
-
- code, selection = self._get_valid_int_ans(len(choices))
-
- return code, selection - 1
-
- def input(self, message, default=None,
- cli_flag=None, force_interactive=False, **unused_kwargs):
- """Accept input from the user.
-
- :param str message: message to display to the user
- :param default: default value to return (if one exists)
- :param str cli_flag: option used to set this value with the CLI
- :param bool force_interactive: True if it's safe to prompt the user
- because it won't cause any workflow regressions
-
- :returns: tuple of (`code`, `input`) where
- `code` - str display exit code
- `input` - str of the user's input
- :rtype: tuple
-
- """
- if self._return_default(message, default, cli_flag, force_interactive):
- return OK, default
-
- # Trailing space must be added outside of _wrap_lines to be preserved
- message = _wrap_lines("%s (Enter 'c' to cancel):" % message) + " "
- ans = input_with_timeout(message)
-
- if ans in ("c", "C"):
- return CANCEL, "-1"
- return OK, ans
-
- def yesno(self, message, yes_label="Yes", no_label="No", default=None,
- cli_flag=None, force_interactive=False, **unused_kwargs):
- """Query the user with a yes/no question.
-
- Yes and No label must begin with different letters, and must contain at
- least one letter each.
-
- :param str message: question for the user
- :param str yes_label: Label of the "Yes" parameter
- :param str no_label: Label of the "No" parameter
- :param default: default value to return (if one exists)
- :param str cli_flag: option used to set this value with the CLI
- :param bool force_interactive: True if it's safe to prompt the user
- because it won't cause any workflow regressions
-
- :returns: True for "Yes", False for "No"
- :rtype: bool
-
- """
- if self._return_default(message, default, cli_flag, force_interactive):
- return default
-
- message = _wrap_lines(message)
-
- self.outfile.write("{0}{frame}{msg}{0}{frame}".format(
- os.linesep, frame=SIDE_FRAME + os.linesep, msg=message))
- self.outfile.flush()
-
- while True:
- ans = input_with_timeout("{yes}/{no}: ".format(
- yes=_parens_around_char(yes_label),
- no=_parens_around_char(no_label)))
-
- # Couldn't get pylint indentation right with elif
- # elif doesn't matter in this situation
- if (ans.startswith(yes_label[0].lower()) or
- ans.startswith(yes_label[0].upper())):
- return True
- if (ans.startswith(no_label[0].lower()) or
- ans.startswith(no_label[0].upper())):
- return False
-
- def checklist(self, message, tags, default=None,
- cli_flag=None, force_interactive=False, **unused_kwargs):
- """Display a checklist.
-
- :param str message: Message to display to user
- :param list tags: `str` tags to select, len(tags) > 0
- :param default: default value to return (if one exists)
- :param str cli_flag: option used to set this value with the CLI
- :param bool force_interactive: True if it's safe to prompt the user
- because it won't cause any workflow regressions
-
- :returns: tuple of (`code`, `tags`) where
- `code` - str display exit code
- `tags` - list of selected tags
- :rtype: tuple
-
- """
- if self._return_default(message, default, cli_flag, force_interactive):
- return OK, default
-
- while True:
- self._print_menu(message, tags)
-
- code, ans = self.input("Select the appropriate numbers separated "
- "by commas and/or spaces, or leave input "
- "blank to select all options shown",
- force_interactive=True)
-
- if code == OK:
- if not ans.strip():
- ans = " ".join(str(x) for x in range(1, len(tags)+1))
- indices = separate_list_input(ans)
- selected_tags = self._scrub_checklist_input(indices, tags)
- if selected_tags:
- return code, selected_tags
- self.outfile.write(
- "** Error - Invalid selection **%s" % os.linesep)
- self.outfile.flush()
- else:
- return code, []
-
- def _return_default(self, prompt, default, cli_flag, force_interactive):
- """Should we return the default instead of prompting the user?
-
- :param str prompt: prompt for the user
- :param default: default answer to prompt
- :param str cli_flag: command line option for setting an answer
- to this question
- :param bool force_interactive: if interactivity is forced by the
- IDisplay call
-
- :returns: True if we should return the default without prompting
- :rtype: bool
-
- """
- # assert_valid_call(prompt, default, cli_flag, force_interactive)
- if self._can_interact(force_interactive):
- return False
- if default is None:
- msg = "Unable to get an answer for the question:\n{0}".format(prompt)
- if cli_flag:
- msg += (
- "\nYou can provide an answer on the "
- "command line with the {0} flag.".format(cli_flag))
- raise errors.Error(msg)
- logger.debug(
- "Falling back to default %s for the prompt:\n%s",
- default, prompt)
- return True
-
- def _can_interact(self, force_interactive):
- """Can we safely interact with the user?
-
- :param bool force_interactive: if interactivity is forced by the
- IDisplay call
-
- :returns: True if the display can interact with the user
- :rtype: bool
-
- """
- if (self.force_interactive or force_interactive or
- sys.stdin.isatty() and self.outfile.isatty()):
- return True
- if not self.skipped_interaction:
- logger.warning(
- "Skipped user interaction because Certbot doesn't appear to "
- "be running in a terminal. You should probably include "
- "--non-interactive or %s on the command line.",
- constants.FORCE_INTERACTIVE_FLAG)
- self.skipped_interaction = True
- return False
-
- def directory_select(self, message, default=None, cli_flag=None,
- force_interactive=False, **unused_kwargs):
- """Display a directory selection screen.
-
- :param str message: prompt to give the user
- :param default: default value to return (if one exists)
- :param str cli_flag: option used to set this value with the CLI
- :param bool force_interactive: True if it's safe to prompt the user
- because it won't cause any workflow regressions
-
- :returns: tuple of the form (`code`, `string`) where
- `code` - display exit code
- `string` - input entered by the user
-
- """
- with completer.Completer():
- return self.input(message, default, cli_flag, force_interactive)
-
- def _scrub_checklist_input(self, indices, tags):
- """Validate input and transform indices to appropriate tags.
-
- :param list indices: input
- :param list tags: Original tags of the checklist
-
- :returns: valid tags the user selected
- :rtype: :class:`list` of :class:`str`
-
- """
- # They should all be of type int
- try:
- indices = [int(index) for index in indices]
- except ValueError:
- return []
-
- # Remove duplicates
- indices = list(set(indices))
-
- # Check all input is within range
- for index in indices:
- if index < 1 or index > len(tags):
- return []
- # Transform indices to appropriate tags
- return [tags[index - 1] for index in indices]
-
- def _print_menu(self, message, choices):
- """Print a menu on the screen.
-
- :param str message: title of menu
- :param choices: Menu lines
- :type choices: list of tuples (tag, item) or
- list of descriptions (tags will be enumerated)
-
- """
- # Can take either tuples or single items in choices list
- if choices and isinstance(choices[0], tuple):
- choices = ["%s - %s" % (c[0], c[1]) for c in choices]
-
- # Write out the message to the user
- self.outfile.write(
- "{new}{msg}{new}".format(new=os.linesep, msg=message))
- self.outfile.write(SIDE_FRAME + os.linesep)
-
- # Write out the menu choices
- for i, desc in enumerate(choices, 1):
- msg = "{num}: {desc}".format(num=i, desc=desc)
- self.outfile.write(_wrap_lines(msg))
-
- # Keep this outside of the textwrap
- self.outfile.write(os.linesep)
-
- self.outfile.write(SIDE_FRAME + os.linesep)
- self.outfile.flush()
-
- def _get_valid_int_ans(self, max_):
- """Get a numerical selection.
-
- :param int max: The maximum entry (len of choices), must be positive
-
- :returns: tuple of the form (`code`, `selection`) where
- `code` - str display exit code ('ok' or cancel')
- `selection` - int user's selection
- :rtype: tuple
-
- """
- selection = -1
- if max_ > 1:
- input_msg = ("Select the appropriate number "
- "[1-{max_}] then [enter] (press 'c' to "
- "cancel): ".format(max_=max_))
- else:
- input_msg = ("Press 1 [enter] to confirm the selection "
- "(press 'c' to cancel): ")
- while selection < 1:
- ans = input_with_timeout(input_msg)
- if ans.startswith("c") or ans.startswith("C"):
- return CANCEL, -1
- try:
- selection = int(ans)
- if selection < 1 or selection > max_:
- selection = -1
- raise ValueError
-
- except ValueError:
- self.outfile.write(
- "{0}** Invalid input **{0}".format(os.linesep))
- self.outfile.flush()
-
- return OK, selection
-
-
def assert_valid_call(prompt, default, cli_flag, force_interactive):
"""Verify that provided arguments is a valid IDisplay call.
@@ -476,141 +234,6 @@ def assert_valid_call(prompt, default, cli_flag, force_interactive):
assert default is not None or force_interactive, msg
-@zope.interface.implementer(interfaces.IDisplay)
-class NoninteractiveDisplay:
- """An iDisplay implementation that never asks for interactive user input"""
-
- def __init__(self, outfile, *unused_args, **unused_kwargs):
- super().__init__()
- self.outfile = outfile
-
- def _interaction_fail(self, message, cli_flag, extra=""):
- "Error out in case of an attempt to interact in noninteractive mode"
- msg = "Missing command line flag or config entry for this setting:\n"
- msg += message
- if extra:
- msg += "\n" + extra
- if cli_flag:
- msg += "\n\n(You can set this with the {0} flag)".format(cli_flag)
- raise errors.MissingCommandlineFlag(msg)
-
- def notification(self, message, pause=False, wrap=True, decorate=True, **unused_kwargs): # pylint: disable=unused-argument
- """Displays a notification without waiting for user acceptance.
-
- :param str message: Message to display to stdout
- :param bool pause: The NoninteractiveDisplay waits for no keyboard
- :param bool wrap: Whether or not the application should wrap text
- :param bool decorate: Whether to apply a decorated frame to the message
-
- """
- if wrap:
- message = _wrap_lines(message)
-
- logger.debug("Notifying user: %s", message)
-
- self.outfile.write(
- (("{line}{frame}{line}" if decorate else "") +
- "{msg}{line}" +
- ("{frame}{line}" if decorate else ""))
- .format(line=os.linesep, frame=SIDE_FRAME, msg=message)
- )
- self.outfile.flush()
-
- def menu(self, message, choices, ok_label=None, cancel_label=None,
- help_label=None, default=None, cli_flag=None, **unused_kwargs):
- # pylint: disable=unused-argument
- """Avoid displaying a menu.
-
- :param str message: title of menu
- :param choices: Menu lines, len must be > 0
- :type choices: list of tuples (tag, item) or
- list of descriptions (tags will be enumerated)
- :param int default: the default choice
- :param dict kwargs: absorbs various irrelevant labelling arguments
-
- :returns: tuple of (`code`, `index`) where
- `code` - str display exit code
- `index` - int index of the user's selection
- :rtype: tuple
- :raises errors.MissingCommandlineFlag: if there was no default
-
- """
- if default is None:
- self._interaction_fail(message, cli_flag, "Choices: " + repr(choices))
-
- return OK, default
-
- def input(self, message, default=None, cli_flag=None, **unused_kwargs):
- """Accept input from the user.
-
- :param str message: message to display to the user
-
- :returns: tuple of (`code`, `input`) where
- `code` - str display exit code
- `input` - str of the user's input
- :rtype: tuple
- :raises errors.MissingCommandlineFlag: if there was no default
-
- """
- if default is None:
- self._interaction_fail(message, cli_flag)
- return OK, default
-
- def yesno(self, message, yes_label=None, no_label=None, # pylint: disable=unused-argument
- default=None, cli_flag=None, **unused_kwargs):
- """Decide Yes or No, without asking anybody
-
- :param str message: question for the user
- :param dict kwargs: absorbs yes_label, no_label
-
- :raises errors.MissingCommandlineFlag: if there was no default
- :returns: True for "Yes", False for "No"
- :rtype: bool
-
- """
- if default is None:
- self._interaction_fail(message, cli_flag)
- return default
-
- def checklist(self, message, tags, default=None,
- cli_flag=None, **unused_kwargs):
- """Display a checklist.
-
- :param str message: Message to display to user
- :param list tags: `str` tags to select, len(tags) > 0
- :param dict kwargs: absorbs default_status arg
-
- :returns: tuple of (`code`, `tags`) where
- `code` - str display exit code
- `tags` - list of selected tags
- :rtype: tuple
-
- """
- if default is None:
- self._interaction_fail(message, cli_flag, "? ".join(tags))
- return OK, default
-
- def directory_select(self, message, default=None,
- cli_flag=None, **unused_kwargs):
- """Simulate prompting the user for a directory.
-
- This function returns default if it is not ``None``, otherwise,
- an exception is raised explaining the problem. If cli_flag is
- not ``None``, the error message will include the flag that can
- be used to set this value with the CLI.
-
- :param str message: prompt to give the user
- :param default: default value to return (if one exists)
- :param str cli_flag: option used to set this value with the CLI
-
- :returns: tuple of the form (`code`, `string`) where
- `code` - int display exit code
- `string` - input entered by the user
-
- """
- return self.input(message, default, cli_flag)
-
-
def separate_list_input(input_):
"""Separate a comma or space separated list.
@@ -626,15 +249,6 @@ def separate_list_input(input_):
return [str(string) for string in no_commas.split()]
-def _parens_around_char(label):
- """Place parens around first character of label.
-
- :param str label: Must contain at least one character
-
- """
- return "({first}){rest}".format(first=label[0], rest=label[1:])
-
-
def summarize_domain_list(domains: List[str]) -> str:
"""Summarizes a list of domains in the format of:
example.com.com and N more domains
diff --git a/certbot/certbot/interfaces.py b/certbot/certbot/interfaces.py
index de9175def68..854bd72dc20 100644
--- a/certbot/certbot/interfaces.py
+++ b/certbot/certbot/interfaces.py
@@ -1,16 +1,24 @@
"""Certbot client interfaces."""
-import abc
+from abc import ABCMeta
+from abc import abstractmethod
+from argparse import ArgumentParser
+from typing import Iterable
+from typing import List
from typing import Optional
+from typing import Tuple
+from typing import Union
import zope.interface
-# pylint: disable=no-self-argument,no-method-argument,inherit-non-class
+from acme.challenges import Challenge
+from acme.challenges import ChallengeResponse
+from certbot.achallenges import AnnotatedChallenge
-class AccountStorage(object, metaclass=abc.ABCMeta):
+class AccountStorage(metaclass=ABCMeta):
"""Accounts storage interface."""
- @abc.abstractmethod
+ @abstractmethod
def find_all(self): # pragma: no cover
"""Find all accounts.
@@ -20,7 +28,7 @@ def find_all(self): # pragma: no cover
"""
raise NotImplementedError()
- @abc.abstractmethod
+ @abstractmethod
def load(self, account_id): # pragma: no cover
"""Load an account by its id.
@@ -30,7 +38,7 @@ def load(self, account_id): # pragma: no cover
"""
raise NotImplementedError()
- @abc.abstractmethod
+ @abstractmethod
def save(self, account, client): # pragma: no cover
"""Save account.
@@ -40,8 +48,192 @@ def save(self, account, client): # pragma: no cover
raise NotImplementedError()
-class IPluginFactory(zope.interface.Interface):
- """IPlugin factory.
+class IConfig(zope.interface.Interface): # pylint: disable=inherit-non-class
+ """Deprecated, use certbot.interfaces.Config as ABC instead."""
+
+
+@zope.interface.implementer(IConfig)
+class Config(metaclass=ABCMeta):
+ """Certbot user-supplied configuration.
+
+ .. warning:: The values stored in the configuration have not been
+ filtered, stripped or sanitized.
+
+ """
+
+ @property
+ @abstractmethod
+ def server(self) -> str:
+ """ACME Directory Resource URI."""
+
+ @property
+ @abstractmethod
+ def email(self) -> str:
+ """Email used for registration and recovery contact.
+
+ Use comma to register multiple emails,
+ ex: u1@example.com,u2@example.com. (default: Ask).
+ """
+
+ @property
+ @abstractmethod
+ def rsa_key_size(self) -> int:
+ """Size of the RSA key."""
+
+ @property
+ @abstractmethod
+ def elliptic_curve(self) -> str:
+ """The SECG elliptic curve name to use.
+
+ Please see RFC 8446 for supported values.
+ """
+
+ @property
+ @abstractmethod
+ def key_type(self) -> str:
+ """Type of generated private key.
+
+ Only *ONE* per invocation can be provided at this time.
+ """
+
+ @property
+ @abstractmethod
+ def must_staple(self) -> bool:
+ """Adds the OCSP Must Staple extension to the certificate.
+
+ Autoconfigures OCSP Stapling for supported setups
+ (Apache version >= 2.3.3 ).
+ """
+
+ @property
+ @abstractmethod
+ def config_dir(self) -> str:
+ """Configuration directory."""
+
+ @property
+ @abstractmethod
+ def work_dir(self) -> str:
+ """Working directory."""
+
+ @property
+ @abstractmethod
+ def account_dir(self) -> str:
+ """Directory where all account information is stored."""
+
+ @property
+ @abstractmethod
+ def backup_dir(self) -> str:
+ """Configuration backups directory."""
+
+ @property
+ @abstractmethod
+ def csr_dir(self) -> str:
+ """Directory where new Certificate Signing Requests (CSRs) are saved."""
+
+ @property
+ @abstractmethod
+ def in_progress_dir(self) -> str:
+ """Directory used before a permanent checkpoint is finalized."""
+
+ @property
+ @abstractmethod
+ def key_dir(self) -> str:
+ """Keys storage."""
+
+ @property
+ @abstractmethod
+ def temp_checkpoint_dir(self) -> str:
+ """Temporary checkpoint directory."""
+
+ @property
+ @abstractmethod
+ def no_verify_ssl(self) -> bool:
+ """Disable verification of the ACME server's certificate."""
+
+ @property
+ @abstractmethod
+ def http01_port(self) -> int:
+ """Port used in the http-01 challenge.
+
+ This only affects the port Certbot listens on.
+ A conforming ACME server will still attempt to connect on port 80.
+ """
+
+ @property
+ @abstractmethod
+ def http01_address(self) -> str:
+ """The address the server listens to during http-01 challenge."""
+
+ @property
+ @abstractmethod
+ def https_port(self) -> int:
+ """Port used to serve HTTPS.
+
+ This affects which port Nginx will listen on after a LE certificate
+ is installed.
+ """
+
+ @property
+ @abstractmethod
+ def pref_challs(self) -> List[str]:
+ """List of user specified preferred challenges.
+
+ Sorted with the most preferred challenge listed first.
+ """
+
+ @property
+ @abstractmethod
+ def allow_subset_of_names(self) -> bool:
+ """Allow only a subset of names to be authorized to perform validations.
+
+ When performing domain validation, do not consider it a failure
+ if authorizations can not be obtained for a strict subset of
+ the requested domains. This may be useful for allowing renewals for
+ multiple domains to succeed even if some domains no longer point
+ at this system.
+ """
+
+ @property
+ @abstractmethod
+ def strict_permissions(self) -> bool:
+ """Enable strict permissions checks.
+
+ Require that all configuration files are owned by the current
+ user; only needed if your config is somewhere unsafe like /tmp/.
+ """
+
+ @property
+ @abstractmethod
+ def disable_renew_updates(self) -> bool:
+ """Disable renewal updates.
+
+ If updates provided by installer enhancements when Certbot is being run
+ with \"renew\" verb should be disabled.
+ """
+
+ @property
+ @abstractmethod
+ def preferred_chain(self) -> str:
+ """Set the preferred certificate chain to issue a certificate.
+
+ If the CA offers multiple certificate chains, prefer the chain whose
+ topmost certificate was issued from this Subject Common Name.
+ If no match, the default offered chain will be used.
+ """
+
+
+class IPluginFactory(zope.interface.Interface): # pylint: disable=inherit-non-class
+ """Deprecated, use certbot.interfaces.Plugin as ABC instead."""
+
+
+class IPlugin(zope.interface.Interface): # pylint: disable=inherit-non-class
+ """Deprecated, use certbot.interfaces.Plugin as ABC instead."""
+
+
+@zope.interface.implementer(IPlugin)
+@zope.interface.provider(IPluginFactory)
+class Plugin(metaclass=ABCMeta):
+ """Certbot plugin.
Objects providing this interface will be called without satisfying
any entry point "extras" (extra dependencies) you might have defined
@@ -70,35 +262,22 @@ class IPluginFactory(zope.interface.Interface):
"""
- description = zope.interface.Attribute("Short plugin description")
-
- def __call__(config, name): # pylint: disable=signature-differs
- """Create new `IPlugin`.
-
- :param IConfig config: Configuration.
- :param str name: Unique plugin name.
-
- """
-
- def inject_parser_options(parser, name):
- """Inject argument parser options (flags).
+ description: str = NotImplemented
+ """Short plugin description"""
- 1. Be nice and prepend all options and destinations with
- `~.common.option_namespace` and `~common.dest_namespace`.
-
- 2. Inject options (flags) only. Positional arguments are not
- allowed, as this would break the CLI.
+ @abstractmethod
+ def __init__(self, config: Config, name: str):
+ """Create new `Plugin`.
- :param ArgumentParser parser: (Almost) top-level CLI parser.
+ :param Config config: Configuration.
:param str name: Unique plugin name.
"""
+ self.config = config
+ self.name = name
-
-class IPlugin(zope.interface.Interface):
- """Certbot plugin."""
-
- def prepare(): # type: ignore
+ @abstractmethod
+ def prepare(self) -> None:
"""Prepare the plugin.
Finish up any additional initialization.
@@ -117,7 +296,8 @@ def prepare(): # type: ignore
"""
- def more_info(): # type: ignore
+ @abstractmethod
+ def more_info(self) -> str:
"""Human-readable string to help the user.
Should describe the steps taken and any relevant info to help the user
@@ -127,8 +307,29 @@ def more_info(): # type: ignore
"""
+ @classmethod
+ @abstractmethod
+ def inject_parser_options(cls, parser: ArgumentParser, name: str) -> None:
+ """Inject argument parser options (flags).
-class IAuthenticator(IPlugin):
+ 1. Be nice and prepend all options and destinations with
+ `~.common.option_namespace` and `~common.dest_namespace`.
+
+ 2. Inject options (flags) only. Positional arguments are not
+ allowed, as this would break the CLI.
+
+ :param ArgumentParser parser: (Almost) top-level CLI parser.
+ :param str name: Unique plugin name.
+
+ """
+
+
+class IAuthenticator(IPlugin): # pylint: disable=inherit-non-class
+ """Deprecated, use certbot.interfaces.Authenticator as ABC instead."""
+
+
+@zope.interface.implementer(IAuthenticator)
+class Authenticator(Plugin):
"""Generic Certbot Authenticator.
Class represents all possible tools processes that have the
@@ -136,7 +337,8 @@ class IAuthenticator(IPlugin):
"""
- def get_chall_pref(domain):
+ @abstractmethod
+ def get_chall_pref(self, domain: str) -> Iterable[Challenge]:
"""Return `collections.Iterable` of challenge preferences.
:param str domain: Domain for which challenge preferences are sought.
@@ -149,7 +351,8 @@ def get_chall_pref(domain):
"""
- def perform(achalls):
+ @abstractmethod
+ def perform(self, achalls: List[AnnotatedChallenge]) -> Iterable[ChallengeResponse]:
"""Perform the given challenge.
:param list achalls: Non-empty (guaranteed) list of
@@ -169,7 +372,8 @@ def perform(achalls):
"""
- def cleanup(achalls):
+ @abstractmethod
+ def cleanup(self, achalls: List[AnnotatedChallenge]) -> None:
"""Revert changes and shutdown after challenges complete.
This method should be able to revert all changes made by
@@ -184,90 +388,12 @@ def cleanup(achalls):
"""
-class IConfig(zope.interface.Interface):
- """Certbot user-supplied configuration.
+class IInstaller(IPlugin): # pylint: disable=inherit-non-class
+ """Deprecated, use certbot.interfaces.Installer as ABC instead."""
- .. warning:: The values stored in the configuration have not been
- filtered, stripped or sanitized.
- """
- server = zope.interface.Attribute("ACME Directory Resource URI.")
- email = zope.interface.Attribute(
- "Email used for registration and recovery contact. Use comma to "
- "register multiple emails, ex: u1@example.com,u2@example.com. "
- "(default: Ask).")
- rsa_key_size = zope.interface.Attribute("Size of the RSA key.")
- elliptic_curve = zope.interface.Attribute(
- "The SECG elliptic curve name to use. Please see RFC 8446 "
- "for supported values."
- )
- key_type = zope.interface.Attribute(
- "Type of generated private key"
- "(Only *ONE* per invocation can be provided at this time)")
- must_staple = zope.interface.Attribute(
- "Adds the OCSP Must Staple extension to the certificate. "
- "Autoconfigures OCSP Stapling for supported setups "
- "(Apache version >= 2.3.3 ).")
-
- config_dir = zope.interface.Attribute("Configuration directory.")
- work_dir = zope.interface.Attribute("Working directory.")
-
- accounts_dir = zope.interface.Attribute(
- "Directory where all account information is stored.")
- backup_dir = zope.interface.Attribute("Configuration backups directory.")
- csr_dir = zope.interface.Attribute(
- "Directory where newly generated Certificate Signing Requests "
- "(CSRs) are saved.")
- in_progress_dir = zope.interface.Attribute(
- "Directory used before a permanent checkpoint is finalized.")
- key_dir = zope.interface.Attribute("Keys storage.")
- temp_checkpoint_dir = zope.interface.Attribute(
- "Temporary checkpoint directory.")
-
- no_verify_ssl = zope.interface.Attribute(
- "Disable verification of the ACME server's certificate.")
-
- http01_port = zope.interface.Attribute(
- "Port used in the http-01 challenge. "
- "This only affects the port Certbot listens on. "
- "A conforming ACME server will still attempt to connect on port 80.")
-
- http01_address = zope.interface.Attribute(
- "The address the server listens to during http-01 challenge.")
-
- https_port = zope.interface.Attribute(
- "Port used to serve HTTPS. "
- "This affects which port Nginx will listen on after a LE certificate "
- "is installed.")
-
- pref_challs = zope.interface.Attribute(
- "Sorted user specified preferred challenges"
- "type strings with the most preferred challenge listed first")
-
- allow_subset_of_names = zope.interface.Attribute(
- "When performing domain validation, do not consider it a failure "
- "if authorizations can not be obtained for a strict subset of "
- "the requested domains. This may be useful for allowing renewals for "
- "multiple domains to succeed even if some domains no longer point "
- "at this system. This is a boolean")
-
- strict_permissions = zope.interface.Attribute(
- "Require that all configuration files are owned by the current "
- "user; only needed if your config is somewhere unsafe like /tmp/."
- "This is a boolean")
-
- disable_renew_updates = zope.interface.Attribute(
- "If updates provided by installer enhancements when Certbot is being run"
- " with \"renew\" verb should be disabled.")
-
- preferred_chain = zope.interface.Attribute(
- "If the CA offers multiple certificate chains, prefer the chain whose "
- "topmost certificate was issued from this Subject Common Name. "
- "If no match, the default offered chain will be used."
- )
-
-
-class IInstaller(IPlugin):
+@zope.interface.implementer(IInstaller)
+class Installer(Plugin):
"""Generic Certbot Installer Interface.
Represents any server that an X509 certificate can be placed.
@@ -282,14 +408,17 @@ class IInstaller(IPlugin):
"""
- def get_all_names(): # type: ignore
+ @abstractmethod
+ def get_all_names(self) -> Iterable[str]:
"""Returns all names that may be authenticated.
:rtype: `collections.Iterable` of `str`
"""
- def deploy_cert(domain, cert_path, key_path, chain_path, fullchain_path):
+ @abstractmethod
+ def deploy_cert(self, domain: str, cert_path: str, key_path: str,
+ chain_path: str, fullchain_path: str) -> None:
"""Deploy certificate.
:param str domain: domain to deploy certificate file
@@ -303,7 +432,8 @@ def deploy_cert(domain, cert_path, key_path, chain_path, fullchain_path):
"""
- def enhance(domain, enhancement, options=None):
+ @abstractmethod
+ def enhance(self, domain: str, enhancement: str, options: Optional[List[str]] = None) -> None:
"""Perform a configuration enhancement.
:param str domain: domain for which to provide enhancement
@@ -319,7 +449,8 @@ def enhance(domain, enhancement, options=None):
"""
- def supported_enhancements(): # type: ignore
+ @abstractmethod
+ def supported_enhancements(self) -> List[str]:
"""Returns a `collections.Iterable` of supported enhancements.
:returns: supported enhancements which should be a subset of
@@ -328,7 +459,8 @@ def supported_enhancements(): # type: ignore
"""
- def save(title: Optional[str] = None, temporary: bool = False):
+ @abstractmethod
+ def save(self, title: Optional[str] = None, temporary: bool = False) -> None:
"""Saves all changes to the configuration files.
Both title and temporary are needed because a save may be
@@ -350,14 +482,16 @@ def save(title: Optional[str] = None, temporary: bool = False):
"""
- def rollback_checkpoints(rollback: int = 1):
+ @abstractmethod
+ def rollback_checkpoints(self, rollback: int = 1) -> None:
"""Revert `rollback` number of configuration checkpoints.
:raises .PluginError: when configuration cannot be fully reverted
"""
- def recovery_routine(): # type: ignore
+ @abstractmethod
+ def recovery_routine(self) -> None:
"""Revert configuration to most recent finalized checkpoint.
Remove all changes (temporary and permanent) that have not been
@@ -368,14 +502,16 @@ def recovery_routine(): # type: ignore
"""
- def config_test(): # type: ignore
+ @abstractmethod
+ def config_test(self) -> None:
"""Make sure the configuration is valid.
:raises .MisconfigurationError: when the config is not in a usable state
"""
- def restart(): # type: ignore
+ @abstractmethod
+ def restart(self) -> None:
"""Restart or refresh the server content.
:raises .PluginError: when server cannot be restarted
@@ -383,11 +519,18 @@ def restart(): # type: ignore
"""
-class IDisplay(zope.interface.Interface):
+class IDisplay(zope.interface.Interface): # pylint: disable=inherit-non-class
+ """Deprecated, use certbot.interfaces.Display as ABC instead."""
+
+
+@zope.interface.implementer(IDisplay)
+class Display(metaclass=ABCMeta):
"""Generic display."""
# see https://github.com/certbot/certbot/issues/3915
- def notification(message, pause, wrap=True, force_interactive=False):
+ @abstractmethod
+ def notification(self, message: str, pause: bool = False, wrap: bool = True,
+ force_interactive: bool = False, decorate: bool = True):
"""Displays a string message
:param str message: Message to display
@@ -396,12 +539,16 @@ def notification(message, pause, wrap=True, force_interactive=False):
:param bool wrap: Whether or not the application should wrap text
:param bool force_interactive: True if it's safe to prompt the user
because it won't cause any workflow regressions
+ :param bool decorate: Whether to surround the message with a
+ decorated frame
"""
- def menu(message, choices, ok_label=None,
- cancel_label=None, help_label=None,
- default=None, cli_flag=None, force_interactive=False):
+ @abstractmethod
+ def menu(self, message: str, choices: Union[List[str], Tuple[str, str]],
+ ok_label: Optional[str] = None, cancel_label: Optional[str] = None,
+ help_label: Optional[str] = None, default: Optional[int] = None,
+ cli_flag: Optional[str] = None, force_interactive: bool = False) -> Tuple[str, int]:
"""Displays a generic menu.
When not setting force_interactive=True, you must provide a
@@ -409,8 +556,9 @@ def menu(message, choices, ok_label=None,
:param str message: message to display
- :param choices: choices
- :type choices: :class:`list` of :func:`tuple` or :class:`str`
+ :param choices: Menu lines, len must be > 0
+ :type choices: list of tuples (tag, item) or
+ list of descriptions (tags will be enumerated)
:param str ok_label: label for OK button (UNUSED)
:param str cancel_label: label for Cancel button (UNUSED)
@@ -429,7 +577,9 @@ def menu(message, choices, ok_label=None,
"""
- def input(message, default=None, cli_args=None, force_interactive=False):
+ @abstractmethod
+ def input(self, message: str, default: Optional[str] = None, cli_flag: Optional[str] = None,
+ force_interactive: bool = False) -> Tuple[str, str]:
"""Accept input from the user.
When not setting force_interactive=True, you must provide a
@@ -437,6 +587,7 @@ def input(message, default=None, cli_args=None, force_interactive=False):
:param str message: message to display to the user
:param str default: default (non-interactive) response to prompt
+ :param str cli_flag: to automate choice from the menu, eg "--redirect / --no-redirect"
:param bool force_interactive: True if it's safe to prompt the user
because it won't cause any workflow regressions
@@ -450,8 +601,10 @@ def input(message, default=None, cli_args=None, force_interactive=False):
"""
- def yesno(message, yes_label="Yes", no_label="No", default=None,
- cli_args=None, force_interactive=False):
+ @abstractmethod
+ def yesno(self, message: str, yes_label: str = "Yes", no_label: str = "No",
+ default: Optional[bool] = None, cli_flag: Optional[str] = None,
+ force_interactive: bool = False) -> bool:
"""Query the user with a yes/no question.
Yes and No label must begin with different letters.
@@ -460,8 +613,10 @@ def yesno(message, yes_label="Yes", no_label="No", default=None,
default value.
:param str message: question for the user
- :param str default: default (non-interactive) choice from the menu
- :param str cli_flag: to automate choice from the menu, eg "--redirect / --no-redirect"
+ :param str yes_label: label for Yes button
+ :param str no_label: label for No button
+ :param bool default: default (non-interactive) choice from the menu
+ :param str cli_flag: to automate choice from the menu, eg "--agree-tos"
:param bool force_interactive: True if it's safe to prompt the user
because it won't cause any workflow regressions
@@ -473,7 +628,10 @@ def yesno(message, yes_label="Yes", no_label="No", default=None,
"""
- def checklist(message, tags, default=None, cli_args=None, force_interactive=False):
+ @abstractmethod
+ def checklist(self, message: str, tags: List[str], default: Optional[str] = None,
+ cli_flag: Optional[str] = None,
+ force_interactive: bool = False) -> Tuple[str, List[str]]:
"""Allow for multiple selections from a menu.
When not setting force_interactive=True, you must provide a
@@ -487,7 +645,7 @@ def checklist(message, tags, default=None, cli_args=None, force_interactive=Fals
because it won't cause any workflow regressions
:returns: tuple of the form (code, list_tags) where
- `code` - int display exit code
+ `code` - str display exit code
`list_tags` - list of str tags selected by the user
:rtype: tuple
@@ -496,8 +654,10 @@ def checklist(message, tags, default=None, cli_args=None, force_interactive=Fals
"""
- def directory_select(self, message, default=None,
- cli_flag=None, force_interactive=False):
+ @abstractmethod
+ def directory_select(self, message: str, default: Optional[str] = None,
+ cli_flag: Optional[str] = None,
+ force_interactive: bool = False) -> Tuple[int, str]:
"""Display a directory selection screen.
When not setting force_interactive=True, you must provide a
@@ -519,17 +679,23 @@ def directory_select(self, message, default=None,
"""
-class IReporter(zope.interface.Interface):
+class IReporter(zope.interface.Interface): # pylint: disable=inherit-non-class
+ """Deprecated, use certbot.interfaces.Reporter as ABC instead."""
+
+
+@zope.interface.implementer(IReporter)
+class Reporter(metaclass=ABCMeta):
"""Interface to collect and display information to the user."""
- HIGH_PRIORITY = zope.interface.Attribute(
- "Used to denote high priority messages")
- MEDIUM_PRIORITY = zope.interface.Attribute(
- "Used to denote medium priority messages")
- LOW_PRIORITY = zope.interface.Attribute(
- "Used to denote low priority messages")
+ HIGH_PRIORITY: int = 0
+ """High priority constant. See `add_message`."""
+ MEDIUM_PRIORITY: int = 1
+ """Medium priority constant. See `add_message`."""
+ LOW_PRIORITY: int = 2
+ """Low priority constant. See `add_message`."""
- def add_message(self, msg, priority, on_crash=True):
+ @abstractmethod
+ def add_message(self, msg: str, priority: int, on_crash: bool = True) -> None:
"""Adds msg to the list of messages to be printed.
:param str msg: Message to be displayed to the user.
@@ -542,14 +708,16 @@ def add_message(self, msg, priority, on_crash=True):
"""
- def print_messages(self):
+ @abstractmethod
+ def print_messages(self) -> str:
"""Prints messages to the user and clears the message queue."""
-class RenewableCert(object, metaclass=abc.ABCMeta):
+class RenewableCert(metaclass=ABCMeta):
"""Interface to a certificate lineage."""
- @abc.abstractproperty
+ @property
+ @abstractmethod
def cert_path(self):
"""Path to the certificate file.
@@ -557,7 +725,8 @@ def cert_path(self):
"""
- @abc.abstractproperty
+ @property
+ @abstractmethod
def key_path(self):
"""Path to the private key file.
@@ -565,7 +734,8 @@ def key_path(self):
"""
- @abc.abstractproperty
+ @property
+ @abstractmethod
def chain_path(self):
"""Path to the certificate chain file.
@@ -573,7 +743,8 @@ def chain_path(self):
"""
- @abc.abstractproperty
+ @property
+ @abstractmethod
def fullchain_path(self):
"""Path to the full chain file.
@@ -583,7 +754,8 @@ def fullchain_path(self):
"""
- @abc.abstractproperty
+ @property
+ @abstractmethod
def lineagename(self):
"""Name given to the certificate lineage.
@@ -591,7 +763,7 @@ def lineagename(self):
"""
- @abc.abstractmethod
+ @abstractmethod
def names(self):
"""What are the subject names of this certificate?
@@ -611,7 +783,8 @@ def names(self):
# an update during the run or install subcommand, it should do so when
# :func:`IInstaller.deploy_cert` is called.
-class GenericUpdater(object, metaclass=abc.ABCMeta):
+
+class GenericUpdater(metaclass=ABCMeta):
"""Interface for update types not currently specified by Certbot.
This class allows plugins to perform types of updates that Certbot hasn't
@@ -626,7 +799,7 @@ class GenericUpdater(object, metaclass=abc.ABCMeta):
interface methods of `interfaces.IInstaller` such as prepare() and restart()
"""
- @abc.abstractmethod
+ @abstractmethod
def generic_updates(self, lineage, *args, **kwargs):
"""Perform any update types defined by the installer.
@@ -643,7 +816,7 @@ def generic_updates(self, lineage, *args, **kwargs):
"""
-class RenewDeployer(object, metaclass=abc.ABCMeta):
+class RenewDeployer(metaclass=ABCMeta):
"""Interface for update types run when a lineage is renewed
This class allows plugins to perform types of updates that need to run at
@@ -654,7 +827,7 @@ class RenewDeployer(object, metaclass=abc.ABCMeta):
be called from the installer code.
"""
- @abc.abstractmethod
+ @abstractmethod
def renew_deploy(self, lineage, *args, **kwargs):
"""Perform updates defined by installer when a certificate has been renewed
diff --git a/certbot/certbot/plugins/common.py b/certbot/certbot/plugins/common.py
index 3181d7b5002..421964e1604 100644
--- a/certbot/certbot/plugins/common.py
+++ b/certbot/certbot/plugins/common.py
@@ -1,4 +1,5 @@
"""Plugin common functions."""
+from abc import ABCMeta
import logging
import re
import shutil
@@ -7,16 +8,16 @@
from josepy import util as jose_util
import pkg_resources
-import zope.interface
from certbot import achallenges
from certbot import crypto_util
from certbot import errors
-from certbot import interfaces
from certbot import reverter
from certbot._internal import constants
from certbot.compat import filesystem
from certbot.compat import os
+from certbot.interfaces import Installer as AbstractInstaller
+from certbot.interfaces import Plugin as AbstractPlugin
from certbot.plugins.storage import PluginStorage
logger = logging.getLogger(__name__)
@@ -39,15 +40,11 @@ def dest_namespace(name):
r"^(([a-z0-9]|[a-z0-9][a-z0-9\-]*[a-z0-9])\.)*[a-z]+$", re.IGNORECASE)
-@zope.interface.implementer(interfaces.IPlugin)
-class Plugin:
+class Plugin(AbstractPlugin, metaclass=ABCMeta): # pylint: disable=abstract-method
"""Generic plugin."""
- # provider is not inherited, subclasses must define it on their own
- # @zope.interface.provider(interfaces.IPluginFactory)
- def __init__(self, config, name):
- self.config = config
- self.name = name
+ def __init__(self, config, name): # pylint: disable=useless-super-delegation
+ super().__init__(config, name)
@jose_util.abstractclassmethod
def add_parser_arguments(cls, add):
@@ -119,13 +116,13 @@ def auth_hint(self, failed_achalls):
# 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 exterally verify that the {name} plugin "
+ 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):
+class Installer(AbstractInstaller, Plugin, metaclass=ABCMeta): # pylint: disable=abstract-method
"""An installer base class with reverter and ssl_dhparam methods defined.
Installer plugins do not have to inherit from this class.
diff --git a/certbot/certbot/plugins/dns_common.py b/certbot/certbot/plugins/dns_common.py
index 23e25554434..a9acac7b12e 100644
--- a/certbot/certbot/plugins/dns_common.py
+++ b/certbot/certbot/plugins/dns_common.py
@@ -5,7 +5,6 @@
from time import sleep
import configobj
-import zope.interface
from acme import challenges
from certbot import errors
@@ -19,9 +18,7 @@
logger = logging.getLogger(__name__)
-@zope.interface.implementer(interfaces.IAuthenticator)
-@zope.interface.provider(interfaces.IPluginFactory)
-class DNSAuthenticator(common.Plugin):
+class DNSAuthenticator(common.Plugin, interfaces.Authenticator, metaclass=abc.ABCMeta):
"""Base class for DNS Authenticators"""
def __init__(self, config, name):
@@ -169,7 +166,7 @@ def _configure_credentials(self, key, label, required_variables=None,
indicate any issue.
"""
- def __validator(filename):
+ def __validator(filename): # pylint: disable=unused-private-member
configuration = CredentialsConfiguration(filename, self.dest)
if required_variables:
@@ -199,7 +196,7 @@ def _prompt_for_data(label):
: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 +222,7 @@ def _prompt_for_file(label, validator=None):
: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))
diff --git a/certbot/certbot/plugins/dns_test_common_lexicon.py b/certbot/certbot/plugins/dns_test_common_lexicon.py
index 0b1375cc1b1..3292eaf8098 100644
--- a/certbot/certbot/plugins/dns_test_common_lexicon.py
+++ b/certbot/certbot/plugins/dns_test_common_lexicon.py
@@ -67,7 +67,7 @@ def assertRaises(self, *unused_args) -> None:
class BaseLexiconAuthenticatorTest(dns_test_common.BaseAuthenticatorTest):
- @test_util.patch_get_utility()
+ @test_util.patch_display_util()
def test_perform(self: _AuthenticatorCallableLexiconTestCase, unused_mock_get_utility):
self.auth.perform([self.achall])
diff --git a/certbot/certbot/tests/util.py b/certbot/certbot/tests/util.py
index 41f412e7222..01f6ce6baee 100644
--- a/certbot/certbot/tests/util.py
+++ b/certbot/certbot/tests/util.py
@@ -7,6 +7,9 @@
import shutil
import sys
import tempfile
+from typing import Iterable
+from typing import List
+from typing import Optional
import unittest
import warnings
@@ -25,6 +28,7 @@
from certbot.compat import filesystem
from certbot.compat import os
from certbot.display import util as display_util
+from certbot.plugins import common
try:
# When we remove this deprecated import, we should also remove the
@@ -37,10 +41,44 @@
"use unittest.mock. Be sure to update your code accordingly.",
PendingDeprecationWarning
)
-except ImportError: # pragma: no cover
- from unittest import mock # type: ignore
+except ImportError: # pragma: no cover
+ from unittest import mock # type: ignore
+class DummyInstaller(common.Installer):
+ """Dummy installer plugin for test purpose."""
+ def get_all_names(self) -> Iterable[str]:
+ pass
+
+ def deploy_cert(self, domain: str, cert_path: str, key_path: str, chain_path: str,
+ fullchain_path: str) -> None:
+ pass
+
+ def enhance(self, domain: str, enhancement: str, options: Optional[List[str]] = None) -> None:
+ pass
+
+ def supported_enhancements(self) -> List[str]:
+ pass
+
+ def save(self, title: Optional[str] = None, temporary: bool = False) -> None:
+ pass
+
+ def config_test(self) -> None:
+ pass
+
+ def restart(self) -> None:
+ pass
+
+ @classmethod
+ def add_parser_arguments(cls, add):
+ pass
+
+ def prepare(self) -> None:
+ pass
+
+ def more_info(self) -> str:
+ pass
+
def vector_path(*names):
"""Path to a test vector."""
@@ -150,42 +188,97 @@ def make_lineage(config_dir, testfile, ec=False):
def patch_get_utility(target='zope.component.getUtility'):
- """Patch zope.component.getUtility to use a special mock IDisplay.
-
- The mock IDisplay works like a regular mock object, except it also
- also asserts that methods are called with valid arguments.
+ """Deprecated, patch certbot.display.util directly or use patch_display_util instead.
:param str target: path to patch
- :returns: mock zope.component.getUtility
+ :returns: mock certbot.display.util.get_display
:rtype: mock.MagicMock
"""
- return mock.patch(target, new_callable=_create_get_utility_mock)
+ warnings.warn('Decorator certbot.tests.util.patch_get_utility is deprecated. You should now '
+ 'patch certbot.display.util yourself directly or use '
+ 'certbot.tests.util.patch_display_util as a temporary workaround.')
+ return mock.patch(target, new_callable=_create_display_util_mock)
def patch_get_utility_with_stdout(target='zope.component.getUtility',
stdout=None):
- """Patch zope.component.getUtility to use a special mock IDisplay.
+ """Deprecated, patch certbot.display.util directly
+ or use patch_display_util_with_stdout instead.
+
+ :param str target: path to patch
+ :param object stdout: object to write standard output to; it is
+ expected to have a `write` method
+
+ :returns: mock zope.component.getUtility
+ :rtype: mock.MagicMock
+
+ """
+ warnings.warn('Decorator certbot.tests.util.patch_get_utility_with_stdout is deprecated. You '
+ 'should now patch certbot.display.util yourself directly or use '
+ 'use certbot.tests.util.patch_display_util_with_stdout as a temporary '
+ 'workaround.')
+ stdout = stdout if stdout else io.StringIO()
+ freezable_mock = _create_display_util_mock_with_stdout(stdout)
+ return mock.patch(target, new=freezable_mock)
+
+
+def patch_display_util():
+ """Patch certbot.display.util to use a special mock IDisplay.
The mock IDisplay works like a regular mock object, except it also
also asserts that methods are called with valid arguments.
+ The mock created by this patch mocks out Certbot internals so this can be
+ used like the old patch_get_utility function. That is, the mock object will
+ be called by the certbot.display.util functions and the mock returned by
+ that call will be used as the IDisplay object. This was done to simplify
+ the transition from zope.component and mocking certbot.display.util
+ functions directly in test code should be preferred over using this
+ function in the future.
+
+ See https://github.com/certbot/certbot/issues/8948
+
+ :returns: patch on the function used internally by certbot.display.util to
+ get an IDisplay object
+ :rtype: unittest.mock._patch
+
+ """
+ return mock.patch('certbot._internal.display.obj.get_display',
+ new_callable=_create_display_util_mock)
+
+
+def patch_display_util_with_stdout(stdout=None):
+ """Patch certbot.display.util to use a special mock IDisplay.
+
+ The mock IDisplay works like a regular mock object, except it also
+ asserts that methods are called with valid arguments.
+
+ The mock created by this patch mocks out Certbot internals so this can be
+ used like the old patch_get_utility function. That is, the mock object will
+ be called by the certbot.display.util functions and the mock returned by
+ that call will be used as the IDisplay object. This was done to simplify
+ the transition from zope.component and mocking certbot.display.util
+ functions directly in test code should be preferred over using this
+ function in the future.
+
+ See https://github.com/certbot/certbot/issues/8948
+
The `message` argument passed to the IDisplay methods is passed to
stdout's write method.
- :param str target: path to patch
:param object stdout: object to write standard output to; it is
expected to have a `write` method
-
- :returns: mock zope.component.getUtility
- :rtype: mock.MagicMock
+ :returns: patch on the function used internally by certbot.display.util to
+ get an IDisplay object
+ :rtype: unittest.mock._patch
"""
stdout = stdout if stdout else io.StringIO()
- freezable_mock = _create_get_utility_mock_with_stdout(stdout)
- return mock.patch(target, new=freezable_mock)
+ return mock.patch('certbot._internal.display.obj.get_display',
+ new=_create_display_util_mock_with_stdout(stdout))
class FreezableMock:
@@ -256,18 +349,20 @@ def __setattr__(self, name, value):
return object.__setattr__(self, name, value)
-def _create_get_utility_mock():
+def _create_display_util_mock():
display = FreezableMock()
# Use pylint code for disable to keep on single line under line length limit
- for name in interfaces.IDisplay.names():
- if name != 'notification':
+ method_list = [func for func in dir(interfaces.Display)
+ if callable(getattr(interfaces.Display, func)) and not func.startswith("__")]
+ for method in method_list:
+ if method != 'notification':
frozen_mock = FreezableMock(frozen=True, func=_assert_valid_call)
- setattr(display, name, frozen_mock)
+ setattr(display, method, frozen_mock)
display.freeze()
return FreezableMock(frozen=True, return_value=display)
-def _create_get_utility_mock_with_stdout(stdout):
+def _create_display_util_mock_with_stdout(stdout):
def _write_msg(message, *unused_args, **unused_kwargs):
"""Write to message to stdout.
"""
@@ -281,20 +376,19 @@ def mock_method(*args, **kwargs):
_assert_valid_call(args, kwargs)
_write_msg(*args, **kwargs)
-
display = FreezableMock()
# Use pylint code for disable to keep on single line under line length limit
- for name in interfaces.IDisplay.names():
- if name == 'notification':
+ method_list = [func for func in dir(interfaces.Display)
+ if callable(getattr(interfaces.Display, func)) and not func.startswith("__")]
+ for method in method_list:
+ if method == 'notification':
frozen_mock = FreezableMock(frozen=True,
func=_write_msg)
- setattr(display, name, frozen_mock)
else:
frozen_mock = FreezableMock(frozen=True,
func=mock_method)
- setattr(display, name, frozen_mock)
+ setattr(display, method, frozen_mock)
display.freeze()
-
return FreezableMock(frozen=True, return_value=display)
@@ -338,14 +432,14 @@ def setUp(self):
self.config = configuration.NamespaceConfig(
mock.MagicMock(**constants.CLI_DEFAULTS)
)
- self.config.verb = "certonly"
- self.config.config_dir = os.path.join(self.tempdir, 'config')
- self.config.work_dir = os.path.join(self.tempdir, 'work')
- self.config.logs_dir = os.path.join(self.tempdir, 'logs')
- self.config.cert_path = constants.CLI_DEFAULTS['auth_cert_path']
- self.config.fullchain_path = constants.CLI_DEFAULTS['auth_chain_path']
- self.config.chain_path = constants.CLI_DEFAULTS['auth_chain_path']
- self.config.server = "https://example.com"
+ self.config.namespace.verb = "certonly"
+ self.config.namespace.config_dir = os.path.join(self.tempdir, 'config')
+ self.config.namespace.work_dir = os.path.join(self.tempdir, 'work')
+ self.config.namespace.logs_dir = os.path.join(self.tempdir, 'logs')
+ self.config.namespace.cert_path = constants.CLI_DEFAULTS['auth_cert_path']
+ self.config.namespace.fullchain_path = constants.CLI_DEFAULTS['auth_chain_path']
+ self.config.namespace.chain_path = constants.CLI_DEFAULTS['auth_chain_path']
+ self.config.namespace.server = "https://example.com"
def _handle_lock(event_in, event_out, path):
diff --git a/certbot/docs/cli-help.txt b/certbot/docs/cli-help.txt
index 7025073043b..76d337878ad 100644
--- a/certbot/docs/cli-help.txt
+++ b/certbot/docs/cli-help.txt
@@ -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,
diff --git a/certbot/docs/using.rst b/certbot/docs/using.rst
index 1c68c0ac107..8c1a0ac548e 100644
--- a/certbot/docs/using.rst
+++ b/certbot/docs/using.rst
@@ -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 `
@@ -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 `.
@@ -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.
diff --git a/certbot/examples/dev-cli.ini b/certbot/examples/dev-cli.ini
index a405a0aefda..21f7db85c8f 100644
--- a/certbot/examples/dev-cli.ini
+++ b/certbot/examples/dev-cli.ini
@@ -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-level = 2 # -vv (debug)
authenticator = standalone
diff --git a/certbot/examples/plugins/certbot_example_plugins.py b/certbot/examples/plugins/certbot_example_plugins.py
index 9dec2e108a7..8828c30d1b6 100644
--- a/certbot/examples/plugins/certbot_example_plugins.py
+++ b/certbot/examples/plugins/certbot_example_plugins.py
@@ -3,15 +3,11 @@
For full examples, see `certbot.plugins`.
"""
-import zope.interface
-
from certbot import interfaces
from certbot.plugins import common
-@zope.interface.implementer(interfaces.IAuthenticator)
-@zope.interface.provider(interfaces.IPluginFactory)
-class Authenticator(common.Plugin):
+class Authenticator(common.Plugin, interfaces.Authenticator):
"""Example Authenticator."""
description = "Example Authenticator plugin"
@@ -20,9 +16,7 @@ class Authenticator(common.Plugin):
# "self" as first argument, e.g. def prepare(self)...
-@zope.interface.implementer(interfaces.IInstaller)
-@zope.interface.provider(interfaces.IPluginFactory)
-class Installer(common.Plugin):
+class Installer(common.Plugin, interfaces.Installer):
"""Example Installer."""
description = "Example Installer plugin"
diff --git a/certbot/setup.py b/certbot/setup.py
index eba0db3d7d6..99c92a0b8d4 100644
--- a/certbot/setup.py
+++ b/certbot/setup.py
@@ -67,22 +67,13 @@ def read_file(filename, encoding='utf8'):
]
dev_extras = [
- 'astroid',
'azure-devops',
- 'coverage',
'ipdb',
- 'mypy',
'PyGithub',
- # 1.1.0+ is required for poetry to use the poetry-core library for the
- # build system declared in tools/pinning/pyproject.toml.
- 'poetry>=1.1.0',
- 'pylint',
- 'pytest',
- 'pytest-cov',
- 'pytest-xdist',
- # typing-extensions is required to import typing.Protocol and make the mypy checks
- # pass (along with pylint about non-existent objects) on Python 3.6 & 3.7
- 'typing-extensions',
+ 'pip',
+ # poetry 1.2.0+ is required for it to pin pip, setuptools, and wheel. See
+ # https://github.com/python-poetry/poetry/issues/1584.
+ 'poetry>=1.2.0a1',
'tox',
'twine',
'wheel',
@@ -96,6 +87,21 @@ def read_file(filename, encoding='utf8'):
'sphinx_rtd_theme',
]
+test_extras = [
+ 'coverage',
+ 'mypy',
+ 'pylint',
+ 'pytest',
+ 'pytest-cov',
+ 'pytest-xdist',
+ # typing-extensions is required to import typing.Protocol and make the mypy checks
+ # pass (along with pylint about non-existent objects) on Python 3.6 & 3.7
+ 'typing-extensions',
+]
+
+
+all_extras = dev_extras + docs_extras + test_extras
+
setup(
name='certbot',
version=version,
@@ -132,8 +138,10 @@ def read_file(filename, encoding='utf8'):
install_requires=install_requires,
extras_require={
+ 'all': all_extras,
'dev': dev_extras,
'docs': docs_extras,
+ 'test': test_extras,
},
entry_points={
diff --git a/certbot/tests/auth_handler_test.py b/certbot/tests/auth_handler_test.py
index 25b19def70b..ad09067a1cd 100644
--- a/certbot/tests/auth_handler_test.py
+++ b/certbot/tests/auth_handler_test.py
@@ -5,9 +5,8 @@
try:
import mock
-except ImportError: # pragma: no cover
+except ImportError: # pragma: no cover
from unittest import mock
-import zope.component
from acme import challenges
from acme import client as acme_client
@@ -15,8 +14,8 @@
from acme import messages
from certbot import achallenges
from certbot import errors
-from certbot import interfaces
from certbot import util
+from certbot._internal.display import obj as display_obj
from certbot.plugins import common as plugin_common
from certbot.tests import acme_util
from certbot.tests import util as test_util
@@ -70,8 +69,8 @@ def setUp(self):
self.mock_display = mock.Mock()
self.mock_config = mock.Mock(debug_challenges=False)
- zope.component.provideUtility(
- self.mock_display, interfaces.IDisplay)
+ with mock.patch("zope.component.provideUtility"):
+ display_obj.set_display(self.mock_display)
self.mock_auth = mock.MagicMock(name="ApacheConfigurator")
@@ -307,7 +306,7 @@ def test_incomplete_authzr_error(self):
mock_order = mock.MagicMock(authorizations=authzrs)
self.mock_net.poll.side_effect = _gen_mock_on_poll(status=messages.STATUS_INVALID)
- with test_util.patch_get_utility():
+ with test_util.patch_display_util():
with self.assertRaises(errors.AuthorizationError) as error:
self.handler.handle_authorizations(mock_order, self.mock_config, False)
self.assertIn('Some challenges have failed.', str(error.exception))
@@ -342,7 +341,7 @@ def _conditional_mock_on_poll(authzr):
self.mock_net.poll.side_effect = _gen_mock_on_poll(status=messages.STATUS_INVALID)
- with test_util.patch_get_utility():
+ with test_util.patch_display_util():
with self.assertRaises(errors.AuthorizationError) as error:
self.handler.handle_authorizations(mock_order, self.mock_config, True)
diff --git a/certbot/tests/cert_manager_test.py b/certbot/tests/cert_manager_test.py
index 5f2a91cb41e..d4b2c14e766 100644
--- a/certbot/tests/cert_manager_test.py
+++ b/certbot/tests/cert_manager_test.py
@@ -113,7 +113,7 @@ def _call(self):
from certbot._internal import cert_manager
cert_manager.delete(self.config)
- @test_util.patch_get_utility()
+ @test_util.patch_display_util()
@mock.patch('certbot.display.util.notify')
@mock.patch('certbot._internal.cert_manager.lineage_for_certname')
@mock.patch('certbot._internal.storage.delete_files')
@@ -129,7 +129,7 @@ def test_delete_from_config_yes(self, mock_delete_files, mock_lineage_for_certna
"Deleted all files relating to certificate example.org."
)
- @test_util.patch_get_utility()
+ @test_util.patch_display_util()
@mock.patch('certbot._internal.cert_manager.lineage_for_certname')
@mock.patch('certbot._internal.storage.delete_files')
def test_delete_from_config_no(self, mock_delete_files, mock_lineage_for_certname,
@@ -141,7 +141,7 @@ def test_delete_from_config_no(self, mock_delete_files, mock_lineage_for_certnam
self._call()
self.assertEqual(mock_delete_files.call_count, 0)
- @test_util.patch_get_utility()
+ @test_util.patch_display_util()
@mock.patch('certbot._internal.cert_manager.lineage_for_certname')
@mock.patch('certbot._internal.storage.delete_files')
def test_delete_interactive_single_yes(self, mock_delete_files, mock_lineage_for_certname,
@@ -153,7 +153,7 @@ def test_delete_interactive_single_yes(self, mock_delete_files, mock_lineage_for
self._call()
mock_delete_files.assert_called_once_with(self.config, "example.org")
- @test_util.patch_get_utility()
+ @test_util.patch_display_util()
@mock.patch('certbot._internal.cert_manager.lineage_for_certname')
@mock.patch('certbot._internal.storage.delete_files')
def test_delete_interactive_single_no(self, mock_delete_files, mock_lineage_for_certname,
@@ -165,7 +165,7 @@ def test_delete_interactive_single_no(self, mock_delete_files, mock_lineage_for_
self._call()
self.assertEqual(mock_delete_files.call_count, 0)
- @test_util.patch_get_utility()
+ @test_util.patch_display_util()
@mock.patch('certbot._internal.cert_manager.lineage_for_certname')
@mock.patch('certbot._internal.storage.delete_files')
def test_delete_interactive_multiple_yes(self, mock_delete_files, mock_lineage_for_certname,
@@ -179,7 +179,7 @@ def test_delete_interactive_multiple_yes(self, mock_delete_files, mock_lineage_f
mock_delete_files.assert_any_call(self.config, "other.org")
self.assertEqual(mock_delete_files.call_count, 2)
- @test_util.patch_get_utility()
+ @test_util.patch_display_util()
@mock.patch('certbot._internal.cert_manager.lineage_for_certname')
@mock.patch('certbot._internal.storage.delete_files')
def test_delete_interactive_multiple_no(self, mock_delete_files, mock_lineage_for_certname,
@@ -200,14 +200,14 @@ def _certificates(self, *args, **kwargs):
return certificates(*args, **kwargs)
@mock.patch('certbot._internal.cert_manager.logger')
- @test_util.patch_get_utility()
+ @test_util.patch_display_util()
def test_certificates_parse_fail(self, mock_utility, mock_logger):
self._certificates(self.config)
self.assertTrue(mock_logger.warning.called) #pylint: disable=no-member
self.assertTrue(mock_utility.called)
@mock.patch('certbot._internal.cert_manager.logger')
- @test_util.patch_get_utility()
+ @test_util.patch_display_util()
def test_certificates_quiet(self, mock_utility, mock_logger):
self.config.quiet = True
self._certificates(self.config)
@@ -216,7 +216,7 @@ def test_certificates_quiet(self, mock_utility, mock_logger):
@mock.patch('certbot.crypto_util.verify_renewable_cert')
@mock.patch('certbot._internal.cert_manager.logger')
- @test_util.patch_get_utility()
+ @test_util.patch_display_util()
@mock.patch("certbot._internal.storage.RenewableCert")
@mock.patch('certbot._internal.cert_manager._report_human_readable')
def test_certificates_parse_success(self, mock_report, mock_renewable_cert,
@@ -230,7 +230,7 @@ def test_certificates_parse_success(self, mock_report, mock_renewable_cert,
self.assertTrue(mock_renewable_cert.called)
@mock.patch('certbot._internal.cert_manager.logger')
- @test_util.patch_get_utility()
+ @test_util.patch_display_util()
def test_certificates_no_files(self, mock_utility, mock_logger):
empty_tempdir = tempfile.mkdtemp()
empty_config = configuration.NamespaceConfig(mock.MagicMock(
@@ -408,7 +408,7 @@ def _call(self, *args, **kwargs):
return cert_manager.rename_lineage(*args, **kwargs)
@mock.patch('certbot._internal.storage.renewal_conf_files')
- @test_util.patch_get_utility()
+ @test_util.patch_display_util()
def test_no_certname(self, mock_get_utility, mock_renewal_conf_files):
self.config.certname = None
self.config.new_certname = "two"
@@ -425,7 +425,7 @@ def test_no_certname(self, mock_get_utility, mock_renewal_conf_files):
util_mock.menu.return_value = (display_util.OK, -1)
self.assertRaises(errors.Error, self._call, self.config)
- @test_util.patch_get_utility()
+ @test_util.patch_display_util()
def test_no_new_certname(self, mock_get_utility):
self.config.certname = "one"
self.config.new_certname = None
@@ -437,7 +437,7 @@ def test_no_new_certname(self, mock_get_utility):
util_mock.input.return_value = (display_util.OK, None)
self.assertRaises(errors.Error, self._call, self.config)
- @test_util.patch_get_utility()
+ @test_util.patch_display_util()
@mock.patch('certbot._internal.cert_manager.lineage_for_certname')
def test_no_existing_certname(self, mock_lineage_for_certname, unused_get_utility):
self.config.certname = "one"
@@ -446,7 +446,7 @@ def test_no_existing_certname(self, mock_lineage_for_certname, unused_get_utilit
self.assertRaises(errors.ConfigurationError,
self._call, self.config)
- @test_util.patch_get_utility()
+ @test_util.patch_display_util()
@mock.patch("certbot._internal.storage.RenewableCert._check_symlinks")
def test_rename_cert(self, mock_check, unused_get_utility):
mock_check.return_value = True
@@ -456,7 +456,7 @@ def test_rename_cert(self, mock_check, unused_get_utility):
self.assertIsNotNone(updated_lineage)
self.assertEqual(updated_lineage.lineagename, self.config.new_certname)
- @test_util.patch_get_utility()
+ @test_util.patch_display_util()
@mock.patch("certbot._internal.storage.RenewableCert._check_symlinks")
def test_rename_cert_interactive_certname(self, mock_check, mock_get_utility):
mock_check.return_value = True
@@ -469,7 +469,7 @@ def test_rename_cert_interactive_certname(self, mock_check, mock_get_utility):
self.assertIsNotNone(updated_lineage)
self.assertEqual(updated_lineage.lineagename, self.config.new_certname)
- @test_util.patch_get_utility()
+ @test_util.patch_display_util()
@mock.patch("certbot._internal.storage.RenewableCert._check_symlinks")
def test_rename_cert_bad_new_certname(self, mock_check, unused_get_utility):
mock_check.return_value = True
@@ -619,7 +619,7 @@ class GetCertnameTest(unittest.TestCase):
"""Tests for certbot._internal.cert_manager."""
def setUp(self):
- get_utility_patch = test_util.patch_get_utility()
+ get_utility_patch = test_util.patch_display_util()
self.mock_get_utility = get_utility_patch.start()
self.addCleanup(get_utility_patch.stop)
self.config = mock.MagicMock()
diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py
index 8cab7a5b174..514351f32b7 100644
--- a/certbot/tests/cli_test.py
+++ b/certbot/tests/cli_test.py
@@ -86,7 +86,7 @@ def _unmocked_parse(*args, **kwargs):
@staticmethod
def parse(*args, **kwargs):
"""Mocks zope.component.getUtility and calls _unmocked_parse."""
- with test_util.patch_get_utility():
+ with test_util.patch_display_util():
return ParseTest._unmocked_parse(*args, **kwargs)
def _help_output(self, args):
@@ -98,7 +98,7 @@ def write_msg(message, *args, **kwargs): # pylint: disable=missing-docstring,unu
output.write(message)
with mock.patch('certbot._internal.main.sys.stdout', new=output):
- with test_util.patch_get_utility() as mock_get_utility:
+ with test_util.patch_display_util() as mock_get_utility:
mock_get_utility().notification.side_effect = write_msg
with mock.patch('certbot._internal.main.sys.stderr'):
self.assertRaises(SystemExit, self._unmocked_parse, args, output)
@@ -519,7 +519,7 @@ def test_webroot_map(self):
def _call_set_by_cli(var, args, verb):
with mock.patch('certbot._internal.cli.helpful_parser') as mock_parser:
- with test_util.patch_get_utility():
+ with test_util.patch_display_util():
mock_parser.args = args
mock_parser.verb = verb
return cli.set_by_cli(var)
diff --git a/certbot/tests/client_test.py b/certbot/tests/client_test.py
index 51c6767f68e..4d4c3036f0f 100644
--- a/certbot/tests/client_test.py
+++ b/certbot/tests/client_test.py
@@ -3,25 +3,29 @@
import shutil
import tempfile
import unittest
+from unittest.mock import MagicMock
from josepy import interfaces
-try:
- import mock
-except ImportError: # pragma: no cover
- from unittest import mock
from certbot import errors
from certbot import util
+from certbot._internal.display import obj as display_obj
from certbot._internal import account
from certbot.compat import os
import certbot.tests.util as test_util
+try:
+ import mock
+except ImportError: # pragma: no cover
+ from unittest import mock
+
KEY = test_util.load_vector("rsa512_key.pem")
CSR_SAN = test_util.load_vector("csr-san_512.pem")
# pylint: disable=line-too-long
+
class DetermineUserAgentTest(test_util.ConfigTestCase):
"""Tests for certbot._internal.client.determine_user_agent."""
@@ -62,6 +66,8 @@ def setUp(self):
self.config.register_unsafely_without_email = False
self.config.email = "alias@example.com"
self.account_storage = account.AccountMemoryStorage()
+ with mock.patch("zope.component.provideUtility"):
+ display_obj.set_display(MagicMock())
def _call(self):
from certbot._internal.client import register
@@ -99,7 +105,7 @@ def test_no_tos(self):
self._call()
self.assertIs(mock_prepare.called, True)
- @test_util.patch_get_utility()
+ @test_util.patch_display_util()
def test_it(self, unused_mock_get_utility):
with mock.patch("certbot._internal.client.acme_client.BackwardsCompatibleClientV2") as mock_client:
mock_client().external_account_required.side_effect = self._false_mock
@@ -160,7 +166,7 @@ def test_dry_run_no_staging_account(self, mock_get_email):
# check Certbot created an account with no email. Contact should return empty
self.assertFalse(mock_client().new_account_and_tos.call_args[0][0].contact)
- @test_util.patch_get_utility()
+ @test_util.patch_display_util()
def test_with_eab_arguments(self, unused_mock_get_utility):
with mock.patch("certbot._internal.client.acme_client.BackwardsCompatibleClientV2") as mock_client:
mock_client().client.directory.__getitem__ = mock.Mock(
@@ -176,7 +182,7 @@ def test_with_eab_arguments(self, unused_mock_get_utility):
self.assertIs(mock_eab_from_data.called, True)
- @test_util.patch_get_utility()
+ @test_util.patch_display_util()
def test_without_eab_arguments(self, unused_mock_get_utility):
with mock.patch("certbot._internal.client.acme_client.BackwardsCompatibleClientV2") as mock_client:
mock_client().external_account_required.side_effect = self._false_mock
@@ -409,7 +415,7 @@ def test_obtain_certificate_dry_run_authz_deactivations_failed(self, mock_acme_c
# Certificate should get issued despite one failed deactivation
self.eg_order.authorizations = authzrs
self.client.auth_handler.handle_authorizations.return_value = authzrs
- with test_util.patch_get_utility():
+ with test_util.patch_display_util():
result = self.client.obtain_certificate(self.eg_domains)
self.assertEqual(result, (mock.sentinel.cert, mock.sentinel.chain, key, csr))
self._check_obtain_certificate(1)
@@ -453,7 +459,7 @@ def _test_obtain_certificate_common(self, key, csr, authzr_ret=None, auth_count=
self.eg_order.authorizations = authzr
self.client.auth_handler.handle_authorizations.return_value = authzr
- with test_util.patch_get_utility():
+ with test_util.patch_display_util():
result = self.client.obtain_certificate(self.eg_domains)
self.assertEqual(
@@ -519,7 +525,7 @@ def test_save_certificate(self, mock_parser):
shutil.rmtree(tmp_path)
- @test_util.patch_get_utility()
+ @test_util.patch_display_util()
def test_deploy_certificate_success(self, mock_util):
self.assertRaises(errors.Error, self.client.deploy_certificate,
["foo.bar"], "key", "cert", "chain", "fullchain")
@@ -538,7 +544,7 @@ def test_deploy_certificate_success(self, mock_util):
installer.restart.assert_called_once_with()
@mock.patch('certbot._internal.client.display_util.notify')
- @test_util.patch_get_utility()
+ @test_util.patch_display_util()
def test_deploy_certificate_failure(self, mock_util, mock_notify):
installer = mock.MagicMock()
self.client.installer = installer
@@ -552,7 +558,7 @@ def test_deploy_certificate_failure(self, mock_util, mock_notify):
mock_notify.assert_any_call('Deploying certificate')
- @test_util.patch_get_utility()
+ @test_util.patch_display_util()
def test_deploy_certificate_save_failure(self, mock_util):
installer = mock.MagicMock()
self.client.installer = installer
@@ -563,7 +569,7 @@ def test_deploy_certificate_save_failure(self, mock_util):
installer.recovery_routine.assert_called_once_with()
@mock.patch('certbot._internal.client.display_util.notify')
- @test_util.patch_get_utility()
+ @test_util.patch_display_util()
def test_deploy_certificate_restart_failure(self, mock_get_utility, mock_notify):
installer = mock.MagicMock()
installer.restart.side_effect = [errors.PluginError, None]
@@ -578,7 +584,7 @@ def test_deploy_certificate_restart_failure(self, mock_get_utility, mock_notify)
self.assertEqual(installer.restart.call_count, 2)
@mock.patch('certbot._internal.client.logger')
- @test_util.patch_get_utility()
+ @test_util.patch_display_util()
def test_deploy_certificate_restart_failure2(self, mock_get_utility, mock_logger):
installer = mock.MagicMock()
installer.restart.side_effect = errors.PluginError
@@ -706,13 +712,13 @@ def _test_error_with_rollback(self):
def _test_error(self, enhance_error=False, restart_error=False):
self.config.redirect = True
with mock.patch('certbot._internal.client.logger') as mock_logger, \
- test_util.patch_get_utility() as mock_gu:
+ test_util.patch_display_util() as mock_gu:
self.assertRaises(
errors.PluginError, self._test_with_all_supported)
if enhance_error:
self.assertEqual(mock_logger.error.call_count, 1)
- self.assertIn('Unable to set enhancement', mock_logger.error.call_args_list[0][0][0])
+ self.assertEqual('Unable to set the %s enhancement for %s.', mock_logger.error.call_args_list[0][0][0])
if restart_error:
mock_logger.critical.assert_called_with(
'Rolling back to previous server configuration...')
diff --git a/certbot/tests/display/obj_test.py b/certbot/tests/display/obj_test.py
new file mode 100644
index 00000000000..bdb31367814
--- /dev/null
+++ b/certbot/tests/display/obj_test.py
@@ -0,0 +1,373 @@
+"""Test :mod:`certbot._internal.display.obj`."""
+import inspect
+import unittest
+from unittest import mock
+
+from certbot import errors, interfaces
+from certbot._internal.display import obj as display_obj
+from certbot.display import util as display_util
+
+CHOICES = [("First", "Description1"), ("Second", "Description2")]
+TAGS = ["tag1", "tag2", "tag3"]
+
+
+class FileOutputDisplayTest(unittest.TestCase):
+ """Test stdout display.
+
+ Most of this class has to deal with visual output. In order to test how the
+ functions look to a user, uncomment the test_visual function.
+
+ """
+ def setUp(self):
+ super().setUp()
+ self.mock_stdout = mock.MagicMock()
+ self.displayer = display_obj.FileDisplay(self.mock_stdout, False)
+
+ @mock.patch("certbot._internal.display.obj.logger")
+ def test_notification_no_pause(self, mock_logger):
+ self.displayer.notification("message", False)
+ string = self.mock_stdout.write.call_args[0][0]
+
+ self.assertIn("message", string)
+ mock_logger.debug.assert_called_with("Notifying user: %s", "message")
+
+ def test_notification_pause(self):
+ input_with_timeout = "certbot.display.util.input_with_timeout"
+ with mock.patch(input_with_timeout, return_value="enter"):
+ self.displayer.notification("message", force_interactive=True)
+
+ self.assertIn("message", self.mock_stdout.write.call_args[0][0])
+
+ def test_notification_noninteractive(self):
+ self._force_noninteractive(self.displayer.notification, "message")
+ string = self.mock_stdout.write.call_args[0][0]
+ self.assertIn("message", string)
+
+ def test_notification_noninteractive2(self):
+ # The main purpose of this test is to make sure we only call
+ # logger.warning once which _force_noninteractive checks internally
+ self._force_noninteractive(self.displayer.notification, "message")
+ string = self.mock_stdout.write.call_args[0][0]
+ self.assertIn("message", string)
+
+ self.assertTrue(self.displayer.skipped_interaction)
+
+ self._force_noninteractive(self.displayer.notification, "message2")
+ string = self.mock_stdout.write.call_args[0][0]
+ self.assertIn("message2", string)
+
+ def test_notification_decoration(self):
+ from certbot.compat import os
+ self.displayer.notification("message", pause=False, decorate=False)
+ string = self.mock_stdout.write.call_args[0][0]
+ self.assertEqual(string, "message" + os.linesep)
+
+ self.displayer.notification("message2", pause=False)
+ string = self.mock_stdout.write.call_args[0][0]
+ self.assertIn("- - - ", string)
+ self.assertIn("message2" + os.linesep, string)
+
+ @mock.patch("certbot.display.util."
+ "FileDisplay._get_valid_int_ans")
+ def test_menu(self, mock_ans):
+ mock_ans.return_value = (display_util.OK, 1)
+ ret = self.displayer.menu("message", CHOICES, force_interactive=True)
+ self.assertEqual(ret, (display_util.OK, 0))
+
+ def test_menu_noninteractive(self):
+ default = 0
+ result = self._force_noninteractive(
+ self.displayer.menu, "msg", CHOICES, default=default)
+ self.assertEqual(result, (display_util.OK, default))
+
+ def test_input_cancel(self):
+ input_with_timeout = "certbot.display.util.input_with_timeout"
+ with mock.patch(input_with_timeout, return_value="c"):
+ code, _ = self.displayer.input("message", force_interactive=True)
+
+ self.assertTrue(code, display_util.CANCEL)
+
+ def test_input_normal(self):
+ input_with_timeout = "certbot.display.util.input_with_timeout"
+ with mock.patch(input_with_timeout, return_value="domain.com"):
+ code, input_ = self.displayer.input("message", force_interactive=True)
+
+ self.assertEqual(code, display_util.OK)
+ self.assertEqual(input_, "domain.com")
+
+ def test_input_noninteractive(self):
+ default = "foo"
+ code, input_ = self._force_noninteractive(
+ self.displayer.input, "message", default=default)
+
+ self.assertEqual(code, display_util.OK)
+ self.assertEqual(input_, default)
+
+ def test_input_assertion_fail(self):
+ # If the call to util.assert_valid_call is commented out, an
+ # error.Error is raised, otherwise, an AssertionError is raised.
+ self.assertRaises(Exception, self._force_noninteractive,
+ self.displayer.input, "message", cli_flag="--flag")
+
+ def test_input_assertion_fail2(self):
+ with mock.patch("certbot.display.util.assert_valid_call"):
+ self.assertRaises(errors.Error, self._force_noninteractive,
+ self.displayer.input, "msg", cli_flag="--flag")
+
+ def test_yesno(self):
+ input_with_timeout = "certbot.display.util.input_with_timeout"
+ with mock.patch(input_with_timeout, return_value="Yes"):
+ self.assertTrue(self.displayer.yesno(
+ "message", force_interactive=True))
+ with mock.patch(input_with_timeout, return_value="y"):
+ self.assertTrue(self.displayer.yesno(
+ "message", force_interactive=True))
+ with mock.patch(input_with_timeout, side_effect=["maybe", "y"]):
+ self.assertTrue(self.displayer.yesno(
+ "message", force_interactive=True))
+ with mock.patch(input_with_timeout, return_value="No"):
+ self.assertFalse(self.displayer.yesno(
+ "message", force_interactive=True))
+ with mock.patch(input_with_timeout, side_effect=["cancel", "n"]):
+ self.assertFalse(self.displayer.yesno(
+ "message", force_interactive=True))
+
+ with mock.patch(input_with_timeout, return_value="a"):
+ self.assertTrue(self.displayer.yesno(
+ "msg", yes_label="Agree", force_interactive=True))
+
+ def test_yesno_noninteractive(self):
+ self.assertTrue(self._force_noninteractive(
+ self.displayer.yesno, "message", default=True))
+
+ @mock.patch("certbot.display.util.input_with_timeout")
+ def test_checklist_valid(self, mock_input):
+ mock_input.return_value = "2 1"
+ code, tag_list = self.displayer.checklist(
+ "msg", TAGS, force_interactive=True)
+ self.assertEqual(
+ (code, set(tag_list)), (display_util.OK, {"tag1", "tag2"}))
+
+ @mock.patch("certbot.display.util.input_with_timeout")
+ def test_checklist_empty(self, mock_input):
+ mock_input.return_value = ""
+ code, tag_list = self.displayer.checklist("msg", TAGS, force_interactive=True)
+ self.assertEqual(
+ (code, set(tag_list)), (display_util.OK, {"tag1", "tag2", "tag3"}))
+
+ @mock.patch("certbot.display.util.input_with_timeout")
+ def test_checklist_miss_valid(self, mock_input):
+ mock_input.side_effect = ["10", "tag1 please", "1"]
+
+ ret = self.displayer.checklist("msg", TAGS, force_interactive=True)
+ self.assertEqual(ret, (display_util.OK, ["tag1"]))
+
+ @mock.patch("certbot.display.util.input_with_timeout")
+ def test_checklist_miss_quit(self, mock_input):
+ mock_input.side_effect = ["10", "c"]
+
+ ret = self.displayer.checklist("msg", TAGS, force_interactive=True)
+ self.assertEqual(ret, (display_util.CANCEL, []))
+
+ def test_checklist_noninteractive(self):
+ default = TAGS
+ code, input_ = self._force_noninteractive(
+ self.displayer.checklist, "msg", TAGS, default=default)
+
+ self.assertEqual(code, display_util.OK)
+ self.assertEqual(input_, default)
+
+ def test_scrub_checklist_input_valid(self):
+ # pylint: disable=protected-access
+ indices = [
+ ["1"],
+ ["1", "2", "1"],
+ ["2", "3"],
+ ]
+ exp = [
+ {"tag1"},
+ {"tag1", "tag2"},
+ {"tag2", "tag3"},
+ ]
+ for i, list_ in enumerate(indices):
+ set_tags = set(
+ self.displayer._scrub_checklist_input(list_, TAGS))
+ self.assertEqual(set_tags, exp[i])
+
+ @mock.patch("certbot.display.util.input_with_timeout")
+ def test_directory_select(self, mock_input):
+ args = ["msg", "/var/www/html", "--flag", True]
+ user_input = "/var/www/html"
+ mock_input.return_value = user_input
+
+ returned = self.displayer.directory_select(*args)
+ self.assertEqual(returned, (display_util.OK, user_input))
+
+ def test_directory_select_noninteractive(self):
+ default = "/var/www/html"
+ code, input_ = self._force_noninteractive(
+ self.displayer.directory_select, "msg", default=default)
+
+ self.assertEqual(code, display_util.OK)
+ self.assertEqual(input_, default)
+
+ def _force_noninteractive(self, func, *args, **kwargs):
+ skipped_interaction = self.displayer.skipped_interaction
+
+ with mock.patch("certbot.display.util.sys.stdin") as mock_stdin:
+ mock_stdin.isatty.return_value = False
+ with mock.patch("certbot._internal.display.obj.logger") as mock_logger:
+ result = func(*args, **kwargs)
+
+ if skipped_interaction:
+ self.assertIs(mock_logger.warning.called, False)
+ else:
+ self.assertEqual(mock_logger.warning.call_count, 1)
+
+ return result
+
+ def test_scrub_checklist_input_invalid(self):
+ # pylint: disable=protected-access
+ indices = [
+ ["0"],
+ ["4"],
+ ["tag1"],
+ ["1", "tag1"],
+ ["2", "o"]
+ ]
+ for list_ in indices:
+ self.assertEqual(
+ self.displayer._scrub_checklist_input(list_, TAGS), [])
+
+ def test_print_menu(self):
+ # pylint: disable=protected-access
+ # This is purely cosmetic... just make sure there aren't any exceptions
+ self.displayer._print_menu("msg", CHOICES)
+ self.displayer._print_menu("msg", TAGS)
+
+ def test_wrap_lines(self):
+ # pylint: disable=protected-access
+ msg = ("This is just a weak test{0}"
+ "This function is only meant to be for easy viewing{0}"
+ "Test a really really really really really really really really "
+ "really really really really long line...".format('\n'))
+ text = display_obj._wrap_lines(msg)
+
+ self.assertEqual(text.count('\n'), 3)
+
+ def test_get_valid_int_ans_valid(self):
+ # pylint: disable=protected-access
+ input_with_timeout = "certbot.display.util.input_with_timeout"
+ with mock.patch(input_with_timeout, return_value="1"):
+ self.assertEqual(
+ self.displayer._get_valid_int_ans(1), (display_util.OK, 1))
+ ans = "2"
+ with mock.patch(input_with_timeout, return_value=ans):
+ self.assertEqual(
+ self.displayer._get_valid_int_ans(3),
+ (display_util.OK, int(ans)))
+
+ def test_get_valid_int_ans_invalid(self):
+ # pylint: disable=protected-access
+ answers = [
+ ["0", "c"],
+ ["4", "one", "C"],
+ ["c"],
+ ]
+ input_with_timeout = "certbot.display.util.input_with_timeout"
+ for ans in answers:
+ with mock.patch(input_with_timeout, side_effect=ans):
+ self.assertEqual(
+ self.displayer._get_valid_int_ans(3),
+ (display_util.CANCEL, -1))
+
+ def test_methods_take_force_interactive(self):
+ # Every IDisplay method implemented by FileDisplay must take
+ # force_interactive to prevent workflow regressions.
+ for name in interfaces.IDisplay.names():
+ arg_spec = inspect.getfullargspec(getattr(self.displayer, name))
+ self.assertIn("force_interactive", arg_spec.args)
+
+
+class NoninteractiveDisplayTest(unittest.TestCase):
+ """Test non-interactive display. These tests are pretty easy!"""
+ def setUp(self):
+ self.mock_stdout = mock.MagicMock()
+ self.displayer = display_util.NoninteractiveDisplay(self.mock_stdout)
+
+ @mock.patch("certbot._internal.display.obj.logger")
+ def test_notification_no_pause(self, mock_logger):
+ self.displayer.notification("message", 10)
+ string = self.mock_stdout.write.call_args[0][0]
+
+ self.assertIn("message", string)
+ mock_logger.debug.assert_called_with("Notifying user: %s", "message")
+
+ def test_notification_decoration(self):
+ from certbot.compat import os
+ self.displayer.notification("message", pause=False, decorate=False)
+ string = self.mock_stdout.write.call_args[0][0]
+ self.assertEqual(string, "message" + os.linesep)
+
+ self.displayer.notification("message2", pause=False)
+ string = self.mock_stdout.write.call_args[0][0]
+ self.assertTrue("- - - " in string and ("message2" + os.linesep) in string)
+
+ def test_input(self):
+ d = "an incomputable value"
+ ret = self.displayer.input("message", default=d)
+ self.assertEqual(ret, (display_util.OK, d))
+ self.assertRaises(errors.MissingCommandlineFlag, self.displayer.input, "message")
+
+ def test_menu(self):
+ ret = self.displayer.menu("message", CHOICES, default=1)
+ self.assertEqual(ret, (display_util.OK, 1))
+ self.assertRaises(errors.MissingCommandlineFlag, self.displayer.menu, "message", CHOICES)
+
+ def test_yesno(self):
+ d = False
+ ret = self.displayer.yesno("message", default=d)
+ self.assertEqual(ret, d)
+ self.assertRaises(errors.MissingCommandlineFlag, self.displayer.yesno, "message")
+
+ def test_checklist(self):
+ d = [1, 3]
+ ret = self.displayer.checklist("message", TAGS, default=d)
+ self.assertEqual(ret, (display_util.OK, d))
+ self.assertRaises(errors.MissingCommandlineFlag, self.displayer.checklist, "message", TAGS)
+
+ def test_directory_select(self):
+ default = "/var/www/html"
+ expected = (display_util.OK, default)
+ actual = self.displayer.directory_select("msg", default)
+ self.assertEqual(expected, actual)
+
+ self.assertRaises(
+ errors.MissingCommandlineFlag, self.displayer.directory_select, "msg")
+
+ def test_methods_take_kwargs(self):
+ # Every IDisplay method implemented by NoninteractiveDisplay
+ # should take **kwargs because every method of FileDisplay must
+ # take force_interactive which doesn't apply to
+ # NoninteractiveDisplay.
+
+ # Use pylint code for disable to keep on single line under line length limit
+ for name in interfaces.IDisplay.names(): # pylint: disable=E1120
+ method = getattr(self.displayer, name)
+ # asserts method accepts arbitrary keyword arguments
+ result = inspect.getfullargspec(method).varkw
+ self.assertIsNotNone(result)
+
+
+class PlaceParensTest(unittest.TestCase):
+ @classmethod
+ def _call(cls, label): # pylint: disable=protected-access
+ from certbot._internal.display.obj import _parens_around_char
+ return _parens_around_char(label)
+
+ def test_single_letter(self):
+ self.assertEqual("(a)", self._call("a"))
+
+ def test_multiple(self):
+ self.assertEqual("(L)abel", self._call("Label"))
+ self.assertEqual("(y)es please", self._call("yes please"))
diff --git a/certbot/tests/display/ops_test.py b/certbot/tests/display/ops_test.py
index ea51effc363..cf96f224e9e 100644
--- a/certbot/tests/display/ops_test.py
+++ b/certbot/tests/display/ops_test.py
@@ -4,14 +4,10 @@
import unittest
import josepy as jose
-try:
- import mock
-except ImportError: # pragma: no cover
- from unittest import mock
-import zope.component
from acme import messages
from certbot import errors
+from certbot._internal.display import obj as display_obj
from certbot._internal import account
from certbot.compat import filesystem
from certbot.compat import os
@@ -19,6 +15,12 @@
from certbot.display import util as display_util
import certbot.tests.util as test_util
+try:
+ import mock
+except ImportError: # pragma: no cover
+ from unittest import mock
+
+
KEY = jose.JWKRSA.load(test_util.load_vector("rsa512_key.pem"))
@@ -30,14 +32,14 @@ def _call(cls, **kwargs):
from certbot.display.ops import get_email
return get_email(**kwargs)
- @test_util.patch_get_utility("certbot.display.ops.z_util")
+ @test_util.patch_display_util()
def test_cancel_none(self, mock_get_utility):
mock_input = mock_get_utility().input
mock_input.return_value = (display_util.CANCEL, "foo@bar.baz")
self.assertRaises(errors.Error, self._call)
self.assertRaises(errors.Error, self._call, optional=False)
- @test_util.patch_get_utility("certbot.display.ops.z_util")
+ @test_util.patch_display_util()
def test_ok_safe(self, mock_get_utility):
mock_input = mock_get_utility().input
mock_input.return_value = (display_util.OK, "foo@bar.baz")
@@ -45,7 +47,7 @@ def test_ok_safe(self, mock_get_utility):
mock_safe_email.return_value = True
self.assertEqual(self._call(), "foo@bar.baz")
- @test_util.patch_get_utility("certbot.display.ops.z_util")
+ @test_util.patch_display_util()
def test_ok_not_safe(self, mock_get_utility):
mock_input = mock_get_utility().input
mock_input.return_value = (display_util.OK, "foo@bar.baz")
@@ -53,7 +55,7 @@ def test_ok_not_safe(self, mock_get_utility):
mock_safe_email.side_effect = [False, True]
self.assertEqual(self._call(), "foo@bar.baz")
- @test_util.patch_get_utility("certbot.display.ops.z_util")
+ @test_util.patch_display_util()
def test_invalid_flag(self, mock_get_utility):
invalid_txt = "There seem to be problems"
mock_input = mock_get_utility().input
@@ -65,7 +67,7 @@ def test_invalid_flag(self, mock_get_utility):
self._call(invalid=True)
self.assertIn(invalid_txt, mock_input.call_args[0][0])
- @test_util.patch_get_utility("certbot.display.ops.z_util")
+ @test_util.patch_display_util()
def test_optional_flag(self, mock_get_utility):
mock_input = mock_get_utility().input
mock_input.return_value = (display_util.OK, "foo@bar.baz")
@@ -75,7 +77,7 @@ def test_optional_flag(self, mock_get_utility):
for call in mock_input.call_args_list:
self.assertNotIn("--register-unsafely-without-email", call[0][0])
- @test_util.patch_get_utility("certbot.display.ops.z_util")
+ @test_util.patch_display_util()
def test_optional_invalid_unsafe(self, mock_get_utility):
invalid_txt = "There seem to be problems"
mock_input = mock_get_utility().input
@@ -91,8 +93,7 @@ class ChooseAccountTest(test_util.TempDirTestCase):
def setUp(self):
super().setUp()
- zope.component.provideUtility(display_util.FileDisplay(sys.stdout,
- False))
+ display_obj.set_display(display_obj.FileDisplay(sys.stdout, False))
self.account_keys_dir = os.path.join(self.tempdir, "keys")
filesystem.makedirs(self.account_keys_dir, 0o700)
@@ -114,17 +115,17 @@ def setUp(self):
def _call(cls, accounts):
return ops.choose_account(accounts)
- @test_util.patch_get_utility("certbot.display.ops.z_util")
+ @test_util.patch_display_util()
def test_one(self, mock_util):
mock_util().menu.return_value = (display_util.OK, 0)
self.assertEqual(self._call([self.acc1]), self.acc1)
- @test_util.patch_get_utility("certbot.display.ops.z_util")
+ @test_util.patch_display_util()
def test_two(self, mock_util):
mock_util().menu.return_value = (display_util.OK, 1)
self.assertEqual(self._call([self.acc1, self.acc2]), self.acc2)
- @test_util.patch_get_utility("certbot.display.ops.z_util")
+ @test_util.patch_display_util()
def test_cancel(self, mock_util):
mock_util().menu.return_value = (display_util.CANCEL, 1)
self.assertIsNone(self._call([self.acc1, self.acc2]))
@@ -133,8 +134,7 @@ def test_cancel(self, mock_util):
class GenHttpsNamesTest(unittest.TestCase):
"""Test _gen_https_names."""
def setUp(self):
- zope.component.provideUtility(display_util.FileDisplay(sys.stdout,
- False))
+ display_obj.set_display(display_obj.FileDisplay(sys.stdout, False))
@classmethod
def _call(cls, domains):
@@ -181,8 +181,7 @@ def test_four(self):
class ChooseNamesTest(unittest.TestCase):
"""Test choose names."""
def setUp(self):
- zope.component.provideUtility(display_util.FileDisplay(sys.stdout,
- False))
+ display_obj.set_display(display_obj.FileDisplay(sys.stdout, False))
self.mock_install = mock.MagicMock()
@classmethod
@@ -195,12 +194,12 @@ def test_no_installer(self, mock_manual):
self._call(None)
self.assertEqual(mock_manual.call_count, 1)
- @test_util.patch_get_utility("certbot.display.ops.z_util")
+ @test_util.patch_display_util()
def test_no_installer_cancel(self, mock_util):
mock_util().input.return_value = (display_util.CANCEL, [])
self.assertEqual(self._call(None), [])
- @test_util.patch_get_utility("certbot.display.ops.z_util")
+ @test_util.patch_display_util()
def test_no_names_choose(self, mock_util):
self.mock_install().get_all_names.return_value = set()
domain = "example.com"
@@ -249,7 +248,7 @@ def test_sort_names_many(self):
self.assertEqual(_sort_names(to_sort), sortd)
- @test_util.patch_get_utility("certbot.display.ops.z_util")
+ @test_util.patch_display_util()
def test_filter_names_valid_return(self, mock_util):
self.mock_install.get_all_names.return_value = {"example.com"}
mock_util().checklist.return_value = (display_util.OK, ["example.com"])
@@ -258,7 +257,7 @@ def test_filter_names_valid_return(self, mock_util):
self.assertEqual(names, ["example.com"])
self.assertEqual(mock_util().checklist.call_count, 1)
- @test_util.patch_get_utility("certbot.display.ops.z_util")
+ @test_util.patch_display_util()
def test_filter_namees_override_question(self, mock_util):
self.mock_install.get_all_names.return_value = {"example.com"}
mock_util().checklist.return_value = (display_util.OK, ["example.com"])
@@ -267,14 +266,14 @@ def test_filter_namees_override_question(self, mock_util):
self.assertEqual(mock_util().checklist.call_count, 1)
self.assertEqual(mock_util().checklist.call_args[0][0], "Custom")
- @test_util.patch_get_utility("certbot.display.ops.z_util")
+ @test_util.patch_display_util()
def test_filter_names_nothing_selected(self, mock_util):
self.mock_install.get_all_names.return_value = {"example.com"}
mock_util().checklist.return_value = (display_util.OK, [])
self.assertEqual(self._call(self.mock_install), [])
- @test_util.patch_get_utility("certbot.display.ops.z_util")
+ @test_util.patch_display_util()
def test_filter_names_cancel(self, mock_util):
self.mock_install.get_all_names.return_value = {"example.com"}
mock_util().checklist.return_value = (
@@ -293,7 +292,7 @@ def test_get_valid_domains(self):
self.assertEqual(get_valid_domains(all_invalid), [])
self.assertEqual(len(get_valid_domains(two_valid)), 2)
- @test_util.patch_get_utility("certbot.display.ops.z_util")
+ @test_util.patch_display_util()
def test_choose_manually(self, mock_util):
from certbot.display.ops import _choose_names_manually
utility_mock = mock_util()
@@ -320,7 +319,7 @@ def test_choose_manually(self, mock_util):
["example.com", "under_score.example.com",
"justtld", "valid.example.com"])
- @test_util.patch_get_utility("certbot.display.ops.z_util")
+ @test_util.patch_display_util()
def test_choose_manually_retry(self, mock_util):
from certbot.display.ops import _choose_names_manually
utility_mock = mock_util()
@@ -339,10 +338,10 @@ def _call(cls, names):
from certbot.display.ops import success_installation
success_installation(names)
- @test_util.patch_get_utility("certbot.display.util.notify")
- @test_util.patch_get_utility("certbot.display.ops.z_util")
- def test_success_installation(self, mock_util, mock_notify):
- mock_util().notification.return_value = None
+ @test_util.patch_display_util()
+ @mock.patch("certbot.display.util.notify")
+ def test_success_installation(self, mock_notify, mock_display):
+ mock_display().notification.return_value = None
names = ["example.com", "abc.com"]
self._call(names)
@@ -361,16 +360,17 @@ def _call(cls, names):
from certbot.display.ops import success_renewal
success_renewal(names)
- @test_util.patch_get_utility("certbot.display.util.notify")
- @test_util.patch_get_utility("certbot.display.ops.z_util")
- def test_success_renewal(self, mock_util, mock_notify):
- mock_util().notification.return_value = None
+ @test_util.patch_display_util()
+ @mock.patch("certbot.display.util.notify")
+ def test_success_renewal(self, mock_notify, mock_display):
+ mock_display().notification.return_value = None
names = ["example.com", "abc.com"]
self._call(names)
self.assertEqual(mock_notify.call_count, 1)
+
class SuccessRevocationTest(unittest.TestCase):
"""Test the success revocation message."""
@classmethod
@@ -378,9 +378,9 @@ def _call(cls, path):
from certbot.display.ops import success_revocation
success_revocation(path)
- @test_util.patch_get_utility("certbot.display.ops.z_util")
+ @test_util.patch_display_util()
@mock.patch("certbot.display.util.notify")
- def test_success_revocation(self, mock_notify, unused_mock_util):
+ def test_success_revocation(self, mock_notify, unused_mock_display):
path = "/path/to/cert.pem"
self._call(path)
mock_notify.assert_called_once_with(
@@ -402,7 +402,7 @@ def __validator(m):
if m == "":
raise errors.PluginError(ValidatorTests.__ERROR)
- @test_util.patch_get_utility()
+ @test_util.patch_display_util()
def test_input_blank_with_validator(self, mock_util):
mock_util().input.side_effect = [(display_util.OK, ""),
(display_util.OK, ""),
@@ -413,14 +413,14 @@ def test_input_blank_with_validator(self, mock_util):
self.assertEqual(ValidatorTests.__ERROR, mock_util().notification.call_args[0][0])
self.assertEqual(returned, (display_util.OK, self.valid_input))
- @test_util.patch_get_utility()
+ @test_util.patch_display_util()
def test_input_validation_with_default(self, mock_util):
mock_util().input.side_effect = [(display_util.OK, self.valid_input)]
returned = ops.validated_input(self.__validator, "msg", default="other")
self.assertEqual(returned, (display_util.OK, self.valid_input))
- @test_util.patch_get_utility()
+ @test_util.patch_display_util()
def test_input_validation_with_bad_default(self, mock_util):
mock_util().input.side_effect = [(display_util.OK, self.valid_input)]
@@ -428,14 +428,14 @@ def test_input_validation_with_bad_default(self, mock_util):
ops.validated_input,
self.__validator, "msg", default="")
- @test_util.patch_get_utility()
+ @test_util.patch_display_util()
def test_input_cancel_with_validator(self, mock_util):
mock_util().input.side_effect = [(display_util.CANCEL, "")]
code, unused_raw = ops.validated_input(self.__validator, "message", force_interactive=True)
self.assertEqual(code, display_util.CANCEL)
- @test_util.patch_get_utility()
+ @test_util.patch_display_util()
def test_directory_select_validation(self, mock_util):
mock_util().directory_select.side_effect = [(display_util.OK, ""),
(display_util.OK, self.valid_directory)]
@@ -444,14 +444,14 @@ def test_directory_select_validation(self, mock_util):
self.assertEqual(ValidatorTests.__ERROR, mock_util().notification.call_args[0][0])
self.assertEqual(returned, (display_util.OK, self.valid_directory))
- @test_util.patch_get_utility()
+ @test_util.patch_display_util()
def test_directory_select_validation_with_default(self, mock_util):
mock_util().directory_select.side_effect = [(display_util.OK, self.valid_directory)]
returned = ops.validated_directory(self.__validator, "msg", default="other")
self.assertEqual(returned, (display_util.OK, self.valid_directory))
- @test_util.patch_get_utility()
+ @test_util.patch_display_util()
def test_directory_select_validation_with_bad_default(self, mock_util):
mock_util().directory_select.side_effect = [(display_util.OK, self.valid_directory)]
@@ -467,7 +467,7 @@ def _call(cls, values, question):
from certbot.display.ops import choose_values
return choose_values(values, question)
- @test_util.patch_get_utility("certbot.display.ops.z_util")
+ @test_util.patch_display_util()
def test_choose_names_success(self, mock_util):
items = ["first", "second", "third"]
mock_util().checklist.return_value = (display_util.OK, [items[2]])
@@ -476,7 +476,7 @@ def test_choose_names_success(self, mock_util):
self.assertIs(mock_util().checklist.called, True)
self.assertIsNone(mock_util().checklist.call_args[0][0])
- @test_util.patch_get_utility("certbot.display.ops.z_util")
+ @test_util.patch_display_util()
def test_choose_names_success_question(self, mock_util):
items = ["first", "second", "third"]
question = "Which one?"
@@ -486,7 +486,7 @@ def test_choose_names_success_question(self, mock_util):
self.assertIs(mock_util().checklist.called, True)
self.assertEqual(mock_util().checklist.call_args[0][0], question)
- @test_util.patch_get_utility("certbot.display.ops.z_util")
+ @test_util.patch_display_util()
def test_choose_names_user_cancel(self, mock_util):
items = ["first", "second", "third"]
question = "Want to cancel?"
diff --git a/certbot/tests/display/util_test.py b/certbot/tests/display/util_test.py
index ca7ecf908ca..aa6ce876936 100644
--- a/certbot/tests/display/util_test.py
+++ b/certbot/tests/display/util_test.py
@@ -1,25 +1,101 @@
"""Test :mod:`certbot.display.util`."""
-import inspect
import io
import socket
import tempfile
import unittest
-
from certbot import errors
-from certbot import interfaces
-from certbot.display import util as display_util
import certbot.tests.util as test_util
try:
import mock
-except ImportError: # pragma: no cover
+except ImportError: # pragma: no cover
from unittest import mock
-CHOICES = [("First", "Description1"), ("Second", "Description2")]
-TAGS = ["tag1", "tag2", "tag3"]
-TAGS_CHOICES = [("1", "tag1"), ("2", "tag2"), ("3", "tag3")]
+class NotifyTest(unittest.TestCase):
+ """Tests for certbot.display.util.notify"""
+
+ @test_util.patch_display_util()
+ def test_notify(self, mock_util):
+ from certbot.display.util import notify
+ notify("Hello World")
+ mock_util().notification.assert_called_with(
+ "Hello World", pause=False, decorate=False, wrap=False
+ )
+
+
+class NotificationTest(unittest.TestCase):
+ """Tests for certbot.display.util.notification"""
+
+ @test_util.patch_display_util()
+ def test_notification(self, mock_util):
+ from certbot.display.util import notification
+ notification("Hello World")
+ mock_util().notification.assert_called_with(
+ "Hello World", pause=True, decorate=True, wrap=True, force_interactive=False
+ )
+
+
+class MenuTest(unittest.TestCase):
+ """Tests for certbot.display.util.menu"""
+
+ @test_util.patch_display_util()
+ def test_menu(self, mock_util):
+ from certbot.display.util import menu
+ menu("Hello World", ["one", "two"], default=0)
+ mock_util().menu.assert_called_with(
+ "Hello World", ["one", "two"], default=0, cli_flag=None, force_interactive=False
+ )
+
+
+class InputTextTest(unittest.TestCase):
+ """Tests for certbot.display.util.input_text"""
+
+ @test_util.patch_display_util()
+ def test_input_text(self, mock_util):
+ from certbot.display.util import input_text
+ input_text("Hello World", default="something")
+ mock_util().input.assert_called_with(
+ "Hello World", default='something', cli_flag=None, force_interactive=False
+ )
+
+
+class YesNoTest(unittest.TestCase):
+ """Tests for certbot.display.util.yesno"""
+
+ @test_util.patch_display_util()
+ def test_yesno(self, mock_util):
+ from certbot.display.util import yesno
+ yesno("Hello World", default=True)
+ mock_util().yesno.assert_called_with(
+ "Hello World", yes_label='Yes', no_label='No', default=True, cli_flag=None,
+ force_interactive=False
+ )
+
+
+class ChecklistTest(unittest.TestCase):
+ """Tests for certbot.display.util.checklist"""
+
+ @test_util.patch_display_util()
+ def test_checklist(self, mock_util):
+ from certbot.display.util import checklist
+ checklist("Hello World", ["one", "two"], default="one")
+ mock_util().checklist.assert_called_with(
+ "Hello World", ['one', 'two'], default='one', cli_flag=None, force_interactive=False
+ )
+
+
+class DirectorySelectTest(unittest.TestCase):
+ """Tests for certbot.display.util.directory_select"""
+
+ @test_util.patch_display_util()
+ def test_directory_select(self, mock_util):
+ from certbot.display.util import directory_select
+ directory_select("Hello World", default="something")
+ mock_util().directory_select.assert_called_with(
+ "Hello World", default='something', cli_flag=None, force_interactive=False
+ )
class InputWithTimeoutTest(unittest.TestCase):
@@ -57,354 +133,6 @@ def test_timeout(self):
stdin.close()
-class FileOutputDisplayTest(unittest.TestCase):
- """Test stdout display.
-
- Most of this class has to deal with visual output. In order to test how the
- functions look to a user, uncomment the test_visual function.
-
- """
- def setUp(self):
- super().setUp()
- self.mock_stdout = mock.MagicMock()
- self.displayer = display_util.FileDisplay(self.mock_stdout, False)
-
- @mock.patch("certbot.display.util.logger")
- def test_notification_no_pause(self, mock_logger):
- self.displayer.notification("message", False)
- string = self.mock_stdout.write.call_args[0][0]
-
- self.assertIn("message", string)
- mock_logger.debug.assert_called_with("Notifying user: %s", "message")
-
- def test_notification_pause(self):
- input_with_timeout = "certbot.display.util.input_with_timeout"
- with mock.patch(input_with_timeout, return_value="enter"):
- self.displayer.notification("message", force_interactive=True)
-
- self.assertIn("message", self.mock_stdout.write.call_args[0][0])
-
- def test_notification_noninteractive(self):
- self._force_noninteractive(self.displayer.notification, "message")
- string = self.mock_stdout.write.call_args[0][0]
- self.assertIn("message", string)
-
- def test_notification_noninteractive2(self):
- # The main purpose of this test is to make sure we only call
- # logger.warning once which _force_noninteractive checks internally
- self._force_noninteractive(self.displayer.notification, "message")
- string = self.mock_stdout.write.call_args[0][0]
- self.assertIn("message", string)
-
- self.assertTrue(self.displayer.skipped_interaction)
-
- self._force_noninteractive(self.displayer.notification, "message2")
- string = self.mock_stdout.write.call_args[0][0]
- self.assertIn("message2", string)
-
- def test_notification_decoration(self):
- from certbot.compat import os
- self.displayer.notification("message", pause=False, decorate=False)
- string = self.mock_stdout.write.call_args[0][0]
- self.assertEqual(string, "message" + os.linesep)
-
- self.displayer.notification("message2", pause=False)
- string = self.mock_stdout.write.call_args[0][0]
- self.assertIn("- - - ", string)
- self.assertIn("message2" + os.linesep, string)
-
- @mock.patch("certbot.display.util."
- "FileDisplay._get_valid_int_ans")
- def test_menu(self, mock_ans):
- mock_ans.return_value = (display_util.OK, 1)
- ret = self.displayer.menu("message", CHOICES, force_interactive=True)
- self.assertEqual(ret, (display_util.OK, 0))
-
- def test_menu_noninteractive(self):
- default = 0
- result = self._force_noninteractive(
- self.displayer.menu, "msg", CHOICES, default=default)
- self.assertEqual(result, (display_util.OK, default))
-
- def test_input_cancel(self):
- input_with_timeout = "certbot.display.util.input_with_timeout"
- with mock.patch(input_with_timeout, return_value="c"):
- code, _ = self.displayer.input("message", force_interactive=True)
-
- self.assertTrue(code, display_util.CANCEL)
-
- def test_input_normal(self):
- input_with_timeout = "certbot.display.util.input_with_timeout"
- with mock.patch(input_with_timeout, return_value="domain.com"):
- code, input_ = self.displayer.input("message", force_interactive=True)
-
- self.assertEqual(code, display_util.OK)
- self.assertEqual(input_, "domain.com")
-
- def test_input_noninteractive(self):
- default = "foo"
- code, input_ = self._force_noninteractive(
- self.displayer.input, "message", default=default)
-
- self.assertEqual(code, display_util.OK)
- self.assertEqual(input_, default)
-
- def test_input_assertion_fail(self):
- # If the call to util.assert_valid_call is commented out, an
- # error.Error is raised, otherwise, an AssertionError is raised.
- self.assertRaises(Exception, self._force_noninteractive,
- self.displayer.input, "message", cli_flag="--flag")
-
- def test_input_assertion_fail2(self):
- with mock.patch("certbot.display.util.assert_valid_call"):
- self.assertRaises(errors.Error, self._force_noninteractive,
- self.displayer.input, "msg", cli_flag="--flag")
-
- def test_yesno(self):
- input_with_timeout = "certbot.display.util.input_with_timeout"
- with mock.patch(input_with_timeout, return_value="Yes"):
- self.assertTrue(self.displayer.yesno(
- "message", force_interactive=True))
- with mock.patch(input_with_timeout, return_value="y"):
- self.assertTrue(self.displayer.yesno(
- "message", force_interactive=True))
- with mock.patch(input_with_timeout, side_effect=["maybe", "y"]):
- self.assertTrue(self.displayer.yesno(
- "message", force_interactive=True))
- with mock.patch(input_with_timeout, return_value="No"):
- self.assertFalse(self.displayer.yesno(
- "message", force_interactive=True))
- with mock.patch(input_with_timeout, side_effect=["cancel", "n"]):
- self.assertFalse(self.displayer.yesno(
- "message", force_interactive=True))
-
- with mock.patch(input_with_timeout, return_value="a"):
- self.assertTrue(self.displayer.yesno(
- "msg", yes_label="Agree", force_interactive=True))
-
- def test_yesno_noninteractive(self):
- self.assertTrue(self._force_noninteractive(
- self.displayer.yesno, "message", default=True))
-
- @mock.patch("certbot.display.util.input_with_timeout")
- def test_checklist_valid(self, mock_input):
- mock_input.return_value = "2 1"
- code, tag_list = self.displayer.checklist(
- "msg", TAGS, force_interactive=True)
- self.assertEqual(
- (code, set(tag_list)), (display_util.OK, {"tag1", "tag2"}))
-
- @mock.patch("certbot.display.util.input_with_timeout")
- def test_checklist_empty(self, mock_input):
- mock_input.return_value = ""
- code, tag_list = self.displayer.checklist("msg", TAGS, force_interactive=True)
- self.assertEqual(
- (code, set(tag_list)), (display_util.OK, {"tag1", "tag2", "tag3"}))
-
- @mock.patch("certbot.display.util.input_with_timeout")
- def test_checklist_miss_valid(self, mock_input):
- mock_input.side_effect = ["10", "tag1 please", "1"]
-
- ret = self.displayer.checklist("msg", TAGS, force_interactive=True)
- self.assertEqual(ret, (display_util.OK, ["tag1"]))
-
- @mock.patch("certbot.display.util.input_with_timeout")
- def test_checklist_miss_quit(self, mock_input):
- mock_input.side_effect = ["10", "c"]
-
- ret = self.displayer.checklist("msg", TAGS, force_interactive=True)
- self.assertEqual(ret, (display_util.CANCEL, []))
-
- def test_checklist_noninteractive(self):
- default = TAGS
- code, input_ = self._force_noninteractive(
- self.displayer.checklist, "msg", TAGS, default=default)
-
- self.assertEqual(code, display_util.OK)
- self.assertEqual(input_, default)
-
- def test_scrub_checklist_input_valid(self):
- # pylint: disable=protected-access
- indices = [
- ["1"],
- ["1", "2", "1"],
- ["2", "3"],
- ]
- exp = [
- {"tag1"},
- {"tag1", "tag2"},
- {"tag2", "tag3"},
- ]
- for i, list_ in enumerate(indices):
- set_tags = set(
- self.displayer._scrub_checklist_input(list_, TAGS))
- self.assertEqual(set_tags, exp[i])
-
- @mock.patch("certbot.display.util.input_with_timeout")
- def test_directory_select(self, mock_input):
- args = ["msg", "/var/www/html", "--flag", True]
- user_input = "/var/www/html"
- mock_input.return_value = user_input
-
- returned = self.displayer.directory_select(*args)
- self.assertEqual(returned, (display_util.OK, user_input))
-
- def test_directory_select_noninteractive(self):
- default = "/var/www/html"
- code, input_ = self._force_noninteractive(
- self.displayer.directory_select, "msg", default=default)
-
- self.assertEqual(code, display_util.OK)
- self.assertEqual(input_, default)
-
- def _force_noninteractive(self, func, *args, **kwargs):
- skipped_interaction = self.displayer.skipped_interaction
-
- with mock.patch("certbot.display.util.sys.stdin") as mock_stdin:
- mock_stdin.isatty.return_value = False
- with mock.patch("certbot.display.util.logger") as mock_logger:
- result = func(*args, **kwargs)
-
- if skipped_interaction:
- self.assertIs(mock_logger.warning.called, False)
- else:
- self.assertEqual(mock_logger.warning.call_count, 1)
-
- return result
-
- def test_scrub_checklist_input_invalid(self):
- # pylint: disable=protected-access
- indices = [
- ["0"],
- ["4"],
- ["tag1"],
- ["1", "tag1"],
- ["2", "o"]
- ]
- for list_ in indices:
- self.assertEqual(
- self.displayer._scrub_checklist_input(list_, TAGS), [])
-
- def test_print_menu(self):
- # pylint: disable=protected-access
- # This is purely cosmetic... just make sure there aren't any exceptions
- self.displayer._print_menu("msg", CHOICES)
- self.displayer._print_menu("msg", TAGS)
-
- def test_wrap_lines(self):
- # pylint: disable=protected-access
- msg = ("This is just a weak test{0}"
- "This function is only meant to be for easy viewing{0}"
- "Test a really really really really really really really really "
- "really really really really long line...".format('\n'))
- text = display_util._wrap_lines(msg)
-
- self.assertEqual(text.count('\n'), 3)
-
- def test_get_valid_int_ans_valid(self):
- # pylint: disable=protected-access
- input_with_timeout = "certbot.display.util.input_with_timeout"
- with mock.patch(input_with_timeout, return_value="1"):
- self.assertEqual(
- self.displayer._get_valid_int_ans(1), (display_util.OK, 1))
- ans = "2"
- with mock.patch(input_with_timeout, return_value=ans):
- self.assertEqual(
- self.displayer._get_valid_int_ans(3),
- (display_util.OK, int(ans)))
-
- def test_get_valid_int_ans_invalid(self):
- # pylint: disable=protected-access
- answers = [
- ["0", "c"],
- ["4", "one", "C"],
- ["c"],
- ]
- input_with_timeout = "certbot.display.util.input_with_timeout"
- for ans in answers:
- with mock.patch(input_with_timeout, side_effect=ans):
- self.assertEqual(
- self.displayer._get_valid_int_ans(3),
- (display_util.CANCEL, -1))
-
- def test_methods_take_force_interactive(self):
- # Every IDisplay method implemented by FileDisplay must take
- # force_interactive to prevent workflow regressions.
- for name in interfaces.IDisplay.names():
- arg_spec = inspect.getfullargspec(getattr(self.displayer, name))
- self.assertIn("force_interactive", arg_spec.args)
-
-
-class NoninteractiveDisplayTest(unittest.TestCase):
- """Test non-interactive display. These tests are pretty easy!"""
- def setUp(self):
- self.mock_stdout = mock.MagicMock()
- self.displayer = display_util.NoninteractiveDisplay(self.mock_stdout)
-
- @mock.patch("certbot.display.util.logger")
- def test_notification_no_pause(self, mock_logger):
- self.displayer.notification("message", 10)
- string = self.mock_stdout.write.call_args[0][0]
-
- self.assertIn("message", string)
- mock_logger.debug.assert_called_with("Notifying user: %s", "message")
-
- def test_notification_decoration(self):
- from certbot.compat import os
- self.displayer.notification("message", pause=False, decorate=False)
- string = self.mock_stdout.write.call_args[0][0]
- self.assertEqual(string, "message" + os.linesep)
-
- self.displayer.notification("message2", pause=False)
- string = self.mock_stdout.write.call_args[0][0]
- self.assertTrue("- - - " in string and ("message2" + os.linesep) in string)
-
- def test_input(self):
- d = "an incomputable value"
- ret = self.displayer.input("message", default=d)
- self.assertEqual(ret, (display_util.OK, d))
- self.assertRaises(errors.MissingCommandlineFlag, self.displayer.input, "message")
-
- def test_menu(self):
- ret = self.displayer.menu("message", CHOICES, default=1)
- self.assertEqual(ret, (display_util.OK, 1))
- self.assertRaises(errors.MissingCommandlineFlag, self.displayer.menu, "message", CHOICES)
-
- def test_yesno(self):
- d = False
- ret = self.displayer.yesno("message", default=d)
- self.assertEqual(ret, d)
- self.assertRaises(errors.MissingCommandlineFlag, self.displayer.yesno, "message")
-
- def test_checklist(self):
- d = [1, 3]
- ret = self.displayer.checklist("message", TAGS, default=d)
- self.assertEqual(ret, (display_util.OK, d))
- self.assertRaises(errors.MissingCommandlineFlag, self.displayer.checklist, "message", TAGS)
-
- def test_directory_select(self):
- default = "/var/www/html"
- expected = (display_util.OK, default)
- actual = self.displayer.directory_select("msg", default)
- self.assertEqual(expected, actual)
-
- self.assertRaises(
- errors.MissingCommandlineFlag, self.displayer.directory_select, "msg")
-
- def test_methods_take_kwargs(self):
- # Every IDisplay method implemented by NoninteractiveDisplay
- # should take **kwargs because every method of FileDisplay must
- # take force_interactive which doesn't apply to
- # NoninteractiveDisplay.
-
- # Use pylint code for disable to keep on single line under line length limit
- for name in interfaces.IDisplay.names(): # pylint: disable=E1120
- method = getattr(self.displayer, name)
- # asserts method accepts arbitrary keyword arguments
- result = inspect.getfullargspec(method).varkw
- self.assertIsNotNone(result)
-
-
class SeparateListInputTest(unittest.TestCase):
"""Test Module functions."""
def setUp(self):
@@ -435,20 +163,6 @@ def test_mess(self):
self.assertEqual(act, self.exp)
-class PlaceParensTest(unittest.TestCase):
- @classmethod
- def _call(cls, label): # pylint: disable=protected-access
- from certbot.display.util import _parens_around_char
- return _parens_around_char(label)
-
- def test_single_letter(self):
- self.assertEqual("(a)", self._call("a"))
-
- def test_multiple(self):
- self.assertEqual("(L)abel", self._call("Label"))
- self.assertEqual("(y)es please", self._call("yes please"))
-
-
class SummarizeDomainListTest(unittest.TestCase):
@classmethod
def _call(cls, domains):
@@ -470,17 +184,5 @@ def test_empty_domains(self):
self.assertEqual("", self._call([]))
-class NotifyTest(unittest.TestCase):
- """Test the notify function """
-
- @test_util.patch_get_utility()
- def test_notify(self, mock_util):
- from certbot.display.util import notify
- notify("Hello World")
- mock_util().notification.assert_called_with(
- "Hello World", pause=False, decorate=False, wrap=False
- )
-
-
if __name__ == "__main__":
unittest.main() # pragma: no cover
diff --git a/certbot/tests/eff_test.py b/certbot/tests/eff_test.py
index 0527d87d9a4..c61f183cb11 100644
--- a/certbot/tests/eff_test.py
+++ b/certbot/tests/eff_test.py
@@ -42,7 +42,7 @@ def _call(self):
from certbot._internal.eff import prepare_subscription
prepare_subscription(self.config, self.account)
- @test_util.patch_get_utility()
+ @test_util.patch_display_util()
@mock.patch("certbot._internal.eff.display_util.notify")
def test_failure(self, mock_notify, mock_get_utility):
self.config.email = None
@@ -53,21 +53,21 @@ def test_failure(self, mock_notify, mock_get_utility):
self.assertIn(expected_part, actual)
self.assertIsNone(self.account.meta.register_to_eff)
- @test_util.patch_get_utility()
+ @test_util.patch_display_util()
def test_will_not_subscribe_with_no_prompt(self, mock_get_utility):
self.config.eff_email = False
self._call()
self._assert_no_get_utility_calls(mock_get_utility)
self.assertIsNone(self.account.meta.register_to_eff)
- @test_util.patch_get_utility()
+ @test_util.patch_display_util()
def test_will_subscribe_with_no_prompt(self, mock_get_utility):
self.config.eff_email = True
self._call()
self._assert_no_get_utility_calls(mock_get_utility)
self.assertEqual(self.account.meta.register_to_eff, self.config.email)
- @test_util.patch_get_utility()
+ @test_util.patch_display_util()
def test_will_not_subscribe_with_prompt(self, mock_get_utility):
mock_get_utility().yesno.return_value = False
self._call()
@@ -75,7 +75,7 @@ def test_will_not_subscribe_with_prompt(self, mock_get_utility):
self._assert_correct_yesno_call(mock_get_utility)
self.assertIsNone(self.account.meta.register_to_eff)
- @test_util.patch_get_utility()
+ @test_util.patch_display_util()
def test_will_subscribe_with_prompt(self, mock_get_utility):
mock_get_utility().yesno.return_value = True
self._call()
@@ -176,7 +176,7 @@ def _get_reported_message(self):
self.assertTrue(self.mock_notify.called)
return self.mock_notify.call_args[0][0]
- @test_util.patch_get_utility()
+ @test_util.patch_display_util()
def test_subscribe(self, mock_get_utility):
self._call()
self.assertIs(mock_get_utility.called, False)
diff --git a/certbot/tests/error_handler_test.py b/certbot/tests/error_handler_test.py
index ee4c2215d94..0146f0edd65 100644
--- a/certbot/tests/error_handler_test.py
+++ b/certbot/tests/error_handler_test.py
@@ -2,15 +2,18 @@
import contextlib
import signal
import sys
+from typing import Callable
+from typing import Dict
+from typing import Union
import unittest
-from typing import Callable, Dict, Union
+
+from certbot.compat import os
try:
import mock
except ImportError: # pragma: no cover
from unittest import mock
-from certbot.compat import os
def get_signals(signums):
diff --git a/certbot/tests/log_test.py b/certbot/tests/log_test.py
index 9b3b31030d1..3c8ac024d32 100644
--- a/certbot/tests/log_test.py
+++ b/certbot/tests/log_test.py
@@ -4,8 +4,8 @@
import logging.handlers
import sys
import time
-import unittest
from typing import Optional
+import unittest
from acme import messages
from certbot import errors
@@ -122,7 +122,7 @@ def test_common(self):
if self.config.quiet:
self.assertEqual(level, constants.QUIET_LOGGING_LEVEL)
else:
- self.assertEqual(level, -self.config.verbose_count * 10)
+ self.assertEqual(level, constants.DEFAULT_LOGGING_LEVEL)
def test_debug(self):
self.config.debug = True
diff --git a/certbot/tests/main_test.py b/certbot/tests/main_test.py
index 166b29dea2a..f85d40199fc 100644
--- a/certbot/tests/main_test.py
+++ b/certbot/tests/main_test.py
@@ -10,8 +10,8 @@
import sys
import tempfile
import traceback
-import unittest
from typing import List
+import unittest
import josepy as jose
import pytz
@@ -182,7 +182,7 @@ class CertonlyTest(unittest.TestCase):
"""Tests for certbot._internal.main.certonly."""
def setUp(self):
- self.get_utility_patch = test_util.patch_get_utility()
+ self.get_utility_patch = test_util.patch_display_util()
self.mock_get_utility = self.get_utility_patch.start()
def tearDown(self):
@@ -203,16 +203,15 @@ def _call(self, args):
@mock.patch('certbot._internal.main._find_cert')
@mock.patch('certbot._internal.main._get_and_save_cert')
@mock.patch('certbot._internal.main._report_new_cert')
- def test_no_reinstall_text_pause(self, unused_report, mock_auth,
- mock_find_cert):
+ def test_no_reinstall_text_pause(self, unused_report, mock_auth, mock_find_cert):
mock_notification = self.mock_get_utility().notification
mock_notification.side_effect = self._assert_no_pause
mock_auth.return_value = mock.Mock()
mock_find_cert.return_value = False, None
self._call('certonly --webroot -d example.com'.split())
- def _assert_no_pause(self, message, pause=True): # pylint: disable=unused-argument
- self.assertIs(pause, False)
+ def _assert_no_pause(self, *args, **kwargs): # pylint: disable=unused-argument
+ self.assertIs(kwargs.get("pause"), False)
@mock.patch('certbot._internal.main._report_next_steps')
@mock.patch('certbot._internal.cert_manager.lineage_for_certname')
@@ -271,6 +270,21 @@ def test_find_lineage_for_domains_new_certname(self, mock_report_cert,
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."""
@@ -417,7 +431,7 @@ def test_revocation_error(self):
@mock.patch('certbot._internal.main._delete_if_appropriate')
@mock.patch('certbot._internal.cert_manager.delete')
- @test_util.patch_get_utility()
+ @test_util.patch_display_util()
def test_revocation_with_prompt(self, mock_get_utility,
mock_delete, mock_delete_if_appropriate):
mock_get_utility().yesno.return_value = False
@@ -437,12 +451,12 @@ def _test_delete_opt_out_common(self):
self._call(self.config)
mock_delete.assert_not_called()
- @test_util.patch_get_utility()
+ @test_util.patch_display_util()
def test_delete_flag_opt_out(self, unused_mock_get_utility):
self.config.delete_after_revoke = False
self._test_delete_opt_out_common()
- @test_util.patch_get_utility()
+ @test_util.patch_display_util()
def test_delete_prompt_opt_out(self, mock_get_utility):
util_mock = mock_get_utility()
util_mock.yesno.return_value = False
@@ -454,7 +468,7 @@ def test_delete_prompt_opt_out(self, mock_get_utility):
@mock.patch('certbot._internal.cert_manager.match_and_check_overlaps')
@mock.patch('certbot._internal.storage.full_archive_path')
@mock.patch('certbot._internal.cert_manager.cert_path_to_lineage')
- @test_util.patch_get_utility()
+ @test_util.patch_display_util()
def test_overlapping_archive_dirs(self, mock_get_utility,
mock_cert_path_to_lineage, mock_archive,
mock_match_and_check_overlaps, mock_delete,
@@ -474,7 +488,7 @@ def test_overlapping_archive_dirs(self, mock_get_utility,
@mock.patch('certbot._internal.storage.full_archive_path')
@mock.patch('certbot._internal.cert_manager.delete')
@mock.patch('certbot._internal.cert_manager.cert_path_to_lineage')
- @test_util.patch_get_utility()
+ @test_util.patch_display_util()
def test_cert_path_only(self, mock_get_utility,
mock_cert_path_to_lineage, mock_delete, mock_archive,
mock_overlapping_archive_dirs, mock_renewal_file_for_certname):
@@ -492,7 +506,7 @@ def test_cert_path_only(self, mock_get_utility,
@mock.patch('certbot._internal.storage.full_archive_path')
@mock.patch('certbot._internal.cert_manager.cert_path_to_lineage')
@mock.patch('certbot._internal.cert_manager.delete')
- @test_util.patch_get_utility()
+ @test_util.patch_display_util()
def test_noninteractive_deletion(self, mock_get_utility, mock_delete,
mock_cert_path_to_lineage, mock_full_archive_dir,
mock_match_and_check_overlaps, mock_renewal_file_for_certname):
@@ -512,7 +526,7 @@ def test_noninteractive_deletion(self, mock_get_utility, mock_delete,
@mock.patch('certbot._internal.storage.full_archive_path')
@mock.patch('certbot._internal.cert_manager.cert_path_to_lineage')
@mock.patch('certbot._internal.cert_manager.delete')
- @test_util.patch_get_utility()
+ @test_util.patch_display_util()
def test_opt_in_deletion(self, mock_get_utility, mock_delete,
mock_cert_path_to_lineage, mock_full_archive_dir,
mock_match_and_check_overlaps, mock_renewal_file_for_certname):
@@ -548,7 +562,7 @@ def _call(self):
# pylint: disable=protected-access
from certbot._internal.main import _determine_account
with mock.patch('certbot._internal.main.account.AccountFileStorage') as mock_storage, \
- test_util.patch_get_utility():
+ test_util.patch_display_util():
mock_storage.return_value = self.account_storage
return _determine_account(self.config)
@@ -646,7 +660,7 @@ def mock_isfile(fn, *args, **kwargs): # pylint: disable=unused-argument
return ret, stdout, stderr, client
def _call_no_clientmock(self, args, stdout=None):
- "Run the client with output streams mocked out"
+ """Run the client with output streams mocked out"""
args = self.standard_args + args
toy_stdout = stdout if stdout else io.StringIO()
@@ -877,11 +891,11 @@ def test_plugins(self, _, _det, mock_disco):
@mock.patch('certbot._internal.main.plugins_disco')
@mock.patch('certbot._internal.main.cli.HelpfulArgumentParser.determine_help_topics')
def test_plugins_no_args(self, _det, mock_disco):
- ifaces: List[interfaces.IPlugin] = []
+ ifaces: List[interfaces.Plugin] = []
plugins = mock_disco.PluginsRegistry.find_all()
stdout = io.StringIO()
- with test_util.patch_get_utility_with_stdout(stdout=stdout):
+ with test_util.patch_display_util_with_stdout(stdout=stdout):
_, stdout, _, _ = self._call(['plugins'], stdout)
plugins.visible.assert_called_once_with()
@@ -892,7 +906,7 @@ def test_plugins_no_args(self, _det, mock_disco):
@mock.patch('certbot._internal.main.plugins_disco')
@mock.patch('certbot._internal.main.cli.HelpfulArgumentParser.determine_help_topics')
def test_plugins_no_args_unprivileged(self, _det, mock_disco):
- ifaces: List[interfaces.IPlugin] = []
+ ifaces: List[interfaces.Plugin] = []
plugins = mock_disco.PluginsRegistry.find_all()
def throw_error(directory, mode, strict):
@@ -902,7 +916,7 @@ def throw_error(directory, mode, strict):
stdout = io.StringIO()
with mock.patch('certbot.util.set_up_core_dir') as mock_set_up_core_dir:
- with test_util.patch_get_utility_with_stdout(stdout=stdout):
+ with test_util.patch_display_util_with_stdout(stdout=stdout):
mock_set_up_core_dir.side_effect = throw_error
_, stdout, _, _ = self._call(['plugins'], stdout)
@@ -914,11 +928,11 @@ def throw_error(directory, mode, strict):
@mock.patch('certbot._internal.main.plugins_disco')
@mock.patch('certbot._internal.main.cli.HelpfulArgumentParser.determine_help_topics')
def test_plugins_init(self, _det, mock_disco):
- ifaces: List[interfaces.IPlugin] = []
+ ifaces: List[interfaces.Plugin] = []
plugins = mock_disco.PluginsRegistry.find_all()
stdout = io.StringIO()
- with test_util.patch_get_utility_with_stdout(stdout=stdout):
+ with test_util.patch_display_util_with_stdout(stdout=stdout):
_, stdout, _, _ = self._call(['plugins', '--init'], stdout)
plugins.visible.assert_called_once_with()
@@ -932,11 +946,11 @@ def test_plugins_init(self, _det, mock_disco):
@mock.patch('certbot._internal.main.plugins_disco')
@mock.patch('certbot._internal.main.cli.HelpfulArgumentParser.determine_help_topics')
def test_plugins_prepare(self, _det, mock_disco):
- ifaces: List[interfaces.IPlugin] = []
+ ifaces: List[interfaces.Plugin] = []
plugins = mock_disco.PluginsRegistry.find_all()
stdout = io.StringIO()
- with test_util.patch_get_utility_with_stdout(stdout=stdout):
+ with test_util.patch_display_util_with_stdout(stdout=stdout):
_, stdout, _, _ = self._call(['plugins', '--init', '--prepare'], stdout)
plugins.visible.assert_called_once_with()
@@ -1025,8 +1039,7 @@ def _certonly_new_request_common(self, mock_client, args=None):
self._call(args)
@mock.patch('certbot._internal.main._report_new_cert')
- @test_util.patch_get_utility()
- def test_certonly_dry_run_new_request_success(self, mock_get_utility, mock_report):
+ def test_certonly_dry_run_new_request_success(self, mock_report):
mock_client = mock.MagicMock()
mock_client.obtain_and_enroll_certificate.return_value = None
self._certonly_new_request_common(mock_client, ['--dry-run'])
@@ -1034,15 +1047,12 @@ def test_certonly_dry_run_new_request_success(self, mock_get_utility, mock_repor
mock_client.obtain_and_enroll_certificate.call_count, 1)
self.assertEqual(mock_report.call_count, 1)
self.assertIs(mock_report.call_args[0][0].dry_run, True)
- # Asserts we don't suggest donating after a successful dry run
- self.assertEqual(mock_get_utility().add_message.call_count, 0)
@mock.patch('certbot._internal.main._report_new_cert')
@mock.patch('certbot._internal.main.util.atexit_register')
@mock.patch('certbot._internal.eff.handle_subscription')
@mock.patch('certbot.crypto_util.notAfter')
- @test_util.patch_get_utility()
- def test_certonly_new_request_success(self, unused_mock_get_utility, mock_notAfter,
+ def test_certonly_new_request_success(self, mock_notAfter,
mock_subscription, mock_register, mock_report):
cert_path = os.path.normpath(os.path.join(self.config.config_dir, 'live/foo.bar'))
key_path = os.path.normpath(os.path.join(self.config.config_dir, 'live/baz.qux'))
@@ -1099,9 +1109,9 @@ def write_msg(message, *args, **kwargs): # pylint: disable=unused-argument
mock_fdc.return_value = (mock_lineage, None)
with mock.patch('certbot._internal.main._init_le_client') as mock_init:
mock_init.return_value = mock_client
- with test_util.patch_get_utility() as mock_get_utility:
+ with mock.patch('certbot._internal.display.obj.get_display') as mock_display:
if not quiet_mode:
- mock_get_utility().notification.side_effect = write_msg
+ mock_display().notification.side_effect = write_msg
with mock.patch('certbot._internal.main.renewal.crypto_util') \
as mock_crypto_util:
mock_crypto_util.notAfter.return_value = expiry_date
@@ -1141,7 +1151,7 @@ def write_msg(message, *args, **kwargs): # pylint: disable=unused-argument
with open(os.path.join(self.config.logs_dir, "letsencrypt.log")) as lf:
self.assertIn(log_out, lf.read())
- return mock_lineage, mock_get_utility, stdout
+ return mock_lineage, mock_display, stdout
@mock.patch('certbot._internal.main._report_new_cert')
@mock.patch('certbot._internal.main.util.atexit_register')
@@ -1167,9 +1177,9 @@ def test_certonly_renewal_triggers(self, _, __, mock_notify):
self._test_renewal_common(False, ['--renew-by-default', '-tvv', '--debug'],
log_out="Auto-renewal forced")
- _, get_utility, _ = self._test_renewal_common(False, ['-tvv', '--debug', '--keep'],
+ _, mock_displayer, _ = self._test_renewal_common(False, ['-tvv', '--debug', '--keep'],
should_renew=False)
- self.assertIn('not yet due', get_utility().notification.call_args[0][0])
+ self.assertIn('not yet due', mock_displayer().notification.call_args[0][0])
def _dump_log(self):
print("Logs:")
@@ -1377,7 +1387,7 @@ def test_no_renewal_with_hooks(self):
.format(sys.executable)])
self.assertIn('No hooks were run.', stdout.getvalue())
- @test_util.patch_get_utility()
+ @test_util.patch_display_util()
@mock.patch('certbot._internal.main._find_lineage_for_domains_and_certname')
@mock.patch('certbot._internal.main._init_le_client')
@mock.patch('certbot._internal.main._report_new_cert')
@@ -1406,17 +1416,16 @@ def _test_certonly_csr_common(self, extra_args=None):
mock_client.save_certificate.return_value = cert_path, None, full_path
with mock.patch('certbot._internal.main._init_le_client') as mock_init:
mock_init.return_value = mock_client
- with test_util.patch_get_utility() as mock_get_utility:
- chain_path = os.path.normpath(os.path.join(
- self.config.config_dir,
- 'live/example.com/chain.pem'))
- args = ('-a standalone certonly --csr {0} --cert-path {1} '
- '--chain-path {2} --fullchain-path {3}').format(
- CSR, cert_path, chain_path, full_path).split()
- if extra_args:
- args += extra_args
- with mock.patch('certbot._internal.main.crypto_util'):
- self._call(args)
+ chain_path = os.path.normpath(os.path.join(
+ self.config.config_dir,
+ 'live/example.com/chain.pem'))
+ args = ('-a standalone certonly --csr {0} --cert-path {1} '
+ '--chain-path {2} --fullchain-path {3}').format(
+ CSR, cert_path, chain_path, full_path).split()
+ if extra_args:
+ args += extra_args
+ with mock.patch('certbot._internal.main.crypto_util'):
+ self._call(args)
if '--dry-run' in args:
self.assertIs(mock_client.save_certificate.called, False)
@@ -1424,13 +1433,11 @@ def _test_certonly_csr_common(self, extra_args=None):
mock_client.save_certificate.assert_called_once_with(
certr, chain, cert_path, chain_path, full_path)
- return mock_get_utility
-
@mock.patch('certbot._internal.main._csr_report_new_cert')
@mock.patch('certbot._internal.main.util.atexit_register')
@mock.patch('certbot._internal.eff.handle_subscription')
def test_certonly_csr(self, mock_subscription, mock_register, mock_csr_report):
- _ = self._test_certonly_csr_common()
+ self._test_certonly_csr_common()
self.assertEqual(mock_csr_report.call_count, 1)
self.assertIn('cert_512.pem', mock_csr_report.call_args[0][1])
self.assertIsNone(mock_csr_report.call_args[0][2])
@@ -1440,7 +1447,7 @@ def test_certonly_csr(self, mock_subscription, mock_register, mock_csr_report):
@mock.patch('certbot._internal.main._csr_report_new_cert')
def test_certonly_csr_dry_run(self, mock_csr_report):
- _ = self._test_certonly_csr_common(['--dry-run'])
+ self._test_certonly_csr_common(['--dry-run'])
self.assertEqual(mock_csr_report.call_count, 1)
self.assertIs(mock_csr_report.call_args[0][0].dry_run, True)
@@ -1537,7 +1544,7 @@ def setUp(self):
'_determine_account': mock.patch('certbot._internal.main._determine_account'),
'account': mock.patch('certbot._internal.main.account'),
'client': mock.patch('certbot._internal.main.client'),
- 'get_utility': test_util.patch_get_utility()}
+ 'get_utility': test_util.patch_display_util()}
self.mocks = {k: v.start() for k, v in self.patchers.items()}
def tearDown(self):
@@ -1618,7 +1625,7 @@ class EnhanceTest(test_util.ConfigTestCase):
def setUp(self):
super().setUp()
- self.get_utility_patch = test_util.patch_get_utility()
+ self.get_utility_patch = test_util.patch_display_util()
self.mock_get_utility = self.get_utility_patch.start()
self.mockinstaller = mock.MagicMock(spec=enhancements.AutoHSTSEnhancement)
@@ -1728,7 +1735,7 @@ def test_plugin_selection_error(self, _rec, mock_choose, mock_pick):
@mock.patch('certbot._internal.main.display_ops.choose_values')
@mock.patch('certbot._internal.main.plug_sel.pick_installer')
@mock.patch('certbot._internal.main.plug_sel.record_chosen_plugins')
- @test_util.patch_get_utility()
+ @test_util.patch_display_util()
def test_enhancement_enable(self, _, _rec, mock_inst, mock_choose, mock_lineage):
mock_inst.return_value = self.mockinstaller
mock_choose.return_value = ["example.com", "another.tld"]
@@ -1742,7 +1749,7 @@ def test_enhancement_enable(self, _, _rec, mock_inst, mock_choose, mock_lineage)
@mock.patch('certbot._internal.main.display_ops.choose_values')
@mock.patch('certbot._internal.main.plug_sel.pick_installer')
@mock.patch('certbot._internal.main.plug_sel.record_chosen_plugins')
- @test_util.patch_get_utility()
+ @test_util.patch_display_util()
def test_enhancement_enable_not_supported(self, _, _rec, mock_inst, mock_choose, mock_lineage):
mock_inst.return_value = null.Installer(self.config, "null")
mock_choose.return_value = ["example.com", "another.tld"]
@@ -1886,6 +1893,71 @@ def test_csr_report(self):
'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"""
@@ -1898,7 +1970,7 @@ def setUp(self):
'determine_account': mock.patch('certbot._internal.main._determine_account'),
'notify': mock.patch('certbot._internal.main.display_util.notify'),
'prepare_sub': mock.patch('certbot._internal.eff.prepare_subscription'),
- 'util': test_util.patch_get_utility()
+ 'util': test_util.patch_display_util()
}
self.mocks = { k: patches[k].start() for k in patches }
for patch in patches.values():
diff --git a/certbot/tests/ocsp_test.py b/certbot/tests/ocsp_test.py
index 417eec8a7cc..8ab5395a509 100644
--- a/certbot/tests/ocsp_test.py
+++ b/certbot/tests/ocsp_test.py
@@ -10,14 +10,16 @@
from cryptography.exceptions import UnsupportedAlgorithm
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes # type: ignore
+import pytz
+
+from certbot import errors
+from certbot.tests import util as test_util
+
try:
import mock
except ImportError: # pragma: no cover
from unittest import mock
-import pytz
-from certbot import errors
-from certbot.tests import util as test_util
try:
# Only cryptography>=2.5 has ocsp module
diff --git a/certbot/tests/plugins/common_test.py b/certbot/tests/plugins/common_test.py
index 376af507bce..46d766bcfaf 100644
--- a/certbot/tests/plugins/common_test.py
+++ b/certbot/tests/plugins/common_test.py
@@ -24,6 +24,7 @@
"pending"),
domain="encryption-example.demo", account_key=AUTH_KEY)
+
class NamespaceFunctionsTest(unittest.TestCase):
"""Tests for certbot.plugins.common.*_namespace functions."""
@@ -47,6 +48,12 @@ def setUp(self):
from certbot.plugins.common import Plugin
class MockPlugin(Plugin): # pylint: disable=missing-docstring
+ def prepare(self) -> None:
+ pass
+
+ def more_info(self) -> str:
+ pass
+
@classmethod
def add_parser_arguments(cls, add):
add("foo-bar", dest="different_to_foo_bar", x=1, y=None)
@@ -97,9 +104,9 @@ class InstallerTest(test_util.ConfigTestCase):
def setUp(self):
super().setUp()
filesystem.mkdir(self.config.config_dir)
- from certbot.plugins.common import Installer
+ from certbot.tests.util import DummyInstaller
- self.installer = Installer(config=self.config,
+ self.installer = DummyInstaller(config=self.config,
name="Installer")
self.reverter = self.installer.reverter
diff --git a/certbot/tests/plugins/disco_test.py b/certbot/tests/plugins/disco_test.py
index 83dfb41ca04..833acdfb0f7 100644
--- a/certbot/tests/plugins/disco_test.py
+++ b/certbot/tests/plugins/disco_test.py
@@ -1,13 +1,9 @@
"""Tests for certbot._internal.plugins.disco."""
import functools
import string
-import unittest
from typing import List
+import unittest
-try:
- import mock
-except ImportError: # pragma: no cover
- from unittest import mock
import pkg_resources
import zope.interface
@@ -17,6 +13,12 @@
from certbot._internal.plugins import standalone
from certbot._internal.plugins import webroot
+try:
+ import mock
+except ImportError: # pragma: no cover
+ from unittest import mock
+
+
EP_SA = pkg_resources.EntryPoint(
"sa", "certbot._internal.plugins.standalone",
attrs=("Authenticator",),
@@ -95,10 +97,10 @@ def test_long_description_nonexistent(self):
"Long desc not found", self.plugin_ep.long_description)
def test_ifaces(self):
- self.assertTrue(self.plugin_ep.ifaces((interfaces.IAuthenticator,)))
- self.assertFalse(self.plugin_ep.ifaces((interfaces.IInstaller,)))
+ self.assertTrue(self.plugin_ep.ifaces((interfaces.Authenticator,)))
+ self.assertFalse(self.plugin_ep.ifaces((interfaces.Installer,)))
self.assertFalse(self.plugin_ep.ifaces((
- interfaces.IInstaller, interfaces.IAuthenticator)))
+ interfaces.Installer, interfaces.Authenticator)))
def test__init__(self):
self.assertIs(self.plugin_ep.initialized, False)
@@ -135,16 +137,16 @@ def test_verify(self):
self.plugin_ep._initialized = plugin = mock.MagicMock()
exceptions = zope.interface.exceptions
- with mock.patch("certbot._internal.plugins."
- "disco.zope.interface") as mock_zope:
- mock_zope.exceptions = exceptions
+ with mock.patch("certbot._internal.plugins.disco._verify") as mock_verify:
+ mock_verify.exceptions = exceptions
- def verify_object(iface, obj): # pylint: disable=missing-docstring
+ def verify_object(obj, cls, iface): # pylint: disable=missing-docstring
assert obj is plugin
assert iface is iface1 or iface is iface2 or iface is iface3
if iface is iface3:
- raise mock_zope.exceptions.BrokenImplementation(None, None)
- mock_zope.verify.verifyObject.side_effect = verify_object
+ return False
+ return True
+ mock_verify.side_effect = verify_object
self.assertTrue(self.plugin_ep.verify((iface1,)))
self.assertTrue(self.plugin_ep.verify((iface1, iface2)))
self.assertFalse(self.plugin_ep.verify((iface3,)))
diff --git a/certbot/tests/plugins/dns_common_test.py b/certbot/tests/plugins/dns_common_test.py
index 6738e11e604..41117f894f4 100644
--- a/certbot/tests/plugins/dns_common_test.py
+++ b/certbot/tests/plugins/dns_common_test.py
@@ -42,7 +42,7 @@ def setUp(self):
self.auth = DNSAuthenticatorTest._FakeDNSAuthenticator(self.config, "fake")
- @test_util.patch_get_utility()
+ @test_util.patch_display_util()
def test_perform(self, unused_mock_get_utility):
self.auth.perform([self.achall])
@@ -55,7 +55,7 @@ def test_cleanup(self):
self.auth._cleanup.assert_called_once_with(dns_test_common.DOMAIN, mock.ANY, mock.ANY)
- @test_util.patch_get_utility()
+ @test_util.patch_display_util()
def test_prompt(self, mock_get_utility):
mock_display = mock_get_utility()
mock_display.input.side_effect = ((display_util.OK, "",),
@@ -64,14 +64,14 @@ def test_prompt(self, mock_get_utility):
self.auth._configure("other_key", "")
self.assertEqual(self.auth.config.fake_other_key, "value")
- @test_util.patch_get_utility()
+ @test_util.patch_display_util()
def test_prompt_canceled(self, mock_get_utility):
mock_display = mock_get_utility()
mock_display.input.side_effect = ((display_util.CANCEL, "c",),)
self.assertRaises(errors.PluginError, self.auth._configure, "other_key", "")
- @test_util.patch_get_utility()
+ @test_util.patch_display_util()
def test_prompt_file(self, mock_get_utility):
path = os.path.join(self.tempdir, 'file.ini')
open(path, "wb").close()
@@ -85,7 +85,7 @@ def test_prompt_file(self, mock_get_utility):
self.auth._configure_file("file_path", "")
self.assertEqual(self.auth.config.fake_file_path, path)
- @test_util.patch_get_utility()
+ @test_util.patch_display_util()
def test_prompt_file_canceled(self, mock_get_utility):
mock_display = mock_get_utility()
mock_display.directory_select.side_effect = ((display_util.CANCEL, "c",),)
@@ -101,7 +101,7 @@ def test_configure_credentials(self):
self.assertEqual(credentials.conf("test"), "value")
- @test_util.patch_get_utility()
+ @test_util.patch_display_util()
def test_prompt_credentials(self, mock_get_utility):
bad_path = os.path.join(self.tempdir, 'bad-file.ini')
dns_test_common.write({"fake_other": "other_value"}, bad_path)
diff --git a/certbot/tests/plugins/enhancements_test.py b/certbot/tests/plugins/enhancements_test.py
index 0aa1512b423..62289d95bf7 100644
--- a/certbot/tests/plugins/enhancements_test.py
+++ b/certbot/tests/plugins/enhancements_test.py
@@ -19,7 +19,7 @@ def setUp(self):
self.mockinstaller = mock.MagicMock(spec=enhancements.AutoHSTSEnhancement)
- @test_util.patch_get_utility()
+ @test_util.patch_display_util()
def test_enhancement_enabled_enhancements(self, _):
FAKEINDEX = [
{
diff --git a/certbot/tests/plugins/manual_test.py b/certbot/tests/plugins/manual_test.py
index 8a4ed48ad8d..cfe2f60fa8e 100644
--- a/certbot/tests/plugins/manual_test.py
+++ b/certbot/tests/plugins/manual_test.py
@@ -21,9 +21,9 @@ class AuthenticatorTest(test_util.TempDirTestCase):
def setUp(self):
super().setUp()
- get_utility_patch = test_util.patch_get_utility()
- self.mock_get_utility = get_utility_patch.start()
- self.addCleanup(get_utility_patch.stop)
+ get_display_patch = test_util.patch_display_util()
+ self.mock_get_display = get_display_patch.start()
+ self.addCleanup(get_display_patch.stop)
self.http_achall = acme_util.HTTP01_A
self.dns_achall = acme_util.DNS01_A
@@ -95,8 +95,8 @@ def test_script_perform(self):
http_expected)
# Successful hook output should be sent to notify
- self.assertEqual(self.mock_get_utility().notification.call_count, len(self.achalls))
- for i, (args, _) in enumerate(self.mock_get_utility().notification.call_args_list):
+ self.assertEqual(self.mock_get_display().notification.call_count, len(self.achalls))
+ for i, (args, _) in enumerate(self.mock_get_display().notification.call_args_list):
needle = textwrap.indent(self.auth.env[self.achalls[i]]['CERTBOT_AUTH_OUTPUT'], ' ')
self.assertIn(needle, args[0])
@@ -105,8 +105,8 @@ def test_manual_perform(self):
self.auth.perform(self.achalls),
[achall.response(achall.account_key) for achall in self.achalls])
- self.assertEqual(self.mock_get_utility().notification.call_count, len(self.achalls))
- for i, (args, kwargs) in enumerate(self.mock_get_utility().notification.call_args_list):
+ self.assertEqual(self.mock_get_display().notification.call_count, len(self.achalls))
+ for i, (args, kwargs) in enumerate(self.mock_get_display().notification.call_args_list):
achall = self.achalls[i]
self.assertIn(achall.validation(achall.account_key), args[0])
self.assertIs(kwargs['wrap'], False)
diff --git a/certbot/tests/plugins/selection_test.py b/certbot/tests/plugins/selection_test.py
index 60917626a71..2a13aa54eab 100644
--- a/certbot/tests/plugins/selection_test.py
+++ b/certbot/tests/plugins/selection_test.py
@@ -1,21 +1,21 @@
"""Tests for letsencrypt.plugins.selection"""
import sys
-import unittest
from typing import List
+import unittest
-try:
- import mock
-except ImportError: # pragma: no cover
- from unittest import mock
-import zope.component
from certbot import errors
from certbot import interfaces
+from certbot._internal.display import obj as display_obj
from certbot._internal.plugins.disco import PluginsRegistry
-from certbot.compat import os
from certbot.display import util as display_util
from certbot.tests import util as test_util
+try:
+ import mock
+except ImportError: # pragma: no cover
+ from unittest import mock
+
class ConveniencePickPluginTest(unittest.TestCase):
"""Tests for certbot._internal.plugins.selection.pick_*."""
@@ -33,16 +33,16 @@ def _test(self, fun, ifaces):
def test_authenticator(self):
from certbot._internal.plugins.selection import pick_authenticator
- self._test(pick_authenticator, (interfaces.IAuthenticator,))
+ self._test(pick_authenticator, (interfaces.Authenticator,))
def test_installer(self):
from certbot._internal.plugins.selection import pick_installer
- self._test(pick_installer, (interfaces.IInstaller,))
+ self._test(pick_installer, (interfaces.Installer,))
def test_configurator(self):
from certbot._internal.plugins.selection import pick_configurator
self._test(pick_configurator,
- (interfaces.IAuthenticator, interfaces.IInstaller))
+ (interfaces.Authenticator, interfaces.Installer))
class PickPluginTest(unittest.TestCase):
@@ -118,8 +118,8 @@ class ChoosePluginTest(unittest.TestCase):
"""Tests for certbot._internal.plugins.selection.choose_plugin."""
def setUp(self):
- zope.component.provideUtility(display_util.FileDisplay(sys.stdout,
- False))
+ display_obj.set_display(display_obj.FileDisplay(sys.stdout, False))
+
self.mock_apache = mock.Mock(
description_with_name="a", misconfigured=True)
self.mock_apache.name = "apache"
@@ -135,14 +135,14 @@ def _call(self):
from certbot._internal.plugins.selection import choose_plugin
return choose_plugin(self.plugins, "Question?")
- @test_util.patch_get_utility("certbot._internal.plugins.selection.z_util")
+ @test_util.patch_display_util()
def test_selection(self, mock_util):
mock_util().menu.side_effect = [(display_util.OK, 0),
(display_util.OK, 1)]
self.assertEqual(self.mock_stand, self._call())
self.assertEqual(mock_util().notification.call_count, 1)
- @test_util.patch_get_utility("certbot._internal.plugins.selection.z_util")
+ @test_util.patch_display_util()
def test_more_info(self, mock_util):
mock_util().menu.side_effect = [
(display_util.OK, 1),
@@ -150,7 +150,7 @@ def test_more_info(self, mock_util):
self.assertEqual(self.mock_stand, self._call())
- @test_util.patch_get_utility("certbot._internal.plugins.selection.z_util")
+ @test_util.patch_display_util()
def test_no_choice(self, mock_util):
mock_util().menu.return_value = (display_util.CANCEL, 0)
self.assertIsNone(self._call())
diff --git a/certbot/tests/plugins/standalone_test.py b/certbot/tests/plugins/standalone_test.py
index 6f2ae91ba8c..3c990a3f54a 100644
--- a/certbot/tests/plugins/standalone_test.py
+++ b/certbot/tests/plugins/standalone_test.py
@@ -1,14 +1,12 @@
"""Tests for certbot._internal.plugins.standalone."""
import errno
import socket
+from typing import Dict
+from typing import Set
+from typing import Tuple
import unittest
-from typing import Dict, Set, Tuple
import josepy as jose
-try:
- import mock
-except ImportError: # pragma: no cover
- from unittest import mock
import OpenSSL.crypto # pylint: disable=unused-import
from acme import challenges
@@ -18,6 +16,12 @@
from certbot.tests import acme_util
from certbot.tests import util as test_util
+try:
+ import mock
+except ImportError: # pragma: no cover
+ from unittest import mock
+
+
class ServerManagerTest(unittest.TestCase):
"""Tests for certbot._internal.plugins.standalone.ServerManager."""
@@ -101,7 +105,7 @@ def test_perform(self):
expected = [achall.response(achall.account_key) for achall in achalls]
self.assertEqual(response, expected)
- @test_util.patch_get_utility()
+ @test_util.patch_display_util()
def test_perform_eaddrinuse_retry(self, mock_get_utility):
mock_utility = mock_get_utility()
encountered_errno = errno.EADDRINUSE
@@ -113,7 +117,7 @@ def test_perform_eaddrinuse_retry(self, mock_get_utility):
self.test_perform()
self._assert_correct_yesno_call(mock_yesno)
- @test_util.patch_get_utility()
+ @test_util.patch_display_util()
def test_perform_eaddrinuse_no_retry(self, mock_get_utility):
mock_utility = mock_get_utility()
mock_yesno = mock_utility.yesno
@@ -177,6 +181,13 @@ def test_cleanup(self):
"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
diff --git a/certbot/tests/plugins/storage_test.py b/certbot/tests/plugins/storage_test.py
index d01845510f2..66034b09ec5 100644
--- a/certbot/tests/plugins/storage_test.py
+++ b/certbot/tests/plugins/storage_test.py
@@ -1,17 +1,20 @@
"""Tests for certbot.plugins.storage.PluginStorage"""
import json
+from typing import Iterable
+from typing import List
+from typing import Optional
import unittest
+from certbot import errors
+from certbot.compat import filesystem
+from certbot.compat import os
+from certbot.tests import util as test_util
+
try:
import mock
except ImportError: # pragma: no cover
from unittest import mock
-from certbot import errors
-from certbot.compat import filesystem
-from certbot.compat import os
-from certbot.plugins import common
-from certbot.tests import util as test_util
class PluginStorageTest(test_util.ConfigTestCase):
@@ -19,7 +22,7 @@ class PluginStorageTest(test_util.ConfigTestCase):
def setUp(self):
super().setUp()
- self.plugin_cls = common.Installer
+ self.plugin_cls = test_util.DummyInstaller
filesystem.mkdir(self.config.config_dir)
with mock.patch("certbot.reverter.util"):
self.plugin = self.plugin_cls(config=self.config, name="mockplugin")
@@ -101,7 +104,6 @@ def test_namespace_isolation(self):
plugin2.storage.fetch, "first")
self.assertEqual(plugin1.storage.fetch("first_key"), "first_value")
-
def test_saved_state(self):
self.plugin.storage.put("testkey", "testvalue")
# Write to disk
diff --git a/certbot/tests/plugins/webroot_test.py b/certbot/tests/plugins/webroot_test.py
index f158486b6c9..53bea8218dc 100644
--- a/certbot/tests/plugins/webroot_test.py
+++ b/certbot/tests/plugins/webroot_test.py
@@ -69,7 +69,7 @@ def test_add_parser_arguments(self):
def test_prepare(self):
self.auth.prepare() # shouldn't raise any exceptions
- @test_util.patch_get_utility()
+ @test_util.patch_display_util()
def test_webroot_from_list(self, mock_get_utility):
self.config.webroot_path = []
self.config.webroot_map = {"otherthing.com": self.path}
@@ -86,7 +86,7 @@ def test_webroot_from_list(self, mock_get_utility):
self.assertEqual(self.config.webroot_map[self.achall.domain],
self.path)
- @test_util.patch_get_utility()
+ @test_util.patch_display_util()
def test_webroot_from_list_help_and_cancel(self, mock_get_utility):
self.config.webroot_path = []
self.config.webroot_map = {"otherthing.com": self.path}
@@ -101,7 +101,7 @@ def test_webroot_from_list_help_and_cancel(self, mock_get_utility):
webroot in call[0][1]
for webroot in self.config.webroot_map.values()))
- @test_util.patch_get_utility()
+ @test_util.patch_display_util()
def test_new_webroot(self, mock_get_utility):
self.config.webroot_path = []
self.config.webroot_map = {"something.com": self.path}
@@ -116,7 +116,7 @@ def test_new_webroot(self, mock_get_utility):
self.assertEqual(self.config.webroot_map[self.achall.domain], self.path)
- @test_util.patch_get_utility()
+ @test_util.patch_display_util()
def test_new_webroot_empty_map_cancel(self, mock_get_utility):
self.config.webroot_path = []
self.config.webroot_map = {}
@@ -154,7 +154,7 @@ def test_failed_chown(self, mock_ownership):
mock_ownership.side_effect = OSError(errno.EACCES, "msg")
self.auth.perform([self.achall]) # exception caught and logged
- @test_util.patch_get_utility()
+ @test_util.patch_display_util()
def test_perform_new_webroot_not_in_map(self, mock_get_utility):
new_webroot = tempfile.mkdtemp()
self.config.webroot_path = []
diff --git a/certbot/tests/renewal_test.py b/certbot/tests/renewal_test.py
index edee8df6cc7..f00b81898fd 100644
--- a/certbot/tests/renewal_test.py
+++ b/certbot/tests/renewal_test.py
@@ -1,19 +1,19 @@
"""Tests for certbot._internal.renewal"""
import copy
-
import unittest
-try:
- import mock
-except ImportError: # pragma: no cover
- from unittest import mock
-
from acme import challenges
from certbot import errors
from certbot._internal import configuration
from certbot._internal import storage
import certbot.tests.util as test_util
+try:
+ import mock
+except ImportError: # pragma: no cover
+ from unittest import mock
+
+
class RenewalTest(test_util.ConfigTestCase):
@mock.patch('certbot._internal.cli.set_by_cli')
@@ -100,7 +100,7 @@ def test_reuse_ec_key_renewal_params(self):
assert self.config.elliptic_curve == 'secp256r1'
- @test_util.patch_get_utility()
+ @test_util.patch_display_util()
@mock.patch('certbot._internal.renewal.cli.set_by_cli')
def test_remove_deprecated_config_elements(self, mock_set_by_cli, unused_mock_get_utility):
mock_set_by_cli.return_value = False
diff --git a/certbot/tests/renewupdater_test.py b/certbot/tests/renewupdater_test.py
index 55faff2a4a2..f086e3cf31f 100644
--- a/certbot/tests/renewupdater_test.py
+++ b/certbot/tests/renewupdater_test.py
@@ -26,7 +26,7 @@ def setUp(self):
@mock.patch('certbot._internal.main._get_and_save_cert')
@mock.patch('certbot._internal.plugins.selection.choose_configurator_plugins')
@mock.patch('certbot._internal.plugins.selection.get_unprepared_installer')
- @test_util.patch_get_utility()
+ @test_util.patch_display_util()
def test_server_updates(self, _, mock_geti, mock_select, mock_getsave):
mock_getsave.return_value = mock.MagicMock()
mock_generic_updater = self.generic_updater
diff --git a/letsencrypt-auto b/letsencrypt-auto
deleted file mode 100755
index c37c45596ef..00000000000
--- a/letsencrypt-auto
+++ /dev/null
@@ -1,1988 +0,0 @@
-#!/bin/sh
-#
-# Download and run the latest release version of the Certbot client.
-#
-# NOTE: THIS SCRIPT IS AUTO-GENERATED AND SELF-UPDATING
-#
-# IF YOU WANT TO EDIT IT LOCALLY, *ALWAYS* RUN YOUR COPY WITH THE
-# "--no-self-upgrade" FLAG
-#
-# IF YOU WANT TO SEND PULL REQUESTS, THE REAL SOURCE FOR THIS FILE IS
-# letsencrypt-auto-source/letsencrypt-auto.template AND
-# letsencrypt-auto-source/pieces/bootstrappers/*
-
-set -e # Work even if somebody does "sh thisscript.sh".
-
-# Note: you can set XDG_DATA_HOME or VENV_PATH before running this script,
-# if you want to change where the virtual environment will be installed
-
-# HOME might not be defined when being run through something like systemd
-if [ -z "$HOME" ]; then
- HOME=~root
-fi
-if [ -z "$XDG_DATA_HOME" ]; then
- XDG_DATA_HOME=~/.local/share
-fi
-if [ -z "$VENV_PATH" ]; then
- # We export these values so they are preserved properly if this script is
- # rerun with sudo/su where $HOME/$XDG_DATA_HOME may have a different value.
- export OLD_VENV_PATH="$XDG_DATA_HOME/letsencrypt"
- export VENV_PATH="/opt/eff.org/certbot/venv"
-fi
-VENV_BIN="$VENV_PATH/bin"
-BOOTSTRAP_VERSION_PATH="$VENV_PATH/certbot-auto-bootstrap-version.txt"
-LE_AUTO_VERSION="1.14.0"
-BASENAME=$(basename $0)
-USAGE="Usage: $BASENAME [OPTIONS]
-A self-updating wrapper script for the Certbot ACME client. When run, updates
-to both this script and certbot will be downloaded and installed. After
-ensuring you have the latest versions installed, certbot will be invoked with
-all arguments you have provided.
-
-Help for certbot itself cannot be provided until it is installed.
-
- --debug attempt experimental installation
- -h, --help print this help
- -n, --non-interactive, --noninteractive run without asking for user input
- --no-bootstrap do not install OS dependencies
- --no-permissions-check do not warn about file system permissions
- --no-self-upgrade do not download updates
- --os-packages-only install OS dependencies and exit
- --install-only install certbot, upgrade if needed, and exit
- -v, --verbose provide more output
- -q, --quiet provide only update/error output;
- implies --non-interactive
-
-All arguments are accepted and forwarded to the Certbot client when run."
-export CERTBOT_AUTO="$0"
-
-for arg in "$@" ; do
- case "$arg" in
- --debug)
- DEBUG=1;;
- --os-packages-only)
- OS_PACKAGES_ONLY=1;;
- --install-only)
- INSTALL_ONLY=1;;
- --no-self-upgrade)
- # Do not upgrade this script (also prevents client upgrades, because each
- # copy of the script pins a hash of the python client)
- NO_SELF_UPGRADE=1;;
- --no-permissions-check)
- NO_PERMISSIONS_CHECK=1;;
- --no-bootstrap)
- NO_BOOTSTRAP=1;;
- --help)
- HELP=1;;
- --noninteractive|--non-interactive)
- NONINTERACTIVE=1;;
- --quiet)
- QUIET=1;;
- renew)
- ASSUME_YES=1;;
- --verbose)
- VERBOSE=1;;
- -[!-]*)
- OPTIND=1
- while getopts ":hnvq" short_arg $arg; do
- case "$short_arg" in
- h)
- HELP=1;;
- n)
- NONINTERACTIVE=1;;
- q)
- QUIET=1;;
- v)
- VERBOSE=1;;
- esac
- done;;
- esac
-done
-
-if [ $BASENAME = "letsencrypt-auto" ]; then
- # letsencrypt-auto does not respect --help or --yes for backwards compatibility
- NONINTERACTIVE=1
- HELP=0
-fi
-
-# Set ASSUME_YES to 1 if QUIET or NONINTERACTIVE
-if [ "$QUIET" = 1 -o "$NONINTERACTIVE" = 1 ]; then
- ASSUME_YES=1
-fi
-
-say() {
- if [ "$QUIET" != 1 ]; then
- echo "$@"
- fi
-}
-
-error() {
- echo "$@"
-}
-
-# Support for busybox and others where there is no "command",
-# but "which" instead
-if command -v command > /dev/null 2>&1 ; then
- export EXISTS="command -v"
-elif which which > /dev/null 2>&1 ; then
- export EXISTS="which"
-else
- error "Cannot find command nor which... please install one!"
- exit 1
-fi
-
-# Certbot itself needs root access for almost all modes of operation.
-# certbot-auto needs root access to bootstrap OS dependencies and install
-# Certbot at a protected path so it can be safely run as root. To accomplish
-# this, this script will attempt to run itself as root if it doesn't have the
-# necessary privileges by using `sudo` or falling back to `su` if it is not
-# available. The mechanism used to obtain root access can be set explicitly by
-# setting the environment variable LE_AUTO_SUDO to 'sudo', 'su', 'su_sudo',
-# 'SuSudo', or '' as used below.
-
-# Because the parameters in `su -c` has to be a string,
-# we need to properly escape it.
-SuSudo() {
- args=""
- # This `while` loop iterates over all parameters given to this function.
- # For each parameter, all `'` will be replace by `'"'"'`, and the escaped string
- # will be wrapped in a pair of `'`, then appended to `$args` string
- # For example, `echo "It's only 1\$\!"` will be escaped to:
- # 'echo' 'It'"'"'s only 1$!'
- # │ │└┼┘│
- # │ │ │ └── `'s only 1$!'` the literal string
- # │ │ └── `\"'\"` is a single quote (as a string)
- # │ └── `'It'`, to be concatenated with the strings following it
- # └── `echo` wrapped in a pair of `'`, it's totally fine for the shell command itself
- while [ $# -ne 0 ]; do
- args="$args'$(printf "%s" "$1" | sed -e "s/'/'\"'\"'/g")' "
- shift
- done
- su root -c "$args"
-}
-
-# Sets the environment variable SUDO to be the name of the program or function
-# to call to get root access. If this script already has root privleges, SUDO
-# is set to an empty string. The value in SUDO should be run with the command
-# to called with root privileges as arguments.
-SetRootAuthMechanism() {
- SUDO=""
- if [ -n "${LE_AUTO_SUDO+x}" ]; then
- case "$LE_AUTO_SUDO" in
- SuSudo|su_sudo|su)
- SUDO=SuSudo
- ;;
- sudo)
- SUDO="sudo -E"
- ;;
- '')
- # If we're not running with root, don't check that this script can only
- # be modified by system users and groups.
- NO_PERMISSIONS_CHECK=1
- ;;
- *)
- error "Error: unknown root authorization mechanism '$LE_AUTO_SUDO'."
- exit 1
- esac
- say "Using preset root authorization mechanism '$LE_AUTO_SUDO'."
- else
- if test "`id -u`" -ne "0" ; then
- if $EXISTS sudo 1>/dev/null 2>&1; then
- SUDO="sudo -E"
- else
- say \"sudo\" is not available, will use \"su\" for installation steps...
- SUDO=SuSudo
- fi
- fi
- fi
-}
-
-if [ "$1" = "--cb-auto-has-root" ]; then
- shift 1
-else
- SetRootAuthMechanism
- if [ -n "$SUDO" ]; then
- say "Requesting to rerun $0 with root privileges..."
- $SUDO "$0" --cb-auto-has-root "$@"
- exit 0
- fi
-fi
-
-# Runs this script again with the given arguments. --cb-auto-has-root is added
-# to the command line arguments to ensure we don't try to acquire root a
-# second time. After the script is rerun, we exit the current script.
-RerunWithArgs() {
- "$0" --cb-auto-has-root "$@"
- exit 0
-}
-
-BootstrapMessage() {
- # Arguments: Platform name
- say "Bootstrapping dependencies for $1... (you can skip this with --no-bootstrap)"
-}
-
-ExperimentalBootstrap() {
- # Arguments: Platform name, bootstrap function name
- if [ "$DEBUG" = 1 ]; then
- if [ "$2" != "" ]; then
- BootstrapMessage $1
- $2
- fi
- else
- error "FATAL: $1 support is very experimental at present..."
- error "if you would like to work on improving it, please ensure you have backups"
- error "and then run this script again with the --debug flag!"
- error "Alternatively, you can install OS dependencies yourself and run this script"
- error "again with --no-bootstrap."
- exit 1
- fi
-}
-
-DeprecationBootstrap() {
- # Arguments: Platform name, bootstrap function name
- if [ "$DEBUG" = 1 ]; then
- if [ "$2" != "" ]; then
- BootstrapMessage $1
- $2
- fi
- else
- error "WARNING: certbot-auto support for this $1 is DEPRECATED!"
- error "Please visit certbot.eff.org to learn how to download a version of"
- error "Certbot that is packaged for your system. While an existing version"
- error "of certbot-auto may work currently, we have stopped supporting updating"
- error "system packages for your system. Please switch to a packaged version"
- error "as soon as possible."
- exit 1
- fi
-}
-
-MIN_PYTHON_2_VERSION="2.7"
-MIN_PYVER2=$(echo "$MIN_PYTHON_2_VERSION" | sed 's/\.//')
-MIN_PYTHON_3_VERSION="3.6"
-MIN_PYVER3=$(echo "$MIN_PYTHON_3_VERSION" | sed 's/\.//')
-# Sets LE_PYTHON to Python version string and PYVER to the first two
-# digits of the python version.
-# MIN_PYVER and MIN_PYTHON_VERSION are also set by this function, and their
-# values depend on if we try to use Python 3 or Python 2.
-DeterminePythonVersion() {
- # Arguments: "NOCRASH" if we shouldn't crash if we don't find a good python
- #
- # If no Python is found, PYVER is set to 0.
- if [ "$USE_PYTHON_3" = 1 ]; then
- MIN_PYVER=$MIN_PYVER3
- MIN_PYTHON_VERSION=$MIN_PYTHON_3_VERSION
- for LE_PYTHON in "$LE_PYTHON" python3; do
- # Break (while keeping the LE_PYTHON value) if found.
- $EXISTS "$LE_PYTHON" > /dev/null && break
- done
- else
- MIN_PYVER=$MIN_PYVER2
- MIN_PYTHON_VERSION=$MIN_PYTHON_2_VERSION
- for LE_PYTHON in "$LE_PYTHON" python2.7 python27 python2 python; do
- # Break (while keeping the LE_PYTHON value) if found.
- $EXISTS "$LE_PYTHON" > /dev/null && break
- done
- fi
- if [ "$?" != "0" ]; then
- if [ "$1" != "NOCRASH" ]; then
- error "Cannot find any Pythons; please install one!"
- exit 1
- else
- PYVER=0
- return 0
- fi
- fi
-
- PYVER=$("$LE_PYTHON" -V 2>&1 | cut -d" " -f 2 | cut -d. -f1,2 | sed 's/\.//')
- if [ "$PYVER" -lt "$MIN_PYVER" ]; then
- if [ "$1" != "NOCRASH" ]; then
- error "You have an ancient version of Python entombed in your operating system..."
- error "This isn't going to work; you'll need at least version $MIN_PYTHON_VERSION."
- exit 1
- fi
- fi
-}
-
-# If new packages are installed by BootstrapDebCommon below, this version
-# number must be increased.
-BOOTSTRAP_DEB_COMMON_VERSION=1
-
-BootstrapDebCommon() {
- # Current version tested with:
- #
- # - Ubuntu
- # - 14.04 (x64)
- # - 15.04 (x64)
- # - Debian
- # - 7.9 "wheezy" (x64)
- # - sid (2015-10-21) (x64)
-
- # Past versions tested with:
- #
- # - Debian 8.0 "jessie" (x64)
- # - Raspbian 7.8 (armhf)
-
- # Believed not to work:
- #
- # - Debian 6.0.10 "squeeze" (x64)
-
- if [ "$QUIET" = 1 ]; then
- QUIET_FLAG='-qq'
- fi
-
- apt-get $QUIET_FLAG update || error apt-get update hit problems but continuing anyway...
-
- # virtualenv binary can be found in different packages depending on
- # distro version (#346)
-
- virtualenv=
- # virtual env is known to apt and is installable
- if apt-cache show virtualenv > /dev/null 2>&1 ; then
- if ! LC_ALL=C apt-cache --quiet=0 show virtualenv 2>&1 | grep -q 'No packages found'; then
- virtualenv="virtualenv"
- fi
- fi
-
- if apt-cache show python-virtualenv > /dev/null 2>&1; then
- virtualenv="$virtualenv python-virtualenv"
- fi
-
- augeas_pkg="libaugeas0 augeas-lenses"
-
- if [ "$ASSUME_YES" = 1 ]; then
- YES_FLAG="-y"
- fi
-
- apt-get install $QUIET_FLAG $YES_FLAG --no-install-recommends \
- python \
- python-dev \
- $virtualenv \
- gcc \
- $augeas_pkg \
- libssl-dev \
- openssl \
- libffi-dev \
- ca-certificates \
-
-
- if ! $EXISTS virtualenv > /dev/null ; then
- error Failed to install a working \"virtualenv\" command, exiting
- exit 1
- fi
-}
-
-# If new packages are installed by BootstrapRpmCommonBase below, version
-# numbers in rpm_common.sh and rpm_python3.sh must be increased.
-
-# Sets TOOL to the name of the package manager
-# Sets appropriate values for YES_FLAG and QUIET_FLAG based on $ASSUME_YES and $QUIET_FLAG.
-# Note: this function is called both while selecting the bootstrap scripts and
-# during the actual bootstrap. Some things like prompting to user can be done in the latter
-# case, but not in the former one.
-InitializeRPMCommonBase() {
- if type dnf 2>/dev/null
- then
- TOOL=dnf
- elif type yum 2>/dev/null
- then
- TOOL=yum
-
- else
- error "Neither yum nor dnf found. Aborting bootstrap!"
- exit 1
- fi
-
- if [ "$ASSUME_YES" = 1 ]; then
- YES_FLAG="-y"
- fi
- if [ "$QUIET" = 1 ]; then
- QUIET_FLAG='--quiet'
- fi
-}
-
-BootstrapRpmCommonBase() {
- # Arguments: whitespace-delimited python packages to install
-
- InitializeRPMCommonBase # This call is superfluous in practice
-
- pkgs="
- gcc
- augeas-libs
- openssl
- openssl-devel
- libffi-devel
- redhat-rpm-config
- ca-certificates
- "
-
- # Add the python packages
- pkgs="$pkgs
- $1
- "
-
- if $TOOL list installed "httpd" >/dev/null 2>&1; then
- pkgs="$pkgs
- mod_ssl
- "
- fi
-
- if ! $TOOL install $YES_FLAG $QUIET_FLAG $pkgs; then
- error "Could not install OS dependencies. Aborting bootstrap!"
- exit 1
- fi
-}
-
-# If new packages are installed by BootstrapRpmCommon below, this version
-# number must be increased.
-BOOTSTRAP_RPM_COMMON_VERSION=1
-
-BootstrapRpmCommon() {
- # Tested with:
- # - Fedora 20, 21, 22, 23 (x64)
- # - Centos 7 (x64: on DigitalOcean droplet)
- # - CentOS 7 Minimal install in a Hyper-V VM
- # - CentOS 6
-
- InitializeRPMCommonBase
-
- # Most RPM distros use the "python" or "python-" naming convention. Let's try that first.
- if $TOOL list python >/dev/null 2>&1; then
- python_pkgs="$python
- python-devel
- python-virtualenv
- python-tools
- python-pip
- "
- # Fedora 26 starts to use the prefix python2 for python2 based packages.
- # this elseif is theoretically for any Fedora over version 26:
- elif $TOOL list python2 >/dev/null 2>&1; then
- python_pkgs="$python2
- python2-libs
- python2-setuptools
- python2-devel
- python2-virtualenv
- python2-tools
- python2-pip
- "
- # Some distros and older versions of current distros use a "python27"
- # instead of the "python" or "python-" naming convention.
- else
- python_pkgs="$python27
- python27-devel
- python27-virtualenv
- python27-tools
- python27-pip
- "
- fi
-
- BootstrapRpmCommonBase "$python_pkgs"
-}
-
-# If new packages are installed by BootstrapRpmPython3 below, this version
-# number must be increased.
-BOOTSTRAP_RPM_PYTHON3_LEGACY_VERSION=1
-
-# Checks if rh-python36 can be installed.
-Python36SclIsAvailable() {
- InitializeRPMCommonBase >/dev/null 2>&1;
-
- if "${TOOL}" list rh-python36 >/dev/null 2>&1; then
- return 0
- fi
- if "${TOOL}" list centos-release-scl >/dev/null 2>&1; then
- return 0
- fi
- return 1
-}
-
-# Try to enable rh-python36 from SCL if it is necessary and possible.
-EnablePython36SCL() {
- if "$EXISTS" python3.6 > /dev/null 2> /dev/null; then
- return 0
- fi
- if [ ! -f /opt/rh/rh-python36/enable ]; then
- return 0
- fi
- set +e
- if ! . /opt/rh/rh-python36/enable; then
- error 'Unable to enable rh-python36!'
- exit 1
- fi
- set -e
-}
-
-# This bootstrap concerns old RedHat-based distributions that do not ship by default
-# with Python 2.7, but only Python 2.6. We bootstrap them by enabling SCL and installing
-# Python 3.6. Some of these distributions are: CentOS/RHEL/OL/SL 6.
-BootstrapRpmPython3Legacy() {
- # Tested with:
- # - CentOS 6
-
- InitializeRPMCommonBase
-
- if ! "${TOOL}" list rh-python36 >/dev/null 2>&1; then
- echo "To use Certbot on this operating system, packages from the SCL repository need to be installed."
- if ! "${TOOL}" list centos-release-scl >/dev/null 2>&1; then
- error "Enable the SCL repository and try running Certbot again."
- exit 1
- fi
- if [ "${ASSUME_YES}" = 1 ]; then
- /bin/echo -n "Enabling the SCL repository in 3 seconds... (Press Ctrl-C to cancel)"
- sleep 1s
- /bin/echo -ne "\e[0K\rEnabling the SCL repository in 2 seconds... (Press Ctrl-C to cancel)"
- sleep 1s
- /bin/echo -e "\e[0K\rEnabling the SCL repository in 1 second... (Press Ctrl-C to cancel)"
- sleep 1s
- fi
- if ! "${TOOL}" install "${YES_FLAG}" "${QUIET_FLAG}" centos-release-scl; then
- error "Could not enable SCL. Aborting bootstrap!"
- exit 1
- fi
- fi
-
- # CentOS 6 must use rh-python36 from SCL
- if "${TOOL}" list rh-python36 >/dev/null 2>&1; then
- python_pkgs="rh-python36-python
- rh-python36-python-virtualenv
- rh-python36-python-devel
- "
- else
- error "No supported Python package available to install. Aborting bootstrap!"
- exit 1
- fi
-
- BootstrapRpmCommonBase "${python_pkgs}"
-
- # Enable SCL rh-python36 after bootstrapping.
- EnablePython36SCL
-}
-
-# If new packages are installed by BootstrapRpmPython3 below, this version
-# number must be increased.
-BOOTSTRAP_RPM_PYTHON3_VERSION=1
-
-BootstrapRpmPython3() {
- # Tested with:
- # - Fedora 29
-
- InitializeRPMCommonBase
-
- # Fedora 29 must use python3-virtualenv
- if $TOOL list python3-virtualenv >/dev/null 2>&1; then
- python_pkgs="python3
- python3-virtualenv
- python3-devel
- "
- else
- error "No supported Python package available to install. Aborting bootstrap!"
- exit 1
- fi
-
- BootstrapRpmCommonBase "$python_pkgs"
-}
-
-# If new packages are installed by BootstrapSuseCommon below, this version
-# number must be increased.
-BOOTSTRAP_SUSE_COMMON_VERSION=1
-
-BootstrapSuseCommon() {
- # SLE12 don't have python-virtualenv
-
- if [ "$ASSUME_YES" = 1 ]; then
- zypper_flags="-nq"
- install_flags="-l"
- fi
-
- if [ "$QUIET" = 1 ]; then
- QUIET_FLAG='-qq'
- fi
-
- if zypper search -x python-virtualenv >/dev/null 2>&1; then
- OPENSUSE_VIRTUALENV_PACKAGES="python-virtualenv"
- else
- # Since Leap 15.0 (and associated Tumbleweed version), python-virtualenv
- # is a source package, and python2-virtualenv must be used instead.
- # Also currently python2-setuptools is not a dependency of python2-virtualenv,
- # while it should be. Installing it explicitly until upstream fix.
- OPENSUSE_VIRTUALENV_PACKAGES="python2-virtualenv python2-setuptools"
- fi
-
- zypper $QUIET_FLAG $zypper_flags in $install_flags \
- python \
- python-devel \
- $OPENSUSE_VIRTUALENV_PACKAGES \
- gcc \
- augeas-lenses \
- libopenssl-devel \
- libffi-devel \
- ca-certificates
-}
-
-# If new packages are installed by BootstrapArchCommon below, this version
-# number must be increased.
-BOOTSTRAP_ARCH_COMMON_VERSION=1
-
-BootstrapArchCommon() {
- # Tested with:
- # - ArchLinux (x86_64)
- #
- # "python-virtualenv" is Python3, but "python2-virtualenv" provides
- # only "virtualenv2" binary, not "virtualenv".
-
- deps="
- python2
- python-virtualenv
- gcc
- augeas
- openssl
- libffi
- ca-certificates
- pkg-config
- "
-
- # pacman -T exits with 127 if there are missing dependencies
- missing=$(pacman -T $deps) || true
-
- if [ "$ASSUME_YES" = 1 ]; then
- noconfirm="--noconfirm"
- fi
-
- if [ "$missing" ]; then
- if [ "$QUIET" = 1 ]; then
- pacman -S --needed $missing $noconfirm > /dev/null
- else
- pacman -S --needed $missing $noconfirm
- fi
- fi
-}
-
-# If new packages are installed by BootstrapGentooCommon below, this version
-# number must be increased.
-BOOTSTRAP_GENTOO_COMMON_VERSION=1
-
-BootstrapGentooCommon() {
- PACKAGES="
- dev-lang/python:2.7
- dev-python/virtualenv
- app-admin/augeas
- dev-libs/openssl
- dev-libs/libffi
- app-misc/ca-certificates
- virtual/pkgconfig"
-
- ASK_OPTION="--ask"
- if [ "$ASSUME_YES" = 1 ]; then
- ASK_OPTION=""
- fi
-
- case "$PACKAGE_MANAGER" in
- (paludis)
- cave resolve --preserve-world --keep-targets if-possible $PACKAGES -x
- ;;
- (pkgcore)
- pmerge --noreplace --oneshot $ASK_OPTION $PACKAGES
- ;;
- (portage|*)
- emerge --noreplace --oneshot $ASK_OPTION $PACKAGES
- ;;
- esac
-}
-
-# If new packages are installed by BootstrapFreeBsd below, this version number
-# must be increased.
-BOOTSTRAP_FREEBSD_VERSION=1
-
-BootstrapFreeBsd() {
- if [ "$QUIET" = 1 ]; then
- QUIET_FLAG="--quiet"
- fi
-
- pkg install -Ay $QUIET_FLAG \
- python \
- py27-virtualenv \
- augeas \
- libffi
-}
-
-# If new packages are installed by BootstrapMac below, this version number must
-# be increased.
-BOOTSTRAP_MAC_VERSION=1
-
-BootstrapMac() {
- if hash brew 2>/dev/null; then
- say "Using Homebrew to install dependencies..."
- pkgman=brew
- pkgcmd="brew install"
- elif hash port 2>/dev/null; then
- say "Using MacPorts to install dependencies..."
- pkgman=port
- pkgcmd="port install"
- else
- say "No Homebrew/MacPorts; installing Homebrew..."
- ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
- pkgman=brew
- pkgcmd="brew install"
- fi
-
- $pkgcmd augeas
- if [ "$(which python)" = "/System/Library/Frameworks/Python.framework/Versions/2.7/bin/python" \
- -o "$(which python)" = "/usr/bin/python" ]; then
- # We want to avoid using the system Python because it requires root to use pip.
- # python.org, MacPorts or HomeBrew Python installations should all be OK.
- say "Installing python..."
- $pkgcmd python
- fi
-
- # Workaround for _dlopen not finding augeas on macOS
- if [ "$pkgman" = "port" ] && ! [ -e "/usr/local/lib/libaugeas.dylib" ] && [ -e "/opt/local/lib/libaugeas.dylib" ]; then
- say "Applying augeas workaround"
- mkdir -p /usr/local/lib/
- ln -s /opt/local/lib/libaugeas.dylib /usr/local/lib/
- fi
-
- if ! hash pip 2>/dev/null; then
- say "pip not installed"
- say "Installing pip..."
- curl --silent --show-error --retry 5 https://bootstrap.pypa.io/get-pip.py | python
- fi
-
- if ! hash virtualenv 2>/dev/null; then
- say "virtualenv not installed."
- say "Installing with pip..."
- pip install virtualenv
- fi
-}
-
-# If new packages are installed by BootstrapSmartOS below, this version number
-# must be increased.
-BOOTSTRAP_SMARTOS_VERSION=1
-
-BootstrapSmartOS() {
- pkgin update
- pkgin -y install 'gcc49' 'py27-augeas' 'py27-virtualenv'
-}
-
-# If new packages are installed by BootstrapMageiaCommon below, this version
-# number must be increased.
-BOOTSTRAP_MAGEIA_COMMON_VERSION=1
-
-BootstrapMageiaCommon() {
- if [ "$QUIET" = 1 ]; then
- QUIET_FLAG='--quiet'
- fi
-
- if ! urpmi --force $QUIET_FLAG \
- python \
- libpython-devel \
- python-virtualenv
- then
- error "Could not install Python dependencies. Aborting bootstrap!"
- exit 1
- fi
-
- if ! urpmi --force $QUIET_FLAG \
- git \
- gcc \
- python-augeas \
- libopenssl-devel \
- libffi-devel \
- rootcerts
- then
- error "Could not install additional dependencies. Aborting bootstrap!"
- exit 1
- fi
-}
-
-
-# Set Bootstrap to the function that installs OS dependencies on this system
-# and BOOTSTRAP_VERSION to the unique identifier for the current version of
-# that function. If Bootstrap is set to a function that doesn't install any
-# packages BOOTSTRAP_VERSION is not set.
-if [ -f /etc/debian_version ]; then
- DEPRECATED_OS=1
- NO_SELF_UPGRADE=1
-elif [ -f /etc/mageia-release ]; then
- # Mageia has both /etc/mageia-release and /etc/redhat-release
- DEPRECATED_OS=1
- NO_SELF_UPGRADE=1
-elif [ -f /etc/redhat-release ]; then
- DEPRECATED_OS=1
- NO_SELF_UPGRADE=1
- # Run DeterminePythonVersion to decide on the basis of available Python versions
- # whether to use 2.x or 3.x on RedHat-like systems.
- # Then, revert LE_PYTHON to its previous state.
- prev_le_python="$LE_PYTHON"
- unset LE_PYTHON
- DeterminePythonVersion "NOCRASH"
-
- RPM_DIST_NAME=`(. /etc/os-release 2> /dev/null && echo $ID) || echo "unknown"`
-
- if [ "$PYVER" -eq 26 -a $(uname -m) != 'x86_64' ]; then
- # 32 bits CentOS 6 and affiliates are not supported anymore by certbot-auto.
- DEPRECATED_OS=1
- fi
-
- # Set RPM_DIST_VERSION to VERSION_ID from /etc/os-release after splitting on
- # '.' characters (e.g. "8.0" becomes "8"). If the command exits with an
- # error, RPM_DIST_VERSION is set to "unknown".
- RPM_DIST_VERSION=$( (. /etc/os-release 2> /dev/null && echo "$VERSION_ID") | cut -d '.' -f1 || echo "unknown")
-
- # If RPM_DIST_VERSION is an empty string or it contains any nonnumeric
- # characters, the value is unexpected so we set RPM_DIST_VERSION to 0.
- if [ -z "$RPM_DIST_VERSION" ] || [ -n "$(echo "$RPM_DIST_VERSION" | tr -d '[0-9]')" ]; then
- RPM_DIST_VERSION=0
- fi
-
- # Handle legacy RPM distributions
- if [ "$PYVER" -eq 26 ]; then
- # Check if an automated bootstrap can be achieved on this system.
- if ! Python36SclIsAvailable; then
- INTERACTIVE_BOOTSTRAP=1
- fi
-
- USE_PYTHON_3=1
-
- # Try now to enable SCL rh-python36 for systems already bootstrapped
- # NB: EnablePython36SCL has been defined along with BootstrapRpmPython3Legacy in certbot-auto
- EnablePython36SCL
- else
- # Starting to Fedora 29, python2 is on a deprecation path. Let's move to python3 then.
- # RHEL 8 also uses python3 by default.
- if [ "$RPM_DIST_NAME" = "fedora" -a "$RPM_DIST_VERSION" -ge 29 ]; then
- RPM_USE_PYTHON_3=1
- elif [ "$RPM_DIST_NAME" = "rhel" -a "$RPM_DIST_VERSION" -ge 8 ]; then
- RPM_USE_PYTHON_3=1
- elif [ "$RPM_DIST_NAME" = "centos" -a "$RPM_DIST_VERSION" -ge 8 ]; then
- RPM_USE_PYTHON_3=1
- else
- RPM_USE_PYTHON_3=0
- fi
-
- if [ "$RPM_USE_PYTHON_3" = 1 ]; then
- USE_PYTHON_3=1
- fi
- fi
-
- LE_PYTHON="$prev_le_python"
-elif [ -f /etc/os-release ] && `grep -q openSUSE /etc/os-release` ; then
- DEPRECATED_OS=1
- NO_SELF_UPGRADE=1
-elif [ -f /etc/arch-release ]; then
- DEPRECATED_OS=1
- NO_SELF_UPGRADE=1
-elif [ -f /etc/manjaro-release ]; then
- DEPRECATED_OS=1
- NO_SELF_UPGRADE=1
-elif [ -f /etc/gentoo-release ]; then
- DEPRECATED_OS=1
- NO_SELF_UPGRADE=1
-elif uname | grep -iq FreeBSD ; then
- DEPRECATED_OS=1
- NO_SELF_UPGRADE=1
-elif uname | grep -iq Darwin ; then
- DEPRECATED_OS=1
- NO_SELF_UPGRADE=1
-elif [ -f /etc/issue ] && grep -iq "Amazon Linux" /etc/issue ; then
- DEPRECATED_OS=1
- NO_SELF_UPGRADE=1
-elif [ -f /etc/product ] && grep -q "Joyent Instance" /etc/product ; then
- DEPRECATED_OS=1
- NO_SELF_UPGRADE=1
-else
- DEPRECATED_OS=1
- NO_SELF_UPGRADE=1
-fi
-
-# We handle this case after determining the normal bootstrap version to allow
-# variables like USE_PYTHON_3 to be properly set. As described above, if the
-# Bootstrap function doesn't install any packages, BOOTSTRAP_VERSION should not
-# be set so we unset it here.
-if [ "$NO_BOOTSTRAP" = 1 ]; then
- Bootstrap() {
- :
- }
- unset BOOTSTRAP_VERSION
-fi
-
-if [ "$DEPRECATED_OS" = 1 ]; then
- Bootstrap() {
- error "Skipping bootstrap because certbot-auto is deprecated on this system."
- }
- unset BOOTSTRAP_VERSION
-fi
-
-# Sets PREV_BOOTSTRAP_VERSION to the identifier for the bootstrap script used
-# to install OS dependencies on this system. PREV_BOOTSTRAP_VERSION isn't set
-# if it is unknown how OS dependencies were installed on this system.
-SetPrevBootstrapVersion() {
- if [ -f $BOOTSTRAP_VERSION_PATH ]; then
- PREV_BOOTSTRAP_VERSION=$(cat "$BOOTSTRAP_VERSION_PATH")
- # The list below only contains bootstrap version strings that existed before
- # we started writing them to disk.
- #
- # DO NOT MODIFY THIS LIST UNLESS YOU KNOW WHAT YOU'RE DOING!
- elif grep -Fqx "$BOOTSTRAP_VERSION" << "UNLIKELY_EOF"
-BootstrapDebCommon 1
-BootstrapMageiaCommon 1
-BootstrapRpmCommon 1
-BootstrapSuseCommon 1
-BootstrapArchCommon 1
-BootstrapGentooCommon 1
-BootstrapFreeBsd 1
-BootstrapMac 1
-BootstrapSmartOS 1
-UNLIKELY_EOF
- then
- # If there's no bootstrap version saved to disk, but the currently selected
- # bootstrap script is from before we started saving the version number,
- # return the currently selected version to prevent us from rebootstrapping
- # unnecessarily.
- PREV_BOOTSTRAP_VERSION="$BOOTSTRAP_VERSION"
- fi
-}
-
-TempDir() {
- mktemp -d 2>/dev/null || mktemp -d -t 'le' # Linux || macOS
-}
-
-# Returns 0 if a letsencrypt installation exists at $OLD_VENV_PATH, otherwise,
-# returns a non-zero number.
-OldVenvExists() {
- [ -n "$OLD_VENV_PATH" -a -f "$OLD_VENV_PATH/bin/letsencrypt" ]
-}
-
-# Given python path, version 1 and version 2, check if version 1 is outdated compared to version 2.
-# An unofficial version provided as version 1 (eg. 0.28.0.dev0) will be treated
-# specifically by printing "UNOFFICIAL". Otherwise, print "OUTDATED" if version 1
-# is outdated, and "UP_TO_DATE" if not.
-# This function relies only on installed python environment (2.x or 3.x) by certbot-auto.
-CompareVersions() {
- "$1" - "$2" "$3" << "UNLIKELY_EOF"
-import sys
-from distutils.version import StrictVersion
-
-try:
- current = StrictVersion(sys.argv[1])
-except ValueError:
- sys.stdout.write('UNOFFICIAL')
- sys.exit()
-
-try:
- remote = StrictVersion(sys.argv[2])
-except ValueError:
- sys.stdout.write('UP_TO_DATE')
- sys.exit()
-
-if current < remote:
- sys.stdout.write('OUTDATED')
-else:
- sys.stdout.write('UP_TO_DATE')
-UNLIKELY_EOF
-}
-
-# Create a new virtual environment for Certbot. It will overwrite any existing one.
-# Parameters: LE_PYTHON, VENV_PATH, PYVER, VERBOSE
-CreateVenv() {
- "$1" - "$2" "$3" "$4" << "UNLIKELY_EOF"
-#!/usr/bin/env python
-import os
-import shutil
-import subprocess
-import sys
-
-
-def create_venv(venv_path, pyver, verbose):
- if os.path.exists(venv_path):
- shutil.rmtree(venv_path)
-
- stdout = sys.stdout if verbose == '1' else open(os.devnull, 'w')
-
- if int(pyver) <= 27:
- # Use virtualenv binary
- environ = os.environ.copy()
- environ['VIRTUALENV_NO_DOWNLOAD'] = '1'
- command = ['virtualenv', '--no-site-packages', '--python', sys.executable, venv_path]
- subprocess.check_call(command, stdout=stdout, env=environ)
- else:
- # Use embedded venv module in Python 3
- command = [sys.executable, '-m', 'venv', venv_path]
- subprocess.check_call(command, stdout=stdout)
-
-
-if __name__ == '__main__':
- create_venv(*sys.argv[1:])
-
-UNLIKELY_EOF
-}
-
-# Check that the given PATH_TO_CHECK has secured permissions.
-# Parameters: LE_PYTHON, PATH_TO_CHECK
-CheckPathPermissions() {
- "$1" - "$2" << "UNLIKELY_EOF"
-"""Verifies certbot-auto cannot be modified by unprivileged users.
-
-This script takes the path to certbot-auto as its only command line
-argument. It then checks that the file can only be modified by uid/gid
-< 1000 and if other users can modify the file, it prints a warning with
-a suggestion on how to solve the problem.
-
-Permissions on symlinks in the absolute path of certbot-auto are ignored
-and only the canonical path to certbot-auto is checked. There could be
-permissions problems due to the symlinks that are unreported by this
-script, however, issues like this were not caused by our documentation
-and are ignored for the sake of simplicity.
-
-All warnings are printed to stdout rather than stderr so all stderr
-output from this script can be suppressed to avoid printing messages if
-this script fails for some reason.
-
-"""
-from __future__ import print_function
-
-import os
-import stat
-import sys
-
-
-FORUM_POST_URL = 'https://community.letsencrypt.org/t/certbot-auto-deployment-best-practices/91979/'
-
-
-def has_safe_permissions(path):
- """Returns True if the given path has secure permissions.
-
- The permissions are considered safe if the file is only writable by
- uid/gid < 1000.
-
- The reason we allow more IDs than 0 is because on some systems such
- as Debian, system users/groups other than uid/gid 0 are used for the
- path we recommend in our instructions which is /usr/local/bin. 1000
- was chosen because on Debian 0-999 is reserved for system IDs[1] and
- on RHEL either 0-499 or 0-999 is reserved depending on the
- version[2][3]. Due to these differences across different OSes, this
- detection isn't perfect so we only determine permissions are
- insecure when we can be reasonably confident there is a problem
- regardless of the underlying OS.
-
- [1] https://www.debian.org/doc/debian-policy/ch-opersys.html#uid-and-gid-classes
- [2] https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/6/html/deployment_guide/ch-managing_users_and_groups
- [3] https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/7/html/system_administrators_guide/ch-managing_users_and_groups
-
- :param str path: filesystem path to check
- :returns: True if the path has secure permissions, otherwise, False
- :rtype: bool
-
- """
- # os.stat follows symlinks before obtaining information about a file.
- stat_result = os.stat(path)
- if stat_result.st_mode & stat.S_IWOTH:
- return False
- if stat_result.st_mode & stat.S_IWGRP and stat_result.st_gid >= 1000:
- return False
- if stat_result.st_mode & stat.S_IWUSR and stat_result.st_uid >= 1000:
- return False
- return True
-
-
-def main(certbot_auto_path):
- current_path = os.path.realpath(certbot_auto_path)
- last_path = None
- permissions_ok = True
- # This loop makes use of the fact that os.path.dirname('/') == '/'.
- while current_path != last_path and permissions_ok:
- permissions_ok = has_safe_permissions(current_path)
- last_path = current_path
- current_path = os.path.dirname(current_path)
-
- if not permissions_ok:
- print('{0} has insecure permissions!'.format(certbot_auto_path))
- print('To learn how to fix them, visit {0}'.format(FORUM_POST_URL))
-
-
-if __name__ == '__main__':
- main(sys.argv[1])
-
-UNLIKELY_EOF
-}
-
-if [ "$1" = "--le-auto-phase2" ]; then
- # Phase 2: Create venv, install LE, and run.
-
- shift 1 # the --le-auto-phase2 arg
-
- if [ "$DEPRECATED_OS" = 1 ]; then
- # Phase 2 damage control mode for deprecated OSes.
- # In this situation, we bypass any bootstrap or certbot venv setup.
- error "Your system is not supported by certbot-auto anymore."
-
- if [ ! -d "$VENV_PATH" ] && OldVenvExists; then
- VENV_BIN="$OLD_VENV_PATH/bin"
- fi
-
- if [ -f "$VENV_BIN/letsencrypt" -a "$INSTALL_ONLY" != 1 ]; then
- error "certbot-auto and its Certbot installation will no longer receive updates."
- error "You will not receive any bug fixes including those fixing server compatibility"
- error "or security problems."
- error "Please visit https://certbot.eff.org/ to check for other alternatives."
- "$VENV_BIN/letsencrypt" "$@"
- exit 0
- else
- error "Certbot cannot be installed."
- error "Please visit https://certbot.eff.org/ to check for other alternatives."
- exit 1
- fi
- fi
-
- SetPrevBootstrapVersion
-
- if [ -z "$PHASE_1_VERSION" -a "$USE_PYTHON_3" = 1 ]; then
- unset LE_PYTHON
- fi
-
- INSTALLED_VERSION="none"
- if [ -d "$VENV_PATH" ] || OldVenvExists; then
- # If the selected Bootstrap function isn't a noop and it differs from the
- # previously used version
- if [ -n "$BOOTSTRAP_VERSION" -a "$BOOTSTRAP_VERSION" != "$PREV_BOOTSTRAP_VERSION" ]; then
- # Check if we can rebootstrap without manual user intervention: this requires that
- # certbot-auto is in non-interactive mode AND selected bootstrap does not claim to
- # require a manual user intervention.
- if [ "$NONINTERACTIVE" = 1 -a "$INTERACTIVE_BOOTSTRAP" != 1 ]; then
- CAN_REBOOTSTRAP=1
- fi
- # Check if rebootstrap can be done non-interactively and current shell is non-interactive
- # (true if stdin and stdout are not attached to a terminal).
- if [ \( "$CAN_REBOOTSTRAP" = 1 \) -o \( \( -t 0 \) -a \( -t 1 \) \) ]; then
- if [ -d "$VENV_PATH" ]; then
- rm -rf "$VENV_PATH"
- fi
- # In the case the old venv was just a symlink to the new one,
- # OldVenvExists is now false because we deleted the venv at VENV_PATH.
- if OldVenvExists; then
- rm -rf "$OLD_VENV_PATH"
- ln -s "$VENV_PATH" "$OLD_VENV_PATH"
- fi
- RerunWithArgs "$@"
- # Otherwise bootstrap needs to be done manually by the user.
- else
- # If it is because bootstrapping is interactive, --non-interactive will be of no use.
- if [ "$INTERACTIVE_BOOTSTRAP" = 1 ]; then
- error "Skipping upgrade because new OS dependencies may need to be installed."
- error "This requires manual user intervention: please run this script again manually."
- # If this is because of the environment (eg. non interactive shell without
- # --non-interactive flag set), help the user in that direction.
- else
- error "Skipping upgrade because new OS dependencies may need to be installed."
- error
- error "To upgrade to a newer version, please run this script again manually so you can"
- error "approve changes or with --non-interactive on the command line to automatically"
- error "install any required packages."
- fi
- # Set INSTALLED_VERSION to be the same so we don't update the venv
- INSTALLED_VERSION="$LE_AUTO_VERSION"
- # Continue to use OLD_VENV_PATH if the new venv doesn't exist
- if [ ! -d "$VENV_PATH" ]; then
- VENV_BIN="$OLD_VENV_PATH/bin"
- fi
- fi
- elif [ -f "$VENV_BIN/letsencrypt" ]; then
- # --version output ran through grep due to python-cryptography DeprecationWarnings
- # grep for both certbot and letsencrypt until certbot and shim packages have been released
- INSTALLED_VERSION=$("$VENV_BIN/letsencrypt" --version 2>&1 | grep "^certbot\|^letsencrypt" | cut -d " " -f 2)
- if [ -z "$INSTALLED_VERSION" ]; then
- error "Error: couldn't get currently installed version for $VENV_BIN/letsencrypt: " 1>&2
- "$VENV_BIN/letsencrypt" --version
- exit 1
- fi
- fi
- fi
-
- if [ "$LE_AUTO_VERSION" != "$INSTALLED_VERSION" ]; then
- say "Creating virtual environment..."
- DeterminePythonVersion
- CreateVenv "$LE_PYTHON" "$VENV_PATH" "$PYVER" "$VERBOSE"
-
- if [ -n "$BOOTSTRAP_VERSION" ]; then
- echo "$BOOTSTRAP_VERSION" > "$BOOTSTRAP_VERSION_PATH"
- elif [ -n "$PREV_BOOTSTRAP_VERSION" ]; then
- echo "$PREV_BOOTSTRAP_VERSION" > "$BOOTSTRAP_VERSION_PATH"
- fi
-
- say "Installing Python packages..."
- TEMP_DIR=$(TempDir)
- trap 'rm -rf "$TEMP_DIR"' EXIT
- # There is no $ interpolation due to quotes on starting heredoc delimiter.
- # -------------------------------------------------------------------------
- cat << "UNLIKELY_EOF" > "$TEMP_DIR/letsencrypt-auto-requirements.txt"
-# This is the flattened list of packages certbot-auto installs.
-# To generate this, do (with docker and package hashin installed):
-# ```
-# letsencrypt-auto-source/rebuild_dependencies.py \
-# letsencrypt-auto-source/pieces/dependency-requirements.txt
-# ```
-# If you want to update a single dependency, run commands similar to these:
-# ```
-# pip install hashin
-# hashin -r dependency-requirements.txt cryptography==1.5.2
-# ```
-ConfigArgParse==1.2.3 \
- --hash=sha256:edd17be986d5c1ba2e307150b8e5f5107aba125f3574dddd02c85d5cdcfd37dc
-certifi==2020.4.5.1 \
- --hash=sha256:1d987a998c75633c40847cc966fcf5904906c920a7f17ef374f5aa4282abd304 \
- --hash=sha256:51fcb31174be6e6664c5f69e3e1691a2d72a1a12e90f872cbdb1567eb47b6519
-cffi==1.14.0 \
- --hash=sha256:001bf3242a1bb04d985d63e138230802c6c8d4db3668fb545fb5005ddf5bb5ff \
- --hash=sha256:00789914be39dffba161cfc5be31b55775de5ba2235fe49aa28c148236c4e06b \
- --hash=sha256:028a579fc9aed3af38f4892bdcc7390508adabc30c6af4a6e4f611b0c680e6ac \
- --hash=sha256:14491a910663bf9f13ddf2bc8f60562d6bc5315c1f09c704937ef17293fb85b0 \
- --hash=sha256:1cae98a7054b5c9391eb3249b86e0e99ab1e02bb0cc0575da191aedadbdf4384 \
- --hash=sha256:2089ed025da3919d2e75a4d963d008330c96751127dd6f73c8dc0c65041b4c26 \
- --hash=sha256:2d384f4a127a15ba701207f7639d94106693b6cd64173d6c8988e2c25f3ac2b6 \
- --hash=sha256:337d448e5a725bba2d8293c48d9353fc68d0e9e4088d62a9571def317797522b \
- --hash=sha256:399aed636c7d3749bbed55bc907c3288cb43c65c4389964ad5ff849b6370603e \
- --hash=sha256:3b911c2dbd4f423b4c4fcca138cadde747abdb20d196c4a48708b8a2d32b16dd \
- --hash=sha256:3d311bcc4a41408cf5854f06ef2c5cab88f9fded37a3b95936c9879c1640d4c2 \
- --hash=sha256:62ae9af2d069ea2698bf536dcfe1e4eed9090211dbaafeeedf5cb6c41b352f66 \
- --hash=sha256:66e41db66b47d0d8672d8ed2708ba91b2f2524ece3dee48b5dfb36be8c2f21dc \
- --hash=sha256:675686925a9fb403edba0114db74e741d8181683dcf216be697d208857e04ca8 \
- --hash=sha256:7e63cbcf2429a8dbfe48dcc2322d5f2220b77b2e17b7ba023d6166d84655da55 \
- --hash=sha256:8a6c688fefb4e1cd56feb6c511984a6c4f7ec7d2a1ff31a10254f3c817054ae4 \
- --hash=sha256:8c0ffc886aea5df6a1762d0019e9cb05f825d0eec1f520c51be9d198701daee5 \
- --hash=sha256:95cd16d3dee553f882540c1ffe331d085c9e629499ceadfbda4d4fde635f4b7d \
- --hash=sha256:99f748a7e71ff382613b4e1acc0ac83bf7ad167fb3802e35e90d9763daba4d78 \
- --hash=sha256:b8c78301cefcf5fd914aad35d3c04c2b21ce8629b5e4f4e45ae6812e461910fa \
- --hash=sha256:c420917b188a5582a56d8b93bdd8e0f6eca08c84ff623a4c16e809152cd35793 \
- --hash=sha256:c43866529f2f06fe0edc6246eb4faa34f03fe88b64a0a9a942561c8e22f4b71f \
- --hash=sha256:cab50b8c2250b46fe738c77dbd25ce017d5e6fb35d3407606e7a4180656a5a6a \
- --hash=sha256:cef128cb4d5e0b3493f058f10ce32365972c554572ff821e175dbc6f8ff6924f \
- --hash=sha256:cf16e3cf6c0a5fdd9bc10c21687e19d29ad1fe863372b5543deaec1039581a30 \
- --hash=sha256:e56c744aa6ff427a607763346e4170629caf7e48ead6921745986db3692f987f \
- --hash=sha256:e577934fc5f8779c554639376beeaa5657d54349096ef24abe8c74c5d9c117c3 \
- --hash=sha256:f2b0fa0c01d8a0c7483afd9f31d7ecf2d71760ca24499c8697aeb5ca37dc090c
-chardet==3.0.4 \
- --hash=sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae \
- --hash=sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691
-configobj==5.0.6 \
- --hash=sha256:a2f5650770e1c87fb335af19a9b7eb73fc05ccf22144eb68db7d00cd2bcb0902
-cryptography==2.8 \
- --hash=sha256:02079a6addc7b5140ba0825f542c0869ff4df9a69c360e339ecead5baefa843c \
- --hash=sha256:1df22371fbf2004c6f64e927668734070a8953362cd8370ddd336774d6743595 \
- --hash=sha256:369d2346db5934345787451504853ad9d342d7f721ae82d098083e1f49a582ad \
- --hash=sha256:3cda1f0ed8747339bbdf71b9f38ca74c7b592f24f65cdb3ab3765e4b02871651 \
- --hash=sha256:44ff04138935882fef7c686878e1c8fd80a723161ad6a98da31e14b7553170c2 \
- --hash=sha256:4b1030728872c59687badcca1e225a9103440e467c17d6d1730ab3d2d64bfeff \
- --hash=sha256:58363dbd966afb4f89b3b11dfb8ff200058fbc3b947507675c19ceb46104b48d \
- --hash=sha256:6ec280fb24d27e3d97aa731e16207d58bd8ae94ef6eab97249a2afe4ba643d42 \
- --hash=sha256:7270a6c29199adc1297776937a05b59720e8a782531f1f122f2eb8467f9aab4d \
- --hash=sha256:73fd30c57fa2d0a1d7a49c561c40c2f79c7d6c374cc7750e9ac7c99176f6428e \
- --hash=sha256:7f09806ed4fbea8f51585231ba742b58cbcfbfe823ea197d8c89a5e433c7e912 \
- --hash=sha256:90df0cc93e1f8d2fba8365fb59a858f51a11a394d64dbf3ef844f783844cc793 \
- --hash=sha256:971221ed40f058f5662a604bd1ae6e4521d84e6cad0b7b170564cc34169c8f13 \
- --hash=sha256:a518c153a2b5ed6b8cc03f7ae79d5ffad7315ad4569b2d5333a13c38d64bd8d7 \
- --hash=sha256:b0de590a8b0979649ebeef8bb9f54394d3a41f66c5584fff4220901739b6b2f0 \
- --hash=sha256:b43f53f29816ba1db8525f006fa6f49292e9b029554b3eb56a189a70f2a40879 \
- --hash=sha256:d31402aad60ed889c7e57934a03477b572a03af7794fa8fb1780f21ea8f6551f \
- --hash=sha256:de96157ec73458a7f14e3d26f17f8128c959084931e8997b9e655a39c8fde9f9 \
- --hash=sha256:df6b4dca2e11865e6cfbfb708e800efb18370f5a46fd601d3755bc7f85b3a8a2 \
- --hash=sha256:ecadccc7ba52193963c0475ac9f6fa28ac01e01349a2ca48509667ef41ffd2cf \
- --hash=sha256:fb81c17e0ebe3358486cd8cc3ad78adbae58af12fc2bf2bc0bb84e8090fa5ce8
-distro==1.5.0 \
- --hash=sha256:0e58756ae38fbd8fc3020d54badb8eae17c5b9dcbed388b17bb55b8a5928df92 \
- --hash=sha256:df74eed763e18d10d0da624258524ae80486432cd17392d9c3d96f5e83cd2799
-enum34==1.1.10; python_version < '3.4' \
- --hash=sha256:a98a201d6de3f2ab3db284e70a33b0f896fbf35f8086594e8c9e74b909058d53 \
- --hash=sha256:c3858660960c984d6ab0ebad691265180da2b43f07e061c0f8dca9ef3cffd328 \
- --hash=sha256:cce6a7477ed816bd2542d03d53db9f0db935dd013b70f336a95c73979289f248
-funcsigs==1.0.2 \
- --hash=sha256:330cc27ccbf7f1e992e69fef78261dc7c6569012cf397db8d3de0234e6c937ca \
- --hash=sha256:a7bb0f2cf3a3fd1ab2732cb49eba4252c2af4240442415b4abce3b87022a8f50
-idna==2.9 \
- --hash=sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb \
- --hash=sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa
-ipaddress==1.0.23 \
- --hash=sha256:6e0f4a39e66cb5bb9a137b00276a2eff74f93b71dcbdad6f10ff7df9d3557fcc \
- --hash=sha256:b7f8e0369580bb4a24d5ba1d7cc29660a4a6987763faf1d8a8046830e020e7e2
-josepy==1.3.0 \
- --hash=sha256:c341ffa403399b18e9eae9012f804843045764d1390f9cb4648980a7569b1619 \
- --hash=sha256:e54882c64be12a2a76533f73d33cba9e331950fda9e2731e843490b774e7a01c
-mock==1.3.0 \
- --hash=sha256:1e247dbecc6ce057299eb7ee019ad68314bb93152e81d9a6110d35f4d5eca0f6 \
- --hash=sha256:3f573a18be94de886d1191f27c168427ef693e8dcfcecf95b170577b2eb69cbb
-parsedatetime==2.5 \
- --hash=sha256:3b835fc54e472c17ef447be37458b400e3fefdf14bb1ffdedb5d2c853acf4ba1 \
- --hash=sha256:d2e9ddb1e463de871d32088a3f3cea3dc8282b1b2800e081bd0ef86900451667
-pbr==5.4.5 \
- --hash=sha256:07f558fece33b05caf857474a366dfcc00562bca13dd8b47b2b3e22d9f9bf55c \
- --hash=sha256:579170e23f8e0c2f24b0de612f71f648eccb79fb1322c814ae6b3c07b5ba23e8
-pyOpenSSL==19.1.0 \
- --hash=sha256:621880965a720b8ece2f1b2f54ea2071966ab00e2970ad2ce11d596102063504 \
- --hash=sha256:9a24494b2602aaf402be5c9e30a0b82d4a5c67528fe8fb475e3f3bc00dd69507
-pyRFC3339==1.1 \
- --hash=sha256:67196cb83b470709c580bb4738b83165e67c6cc60e1f2e4f286cfcb402a926f4 \
- --hash=sha256:81b8cbe1519cdb79bed04910dd6fa4e181faf8c88dff1e1b987b5f7ab23a5b1a
-pycparser==2.20 \
- --hash=sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0 \
- --hash=sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705
-pyparsing==2.4.7 \
- --hash=sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1 \
- --hash=sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b
-python-augeas==0.5.0 \
- --hash=sha256:67d59d66cdba8d624e0389b87b2a83a176f21f16a87553b50f5703b23f29bac2
-pytz==2020.1 \
- --hash=sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed \
- --hash=sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048
-requests==2.23.0 \
- --hash=sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee \
- --hash=sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6
-requests-toolbelt==0.9.1 \
- --hash=sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f \
- --hash=sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0
-six==1.15.0 \
- --hash=sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259 \
- --hash=sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced
-urllib3==1.25.9 \
- --hash=sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527 \
- --hash=sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115
-zope.component==4.6.1 \
- --hash=sha256:bfbe55d4a93e70a78b10edc3aad4de31bb8860919b7cbd8d66f717f7d7b279ac \
- --hash=sha256:d9c7c27673d787faff8a83797ce34d6ebcae26a370e25bddb465ac2182766aca
-zope.deferredimport==4.3.1 \
- --hash=sha256:57b2345e7b5eef47efcd4f634ff16c93e4265de3dcf325afc7315ade48d909e1 \
- --hash=sha256:9a0c211df44aa95f1c4e6d2626f90b400f56989180d3ef96032d708da3d23e0a
-zope.deprecation==4.4.0 \
- --hash=sha256:0d453338f04bacf91bbfba545d8bcdf529aa829e67b705eac8c1a7fdce66e2df \
- --hash=sha256:f1480b74995958b24ce37b0ef04d3663d2683e5d6debc96726eff18acf4ea113
-zope.event==4.4 \
- --hash=sha256:69c27debad9bdacd9ce9b735dad382142281ac770c4a432b533d6d65c4614bcf \
- --hash=sha256:d8e97d165fd5a0997b45f5303ae11ea3338becfe68c401dd88ffd2113fe5cae7
-zope.hookable==5.0.1 \
- --hash=sha256:0194b9b9e7f614abba60c90b231908861036578297515d3d6508eb10190f266d \
- --hash=sha256:0c2977473918bdefc6fa8dfb311f154e7f13c6133957fe649704deca79b92093 \
- --hash=sha256:17b8bdb3b77e03a152ca0d5ca185a7ae0156f5e5a2dbddf538676633a1f7380f \
- --hash=sha256:29d07681a78042cdd15b268ae9decffed9ace68a53eebeb61d65ae931d158841 \
- --hash=sha256:36fb1b35d1150267cb0543a1ddd950c0bc2c75ed0e6e92e3aaa6ac2e29416cb7 \
- --hash=sha256:3aed60c2bb5e812bbf9295c70f25b17ac37c233f30447a96c67913ba5073642f \
- --hash=sha256:3cac1565cc768911e72ca9ec4ddf5c5109e1fef0104f19f06649cf1874943b60 \
- --hash=sha256:3d4bc0cc4a37c3cd3081063142eeb2125511db3c13f6dc932d899c512690378e \
- --hash=sha256:3f73096f27b8c28be53ffb6604f7b570fbbb82f273c6febe5f58119009b59898 \
- --hash=sha256:522d1153d93f2d48aa0bd9fb778d8d4500be2e4dcf86c3150768f0e3adbbc4ef \
- --hash=sha256:523d2928fb7377bbdbc9af9c0b14ad73e6eaf226349f105733bdae27efd15b5a \
- --hash=sha256:5848309d4fc5c02150a45e8f8d2227e5bfda386a508bbd3160fed7c633c5a2fa \
- --hash=sha256:6781f86e6d54a110980a76e761eb54590630fd2af2a17d7edf02a079d2646c1d \
- --hash=sha256:6fd27921ebf3aaa945fa25d790f1f2046204f24dba4946f82f5f0a442577c3e9 \
- --hash=sha256:70d581862863f6bf9e175e85c9d70c2d7155f53fb04dcdb2f73cf288ca559a53 \
- --hash=sha256:81867c23b0dc66c8366f351d00923f2bc5902820a24c2534dfd7bf01a5879963 \
- --hash=sha256:81db29edadcbb740cd2716c95a297893a546ed89db1bfe9110168732d7f0afdd \
- --hash=sha256:86bd12624068cea60860a0759af5e2c3adc89c12aef6f71cf12f577e28deefe3 \
- --hash=sha256:9c184d8f9f7a76e1ced99855ccf390ffdd0ec3765e5cbf7b9cada600accc0a1e \
- --hash=sha256:acc789e8c29c13555e43fe4bf9fcd15a65512c9645e97bbaa5602e3201252b02 \
- --hash=sha256:afaa740206b7660d4cc3b8f120426c85761f51379af7a5b05451f624ad12b0af \
- --hash=sha256:b5f5fa323f878bb16eae68ea1ba7f6c0419d4695d0248bed4b18f51d7ce5ab85 \
- --hash=sha256:bd89e0e2c67bf4ac3aca2a19702b1a37269fb1923827f68324ac2e7afd6e3406 \
- --hash=sha256:c212de743283ec0735db24ec6ad913758df3af1b7217550ff270038062afd6ae \
- --hash=sha256:ca553f524293a0bdea05e7f44c3e685e4b7b022cb37d87bc4a3efa0f86587a8d \
- --hash=sha256:cab67065a3db92f636128d3157cc5424a145f82d96fb47159c539132833a6d36 \
- --hash=sha256:d3b3b3eedfdbf6b02898216e85aa6baf50207f4378a2a6803d6d47650cd37031 \
- --hash=sha256:d9f4a5a72f40256b686d31c5c0b1fde503172307beb12c1568296e76118e402c \
- --hash=sha256:df5067d87aaa111ed5d050e1ee853ba284969497f91806efd42425f5348f1c06 \
- --hash=sha256:e2587644812c6138f05b8a41594a8337c6790e3baf9a01915e52438c13fc6bef \
- --hash=sha256:e27fd877662db94f897f3fd532ef211ca4901eb1a70ba456f15c0866a985464a \
- --hash=sha256:e427ebbdd223c72e06ba94c004bb04e996c84dec8a0fa84e837556ae145c439e \
- --hash=sha256:e583ad4309c203ef75a09d43434cf9c2b4fa247997ecb0dcad769982c39411c7 \
- --hash=sha256:e760b2bc8ece9200804f0c2b64d10147ecaf18455a2a90827fbec4c9d84f3ad5 \
- --hash=sha256:ea9a9cc8bcc70e18023f30fa2f53d11ae069572a162791224e60cd65df55fb69 \
- --hash=sha256:ecb3f17dce4803c1099bd21742cd126b59817a4e76a6544d31d2cca6e30dbffd \
- --hash=sha256:ed794e3b3de42486d30444fb60b5561e724ee8a2d1b17b0c2e0f81e3ddaf7a87 \
- --hash=sha256:ee885d347279e38226d0a437b6a932f207f691c502ee565aba27a7022f1285df \
- --hash=sha256:fd5e7bc5f24f7e3d490698f7b854659a9851da2187414617cd5ed360af7efd63 \
- --hash=sha256:fe45f6870f7588ac7b2763ff1ce98cce59369717afe70cc353ec5218bc854bcc
-zope.interface==5.1.0 \
- --hash=sha256:0103cba5ed09f27d2e3de7e48bb320338592e2fabc5ce1432cf33808eb2dfd8b \
- --hash=sha256:14415d6979356629f1c386c8c4249b4d0082f2ea7f75871ebad2e29584bd16c5 \
- --hash=sha256:1ae4693ccee94c6e0c88a4568fb3b34af8871c60f5ba30cf9f94977ed0e53ddd \
- --hash=sha256:1b87ed2dc05cb835138f6a6e3595593fea3564d712cb2eb2de963a41fd35758c \
- --hash=sha256:269b27f60bcf45438e8683269f8ecd1235fa13e5411de93dae3b9ee4fe7f7bc7 \
- --hash=sha256:27d287e61639d692563d9dab76bafe071fbeb26818dd6a32a0022f3f7ca884b5 \
- --hash=sha256:39106649c3082972106f930766ae23d1464a73b7d30b3698c986f74bf1256a34 \
- --hash=sha256:40e4c42bd27ed3c11b2c983fecfb03356fae1209de10686d03c02c8696a1d90e \
- --hash=sha256:461d4339b3b8f3335d7e2c90ce335eb275488c587b61aca4b305196dde2ff086 \
- --hash=sha256:4f98f70328bc788c86a6a1a8a14b0ea979f81ae6015dd6c72978f1feff70ecda \
- --hash=sha256:558a20a0845d1a5dc6ff87cd0f63d7dac982d7c3be05d2ffb6322a87c17fa286 \
- --hash=sha256:562dccd37acec149458c1791da459f130c6cf8902c94c93b8d47c6337b9fb826 \
- --hash=sha256:5e86c66a6dea8ab6152e83b0facc856dc4d435fe0f872f01d66ce0a2131b7f1d \
- --hash=sha256:60a207efcd8c11d6bbeb7862e33418fba4e4ad79846d88d160d7231fcb42a5ee \
- --hash=sha256:645a7092b77fdbc3f68d3cc98f9d3e71510e419f54019d6e282328c0dd140dcd \
- --hash=sha256:6874367586c020705a44eecdad5d6b587c64b892e34305bb6ed87c9bbe22a5e9 \
- --hash=sha256:74bf0a4f9091131de09286f9a605db449840e313753949fe07c8d0fe7659ad1e \
- --hash=sha256:7b726194f938791a6691c7592c8b9e805fc6d1b9632a833b9c0640828cd49cbc \
- --hash=sha256:8149ded7f90154fdc1a40e0c8975df58041a6f693b8f7edcd9348484e9dc17fe \
- --hash=sha256:8cccf7057c7d19064a9e27660f5aec4e5c4001ffcf653a47531bde19b5aa2a8a \
- --hash=sha256:911714b08b63d155f9c948da2b5534b223a1a4fc50bb67139ab68b277c938578 \
- --hash=sha256:a5f8f85986197d1dd6444763c4a15c991bfed86d835a1f6f7d476f7198d5f56a \
- --hash=sha256:a744132d0abaa854d1aad50ba9bc64e79c6f835b3e92521db4235a1991176813 \
- --hash=sha256:af2c14efc0bb0e91af63d00080ccc067866fb8cbbaca2b0438ab4105f5e0f08d \
- --hash=sha256:b054eb0a8aa712c8e9030065a59b5e6a5cf0746ecdb5f087cca5ec7685690c19 \
- --hash=sha256:b0becb75418f8a130e9d465e718316cd17c7a8acce6fe8fe07adc72762bee425 \
- --hash=sha256:b1d2ed1cbda2ae107283befd9284e650d840f8f7568cb9060b5466d25dc48975 \
- --hash=sha256:ba4261c8ad00b49d48bbb3b5af388bb7576edfc0ca50a49c11dcb77caa1d897e \
- --hash=sha256:d1fe9d7d09bb07228650903d6a9dc48ea649e3b8c69b1d263419cc722b3938e8 \
- --hash=sha256:d7804f6a71fc2dda888ef2de266727ec2f3915373d5a785ed4ddc603bbc91e08 \
- --hash=sha256:da2844fba024dd58eaa712561da47dcd1e7ad544a257482392472eae1c86d5e5 \
- --hash=sha256:dcefc97d1daf8d55199420e9162ab584ed0893a109f45e438b9794ced44c9fd0 \
- --hash=sha256:dd98c436a1fc56f48c70882cc243df89ad036210d871c7427dc164b31500dc11 \
- --hash=sha256:e74671e43ed4569fbd7989e5eecc7d06dc134b571872ab1d5a88f4a123814e9f \
- --hash=sha256:eb9b92f456ff3ec746cd4935b73c1117538d6124b8617bc0fe6fda0b3816e345 \
- --hash=sha256:ebb4e637a1fb861c34e48a00d03cffa9234f42bef923aec44e5625ffb9a8e8f9 \
- --hash=sha256:ef739fe89e7f43fb6494a43b1878a36273e5924869ba1d866f752c5812ae8d58 \
- --hash=sha256:f40db0e02a8157d2b90857c24d89b6310f9b6c3642369852cdc3b5ac49b92afc \
- --hash=sha256:f68bf937f113b88c866d090fea0bc52a098695173fc613b055a17ff0cf9683b6 \
- --hash=sha256:fb55c182a3f7b84c1a2d6de5fa7b1a05d4660d866b91dbf8d74549c57a1499e8
-zope.proxy==4.3.5 \
- --hash=sha256:00573dfa755d0703ab84bb23cb6ecf97bb683c34b340d4df76651f97b0bab068 \
- --hash=sha256:092049280f2848d2ba1b57b71fe04881762a220a97b65288bcb0968bb199ec30 \
- --hash=sha256:0cbd27b4d3718b5ec74fc65ffa53c78d34c65c6fd9411b8352d2a4f855220cf1 \
- --hash=sha256:17fc7e16d0c81f833a138818a30f366696653d521febc8e892858041c4d88785 \
- --hash=sha256:19577dfeb70e8a67249ba92c8ad20589a1a2d86a8d693647fa8385408a4c17b0 \
- --hash=sha256:207aa914576b1181597a1516e1b90599dc690c095343ae281b0772e44945e6a4 \
- --hash=sha256:219a7db5ed53e523eb4a4769f13105118b6d5b04ed169a283c9775af221e231f \
- --hash=sha256:2b50ea79849e46b5f4f2b0247a3687505d32d161eeb16a75f6f7e6cd81936e43 \
- --hash=sha256:5903d38362b6c716e66bbe470f190579c530a5baf03dbc8500e5c2357aa569a5 \
- --hash=sha256:5c24903675e271bd688c6e9e7df5775ac6b168feb87dbe0e4bcc90805f21b28f \
- --hash=sha256:5ef6bc5ed98139e084f4e91100f2b098a0cd3493d4e76f9d6b3f7b95d7ad0f06 \
- --hash=sha256:61b55ae3c23a126a788b33ffb18f37d6668e79a05e756588d9e4d4be7246ab1c \
- --hash=sha256:63ddb992931a5e616c87d3d89f5a58db086e617548005c7f9059fac68c03a5cc \
- --hash=sha256:6943da9c09870490dcfd50c4909c0cc19f434fa6948f61282dc9cb07bcf08160 \
- --hash=sha256:6ad40f85c1207803d581d5d75e9ea25327cd524925699a83dfc03bf8e4ba72b7 \
- --hash=sha256:6b44433a79bdd7af0e3337bd7bbcf53dd1f9b0fa66bf21bcb756060ce32a96c1 \
- --hash=sha256:6bbaa245015d933a4172395baad7874373f162955d73612f0b66b6c2c33b6366 \
- --hash=sha256:7007227f4ea85b40a2f5e5a244479f6a6dfcf906db9b55e812a814a8f0e2c28d \
- --hash=sha256:74884a0aec1f1609190ec8b34b5d58fb3b5353cf22b96161e13e0e835f13518f \
- --hash=sha256:7d25fe5571ddb16369054f54cdd883f23de9941476d97f2b92eb6d7d83afe22d \
- --hash=sha256:7e162bdc5e3baad26b2262240be7d2bab36991d85a6a556e48b9dfb402370261 \
- --hash=sha256:814d62678dc3a30f4aa081982d830b7c342cf230ffc9d030b020cb154eeebf9e \
- --hash=sha256:8878a34c5313ee52e20aa50b03138af8d472bae465710fb954d133a9bfd3c38d \
- --hash=sha256:a66a0d94e5b081d5d695e66d6667e91e74d79e273eee95c1747717ba9cb70792 \
- --hash=sha256:a69f5cbf4addcfdf03dda564a671040127a6b7c34cf9fe4973582e68441b63fa \
- --hash=sha256:b00f9f0c334d07709d3f73a7cb8ae63c6ca1a90c790a63b5e7effa666ef96021 \
- --hash=sha256:b6ed71e4a7b4690447b626f499d978aa13197a0e592950e5d7020308f6054698 \
- --hash=sha256:bdf5041e5851526e885af579d2f455348dba68d74f14a32781933569a327fddf \
- --hash=sha256:be034360dd34e62608419f86e799c97d389c10a0e677a25f236a971b2f40dac9 \
- --hash=sha256:cc8f590a5eed30b314ae6b0232d925519ade433f663de79cc3783e4b10d662ba \
- --hash=sha256:cd7a318a15fe6cc4584bf3c4426f092ed08c0fd012cf2a9173114234fe193e11 \
- --hash=sha256:cf19b5f63a59c20306e034e691402b02055c8f4e38bf6792c23cad489162a642 \
- --hash=sha256:cfc781ce442ec407c841e9aa51d0e1024f72b6ec34caa8fdb6ef9576d549acf2 \
- --hash=sha256:dea9f6f8633571e18bc20cad83603072e697103a567f4b0738d52dd0211b4527 \
- --hash=sha256:e4a86a1d5eb2cce83c5972b3930c7c1eac81ab3508464345e2b8e54f119d5505 \
- --hash=sha256:e7106374d4a74ed9ff00c46cc00f0a9f06a0775f8868e423f85d4464d2333679 \
- --hash=sha256:e98a8a585b5668aa9e34d10f7785abf9545fe72663b4bfc16c99a115185ae6a5 \
- --hash=sha256:f64840e68483316eb58d82c376ad3585ca995e69e33b230436de0cdddf7363f9 \
- --hash=sha256:f8f4b0a9e6683e43889852130595c8854d8ae237f2324a053cdd884de936aa9b \
- --hash=sha256:fc45a53219ed30a7f670a6d8c98527af0020e6fd4ee4c0a8fb59f147f06d816c
-
-# Contains the requirements for the letsencrypt package.
-#
-# Since the letsencrypt package depends on certbot and using pip with hashes
-# requires that all installed packages have hashes listed, this allows
-# dependency-requirements.txt to be used without requiring a hash for a
-# (potentially unreleased) Certbot package.
-
-letsencrypt==0.7.0 \
- --hash=sha256:105a5fb107e45bcd0722eb89696986dcf5f08a86a321d6aef25a0c7c63375ade \
- --hash=sha256:c36e532c486a7e92155ee09da54b436a3c420813ec1c590b98f635d924720de9
-
-certbot==1.14.0 \
- --hash=sha256:67b4d26ceaea6c7f8325d0d45169e7a165a2cabc7122c84bc971ba068ca19cca \
- --hash=sha256:959ea90c6bb8dca38eab9772722cb940972ef6afcd5f15deef08b3c3636841eb
-acme==1.14.0 \
- --hash=sha256:4f48c41261202f1a389ec2986b2580b58f53e0d5a1ae2463b34318d78b87fc66 \
- --hash=sha256:61daccfb0343628cbbca551a7fc4c82482113952c21db3fe0c585b7c98fa1c35
-certbot-apache==1.14.0 \
- --hash=sha256:b757038db23db707c44630fecb46e99172bd791f0db5a8e623c0842613c4d3d9 \
- --hash=sha256:887fe4a21af2de1e5c2c9428bacba6eb7c1219257bc70f1a1d8447c8a321adb0
-certbot-nginx==1.14.0 \
- --hash=sha256:8916a815437988d6c192df9f035bb7a176eab20eee0956677b335d0698d243fb \
- --hash=sha256:cc2a8a0de56d9bb6b2efbda6c80c647dad8db2bb90675cac03ade94bd5fc8597
-
-UNLIKELY_EOF
- # -------------------------------------------------------------------------
- cat << "UNLIKELY_EOF" > "$TEMP_DIR/pipstrap.py"
-#!/usr/bin/env python
-"""A small script that can act as a trust root for installing pip >=8
-Embed this in your project, and your VCS checkout is all you have to trust. In
-a post-peep era, this lets you claw your way to a hash-checking version of pip,
-with which you can install the rest of your dependencies safely. All it assumes
-is Python 2.6 or better and *some* version of pip already installed. If
-anything goes wrong, it will exit with a non-zero status code.
-"""
-# This is here so embedded copies are MIT-compliant:
-# Copyright (c) 2016 Erik Rose
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to
-# deal in the Software without restriction, including without limitation the
-# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
-# sell copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-from __future__ import print_function
-from distutils.version import StrictVersion
-from hashlib import sha256
-from os import environ
-from os.path import join
-from shutil import rmtree
-try:
- from subprocess import check_output
-except ImportError:
- from subprocess import CalledProcessError, PIPE, Popen
-
- def check_output(*popenargs, **kwargs):
- if 'stdout' in kwargs:
- raise ValueError('stdout argument not allowed, it will be '
- 'overridden.')
- process = Popen(stdout=PIPE, *popenargs, **kwargs)
- output, unused_err = process.communicate()
- retcode = process.poll()
- if retcode:
- cmd = kwargs.get("args")
- if cmd is None:
- cmd = popenargs[0]
- raise CalledProcessError(retcode, cmd)
- return output
-import sys
-from tempfile import mkdtemp
-try:
- from urllib2 import build_opener, HTTPHandler, HTTPSHandler
-except ImportError:
- from urllib.request import build_opener, HTTPHandler, HTTPSHandler
-try:
- from urlparse import urlparse
-except ImportError:
- from urllib.parse import urlparse # 3.4
-
-
-__version__ = 1, 5, 1
-PIP_VERSION = '9.0.1'
-DEFAULT_INDEX_BASE = 'https://pypi.python.org'
-
-
-# wheel has a conditional dependency on argparse:
-maybe_argparse = (
- [('18/dd/e617cfc3f6210ae183374cd9f6a26b20514bbb5a792af97949c5aacddf0f/'
- 'argparse-1.4.0.tar.gz',
- '62b089a55be1d8949cd2bc7e0df0bddb9e028faefc8c32038cc84862aefdd6e4')]
- if sys.version_info < (2, 7, 0) else [])
-
-
-# Be careful when updating the pinned versions here, in particular for pip.
-# Indeed starting from 10.0, pip will build dependencies in isolation if the
-# related projects are compliant with PEP 517. This is not something we want
-# as of now, so the isolation build will need to be disabled wherever
-# pipstrap is used (see https://github.com/certbot/certbot/issues/8256).
-PACKAGES = maybe_argparse + [
- # Pip has no dependencies, as it vendors everything:
- ('11/b6/abcb525026a4be042b486df43905d6893fb04f05aac21c32c638e939e447/'
- 'pip-{0}.tar.gz'.format(PIP_VERSION),
- '09f243e1a7b461f654c26a725fa373211bb7ff17a9300058b205c61658ca940d'),
- # This version of setuptools has only optional dependencies:
- ('37/1b/b25507861991beeade31473868463dad0e58b1978c209de27384ae541b0b/'
- 'setuptools-40.6.3.zip',
- '3b474dad69c49f0d2d86696b68105f3a6f195f7ab655af12ef9a9c326d2b08f8'),
- ('c9/1d/bd19e691fd4cfe908c76c429fe6e4436c9e83583c4414b54f6c85471954a/'
- 'wheel-0.29.0.tar.gz',
- '1ebb8ad7e26b448e9caa4773d2357849bf80ff9e313964bcaf79cbf0201a1648')
-]
-
-
-class HashError(Exception):
- def __str__(self):
- url, path, actual, expected = self.args
- return ('{url} did not match the expected hash {expected}. Instead, '
- 'it was {actual}. The file (left at {path}) may have been '
- 'tampered with.'.format(**locals()))
-
-
-def hashed_download(url, temp, digest):
- """Download ``url`` to ``temp``, make sure it has the SHA-256 ``digest``,
- and return its path."""
- # Based on pip 1.4.1's URLOpener but with cert verification removed. Python
- # >=2.7.9 verifies HTTPS certs itself, and, in any case, the cert
- # authenticity has only privacy (not arbitrary code execution)
- # implications, since we're checking hashes.
- def opener(using_https=True):
- opener = build_opener(HTTPSHandler())
- if using_https:
- # Strip out HTTPHandler to prevent MITM spoof:
- for handler in opener.handlers:
- if isinstance(handler, HTTPHandler):
- opener.handlers.remove(handler)
- return opener
-
- def read_chunks(response, chunk_size):
- while True:
- chunk = response.read(chunk_size)
- if not chunk:
- break
- yield chunk
-
- parsed_url = urlparse(url)
- response = opener(using_https=parsed_url.scheme == 'https').open(url)
- path = join(temp, parsed_url.path.split('/')[-1])
- actual_hash = sha256()
- with open(path, 'wb') as file:
- for chunk in read_chunks(response, 4096):
- file.write(chunk)
- actual_hash.update(chunk)
-
- actual_digest = actual_hash.hexdigest()
- if actual_digest != digest:
- raise HashError(url, path, actual_digest, digest)
- return path
-
-
-def get_index_base():
- """Return the URL to the dir containing the "packages" folder.
- Try to wring something out of PIP_INDEX_URL, if set. Hack "/simple" off the
- end if it's there; that is likely to give us the right dir.
- """
- env_var = environ.get('PIP_INDEX_URL', '').rstrip('/')
- if env_var:
- SIMPLE = '/simple'
- if env_var.endswith(SIMPLE):
- return env_var[:-len(SIMPLE)]
- else:
- return env_var
- else:
- return DEFAULT_INDEX_BASE
-
-
-def main():
- python = sys.executable or 'python'
- pip_version = StrictVersion(check_output([python, '-m', 'pip', '--version'])
- .decode('utf-8').split()[1])
- has_pip_cache = pip_version >= StrictVersion('6.0')
- index_base = get_index_base()
- temp = mkdtemp(prefix='pipstrap-')
- try:
- downloads = [hashed_download(index_base + '/packages/' + path,
- temp,
- digest)
- for path, digest in PACKAGES]
- # Calling pip as a module is the preferred way to avoid problems about pip self-upgrade.
- command = [python, '-m', 'pip', 'install', '--no-index', '--no-deps', '-U']
- # Disable cache since it is not used and it otherwise sometimes throws permission warnings:
- command.extend(['--no-cache-dir'] if has_pip_cache else [])
- command.extend(downloads)
- check_output(command)
- except HashError as exc:
- print(exc)
- except Exception:
- rmtree(temp)
- raise
- else:
- rmtree(temp)
- return 0
- return 1
-
-
-if __name__ == '__main__':
- sys.exit(main())
-
-UNLIKELY_EOF
- # -------------------------------------------------------------------------
- # Set PATH so pipstrap upgrades the right (v)env:
- PATH="$VENV_BIN:$PATH" "$VENV_BIN/python" "$TEMP_DIR/pipstrap.py"
- set +e
- if [ "$VERBOSE" = 1 ]; then
- "$VENV_BIN/pip" install --disable-pip-version-check --no-cache-dir --require-hashes -r "$TEMP_DIR/letsencrypt-auto-requirements.txt"
- else
- PIP_OUT=`"$VENV_BIN/pip" install --disable-pip-version-check --no-cache-dir --require-hashes -r "$TEMP_DIR/letsencrypt-auto-requirements.txt" 2>&1`
- fi
- PIP_STATUS=$?
- set -e
- if [ "$PIP_STATUS" != 0 ]; then
- # Report error. (Otherwise, be quiet.)
- error "Had a problem while installing Python packages."
- if [ "$VERBOSE" != 1 ]; then
- error
- error "pip prints the following errors: "
- error "====================================================="
- error "$PIP_OUT"
- error "====================================================="
- error
- error "Certbot has problem setting up the virtual environment."
-
- if `echo $PIP_OUT | grep -q Killed` || `echo $PIP_OUT | grep -q "allocate memory"` ; then
- error
- error "Based on your pip output, the problem can likely be fixed by "
- error "increasing the available memory."
- else
- error
- error "We were not be able to guess the right solution from your pip "
- error "output."
- fi
-
- error
- error "Consult https://certbot.eff.org/docs/install.html#problems-with-python-virtual-environment"
- error "for possible solutions."
- error "You may also find some support resources at https://certbot.eff.org/support/ ."
- fi
- rm -rf "$VENV_PATH"
- exit 1
- fi
-
- if [ -d "$OLD_VENV_PATH" -a ! -L "$OLD_VENV_PATH" ]; then
- rm -rf "$OLD_VENV_PATH"
- ln -s "$VENV_PATH" "$OLD_VENV_PATH"
- fi
-
- say "Installation succeeded."
- fi
-
- # If you're modifying any of the code after this point in this current `if` block, you
- # may need to update the "$DEPRECATED_OS" = 1 case at the beginning of phase 2 as well.
-
- if [ "$INSTALL_ONLY" = 1 ]; then
- say "Certbot is installed."
- exit 0
- fi
-
- "$VENV_BIN/letsencrypt" "$@"
-
-else
- # Phase 1: Upgrade certbot-auto if necessary, then self-invoke.
- #
- # Each phase checks the version of only the thing it is responsible for
- # upgrading. Phase 1 checks the version of the latest release of
- # certbot-auto (which is always the same as that of the certbot
- # package). Phase 2 checks the version of the locally installed certbot.
- export PHASE_1_VERSION="$LE_AUTO_VERSION"
-
- if [ ! -f "$VENV_BIN/letsencrypt" ]; then
- if ! OldVenvExists; then
- if [ "$HELP" = 1 ]; then
- echo "$USAGE"
- exit 0
- fi
- # If it looks like we've never bootstrapped before, bootstrap:
- Bootstrap
- fi
- fi
- if [ "$OS_PACKAGES_ONLY" = 1 ]; then
- say "OS packages installed."
- exit 0
- fi
-
- DeterminePythonVersion "NOCRASH"
- # Don't warn about file permissions if the user disabled the check or we
- # can't find an up-to-date Python.
- if [ "$PYVER" -ge "$MIN_PYVER" -a "$NO_PERMISSIONS_CHECK" != 1 ]; then
- # If the script fails for some reason, don't break certbot-auto.
- set +e
- # Suppress unexpected error output.
- CHECK_PERM_OUT=$(CheckPathPermissions "$LE_PYTHON" "$0" 2>/dev/null)
- CHECK_PERM_STATUS="$?"
- set -e
- # Only print output if the script ran successfully and it actually produced
- # output. The latter check resolves
- # https://github.com/certbot/certbot/issues/7012.
- if [ "$CHECK_PERM_STATUS" = 0 -a -n "$CHECK_PERM_OUT" ]; then
- error "$CHECK_PERM_OUT"
- fi
- fi
-
- if [ "$NO_SELF_UPGRADE" != 1 ]; then
- TEMP_DIR=$(TempDir)
- trap 'rm -rf "$TEMP_DIR"' EXIT
- # ---------------------------------------------------------------------------
- cat << "UNLIKELY_EOF" > "$TEMP_DIR/fetch.py"
-"""Do downloading and JSON parsing without additional dependencies. ::
-
- # Print latest released version of LE to stdout:
- python fetch.py --latest-version
-
- # Download letsencrypt-auto script from git tag v1.2.3 into the folder I'm
- # in, and make sure its signature verifies:
- python fetch.py --le-auto-script v1.2.3
-
-On failure, return non-zero.
-
-"""
-
-from __future__ import print_function, unicode_literals
-
-from distutils.version import LooseVersion
-from json import loads
-from os import devnull, environ
-from os.path import dirname, join
-import re
-import ssl
-from subprocess import check_call, CalledProcessError
-from sys import argv, exit
-try:
- from urllib2 import build_opener, HTTPHandler, HTTPSHandler
- from urllib2 import HTTPError, URLError
-except ImportError:
- from urllib.request import build_opener, HTTPHandler, HTTPSHandler
- from urllib.error import HTTPError, URLError
-
-PUBLIC_KEY = environ.get('LE_AUTO_PUBLIC_KEY', """-----BEGIN PUBLIC KEY-----
-MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6MR8W/galdxnpGqBsYbq
-OzQb2eyW15YFjDDEMI0ZOzt8f504obNs920lDnpPD2/KqgsfjOgw2K7xWDJIj/18
-xUvWPk3LDkrnokNiRkA3KOx3W6fHycKL+zID7zy+xZYBuh2fLyQtWV1VGQ45iNRp
-9+Zo7rH86cdfgkdnWTlNSHyTLW9NbXvyv/E12bppPcEvgCTAQXgnDVJ0/sqmeiij
-n9tTFh03aM+R2V/21h8aTraAS24qiPCz6gkmYGC8yr6mglcnNoYbsLNYZ69zF1XH
-cXPduCPdPdfLlzVlKK1/U7hkA28eG3BIAMh6uJYBRJTpiGgaGdPd7YekUB8S6cy+
-CQIDAQAB
------END PUBLIC KEY-----
-""")
-
-class ExpectedError(Exception):
- """A novice-readable exception that also carries the original exception for
- debugging"""
-
-
-class HttpsGetter(object):
- def __init__(self):
- """Build an HTTPS opener."""
- # Based on pip 1.4.1's URLOpener
- # This verifies certs on only Python >=2.7.9, and when NO_CERT_VERIFY isn't set.
- if environ.get('NO_CERT_VERIFY') == '1' and hasattr(ssl, 'SSLContext'):
- self._opener = build_opener(HTTPSHandler(context=cert_none_context()))
- else:
- self._opener = build_opener(HTTPSHandler())
- # Strip out HTTPHandler to prevent MITM spoof:
- for handler in self._opener.handlers:
- if isinstance(handler, HTTPHandler):
- self._opener.handlers.remove(handler)
-
- def get(self, url):
- """Return the document contents pointed to by an HTTPS URL.
-
- If something goes wrong (404, timeout, etc.), raise ExpectedError.
-
- """
- try:
- # socket module docs say default timeout is None: that is, no
- # timeout
- return self._opener.open(url, timeout=30).read()
- except (HTTPError, IOError) as exc:
- raise ExpectedError("Couldn't download %s." % url, exc)
-
-
-def write(contents, dir, filename):
- """Write something to a file in a certain directory."""
- with open(join(dir, filename), 'wb') as file:
- file.write(contents)
-
-
-def latest_stable_version(get):
- """Return the latest stable release of letsencrypt."""
- metadata = loads(get(
- environ.get('LE_AUTO_JSON_URL',
- 'https://pypi.python.org/pypi/certbot/json')).decode('UTF-8'))
- # metadata['info']['version'] actually returns the latest of any kind of
- # release release, contrary to https://wiki.python.org/moin/PyPIJSON.
- # The regex is a sufficient regex for picking out prereleases for most
- # packages, LE included.
- return str(max(LooseVersion(r) for r
- in metadata['releases'].keys()
- if re.match('^[0-9.]+$', r)))
-
-
-def verified_new_le_auto(get, tag, temp_dir):
- """Return the path to a verified, up-to-date letsencrypt-auto script.
-
- If the download's signature does not verify or something else goes wrong
- with the verification process, raise ExpectedError.
-
- """
- le_auto_dir = environ.get(
- 'LE_AUTO_DIR_TEMPLATE',
- 'https://raw.githubusercontent.com/certbot/certbot/%s/'
- 'letsencrypt-auto-source/') % tag
- write(get(le_auto_dir + 'letsencrypt-auto'), temp_dir, 'letsencrypt-auto')
- write(get(le_auto_dir + 'letsencrypt-auto.sig'), temp_dir, 'letsencrypt-auto.sig')
- write(PUBLIC_KEY.encode('UTF-8'), temp_dir, 'public_key.pem')
- try:
- with open(devnull, 'w') as dev_null:
- check_call(['openssl', 'dgst', '-sha256', '-verify',
- join(temp_dir, 'public_key.pem'),
- '-signature',
- join(temp_dir, 'letsencrypt-auto.sig'),
- join(temp_dir, 'letsencrypt-auto')],
- stdout=dev_null,
- stderr=dev_null)
- except CalledProcessError as exc:
- raise ExpectedError("Couldn't verify signature of downloaded "
- "certbot-auto.", exc)
-
-
-def cert_none_context():
- """Create a SSLContext object to not check hostname."""
- # PROTOCOL_TLS isn't available before 2.7.13 but this code is for 2.7.9+, so use this.
- context = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
- context.verify_mode = ssl.CERT_NONE
- return context
-
-
-def main():
- get = HttpsGetter().get
- flag = argv[1]
- try:
- if flag == '--latest-version':
- print(latest_stable_version(get))
- elif flag == '--le-auto-script':
- tag = argv[2]
- verified_new_le_auto(get, tag, dirname(argv[0]))
- except ExpectedError as exc:
- print(exc.args[0], exc.args[1])
- return 1
- else:
- return 0
-
-
-if __name__ == '__main__':
- exit(main())
-
-UNLIKELY_EOF
- # ---------------------------------------------------------------------------
- if [ "$PYVER" -lt "$MIN_PYVER" ]; then
- error "WARNING: couldn't find Python $MIN_PYTHON_VERSION+ to check for updates."
- elif ! REMOTE_VERSION=`"$LE_PYTHON" "$TEMP_DIR/fetch.py" --latest-version` ; then
- error "WARNING: unable to check for updates."
- fi
-
- # If for any reason REMOTE_VERSION is not set, let's assume certbot-auto is up-to-date,
- # and do not go into the self-upgrading process.
- if [ -n "$REMOTE_VERSION" ]; then
- LE_VERSION_STATE=`CompareVersions "$LE_PYTHON" "$LE_AUTO_VERSION" "$REMOTE_VERSION"`
-
- if [ "$LE_VERSION_STATE" = "UNOFFICIAL" ]; then
- say "Unofficial certbot-auto version detected, self-upgrade is disabled: $LE_AUTO_VERSION"
- elif [ "$LE_VERSION_STATE" = "OUTDATED" ]; then
- say "Upgrading certbot-auto $LE_AUTO_VERSION to $REMOTE_VERSION..."
-
- # Now we drop into Python so we don't have to install even more
- # dependencies (curl, etc.), for better flow control, and for the option of
- # future Windows compatibility.
- "$LE_PYTHON" "$TEMP_DIR/fetch.py" --le-auto-script "v$REMOTE_VERSION"
-
- # Install new copy of certbot-auto.
- # TODO: Deal with quotes in pathnames.
- say "Replacing certbot-auto..."
- # Clone permissions with cp. chmod and chown don't have a --reference
- # option on macOS or BSD, and stat -c on Linux is stat -f on macOS and BSD:
- cp -p "$0" "$TEMP_DIR/letsencrypt-auto.permission-clone"
- cp "$TEMP_DIR/letsencrypt-auto" "$TEMP_DIR/letsencrypt-auto.permission-clone"
- # Using mv rather than cp leaves the old file descriptor pointing to the
- # original copy so the shell can continue to read it unmolested. mv across
- # filesystems is non-atomic, doing `rm dest, cp src dest, rm src`, but the
- # cp is unlikely to fail if the rm doesn't.
- mv -f "$TEMP_DIR/letsencrypt-auto.permission-clone" "$0"
- fi # A newer version is available.
- fi
- fi # Self-upgrading is allowed.
-
- RerunWithArgs --le-auto-phase2 "$@"
-fi
diff --git a/letsencrypt-auto-source/certbot-auto.asc b/letsencrypt-auto-source/certbot-auto.asc
deleted file mode 100644
index c0cf63418a8..00000000000
--- a/letsencrypt-auto-source/certbot-auto.asc
+++ /dev/null
@@ -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-----
diff --git a/letstest/scripts/test_apache2.sh b/letstest/scripts/test_apache2.sh
index 830ae44b221..5c30bdc3548 100755
--- a/letstest/scripts/test_apache2.sh
+++ b/letstest/scripts/test_apache2.sh
@@ -45,7 +45,7 @@ if [ $? -ne 0 ] ; then
exit 1
fi
-tools/venv.py -e acme[dev] -e certbot[dev,docs] -e certbot-apache -e certbot-ci
+tools/venv.py -e acme -e certbot -e certbot-apache -e certbot-ci tox
PEBBLE_LOGS="acme_server.log"
PEBBLE_URL="https://localhost:14000/dir"
# We configure Pebble to use port 80 for http-01 validation rather than an
diff --git a/tests/modification-check.py b/tests/modification-check.py
index 8f3ae126461..c1530d1f981 100755
--- a/tests/modification-check.py
+++ b/tests/modification-check.py
@@ -10,11 +10,6 @@
# 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 @@
# 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',
}
diff --git a/tools/dev_constraints.txt b/tools/dev_constraints.txt
index 70aed2ab332..50b52222d63 100644
--- a/tools/dev_constraints.txt
+++ b/tools/dev_constraints.txt
@@ -71,6 +71,7 @@ parso==0.7.0
pathlib2==2.3.5
pexpect==4.7.0
pickleshare==0.7.5
+pip==20.2.4
pkginfo==1.4.2
pluggy==0.13.0
ply==3.4
@@ -125,5 +126,6 @@ uritemplate==3.0.0
virtualenv==16.6.2
wcwidth==0.1.8
websocket-client==0.56.0
+wheel==0.35.1
wrapt==1.11.2
zipp==0.6.0
diff --git a/tools/pinning/pin.sh b/tools/pinning/pin.sh
index 91d20dc7c07..1c8111e1b52 100755
--- a/tools/pinning/pin.sh
+++ b/tools/pinning/pin.sh
@@ -6,13 +6,11 @@ set -euo pipefail
WORK_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )"
REPO_ROOT="$(dirname "$(dirname "${WORK_DIR}")")"
-PIPSTRAP_CONSTRAINTS="${REPO_ROOT}/tools/pipstrap_constraints.txt"
RELATIVE_SCRIPT_PATH="$(realpath --relative-to "$REPO_ROOT" "$WORK_DIR")/$(basename "${BASH_SOURCE[0]}")"
REQUIREMENTS_FILE="$REPO_ROOT/tools/requirements.txt"
-STRIP_HASHES="${REPO_ROOT}/tools/strip_hashes.py"
-if ! command -v poetry >/dev/null; then
- echo "Please install poetry."
+if ! command -v poetry >/dev/null || [ $(poetry --version | grep -oE '[0-9]+\.[0-9]+' | sed 's/\.//') -lt 12 ]; then
+ echo "Please install poetry 1.2+."
echo "You may need to recreate Certbot's virtual environment and activate it."
exit 1
fi
@@ -37,13 +35,6 @@ trap 'rm poetry.lock; rm $TEMP_REQUIREMENTS' EXIT
poetry export -o "${TEMP_REQUIREMENTS}" --without-hashes
# We need to remove local packages from the requirements file.
sed -i '/^acme @/d; /certbot/d;' "${TEMP_REQUIREMENTS}"
-# Poetry currently will not include pip, setuptools, or wheel in lockfiles or
-# requirements files. This was resolved by
-# https://github.com/python-poetry/poetry/pull/2826, but as of writing this it
-# hasn't been included in a release yet. For now, we continue to keep
-# pipstrap's pinning separate which has the added benefit of having it continue
-# to check hashes when pipstrap is run directly.
-"${STRIP_HASHES}" "${PIPSTRAP_CONSTRAINTS}" >> "${TEMP_REQUIREMENTS}"
cat << EOF > "$REQUIREMENTS_FILE"
# This file was generated by $RELATIVE_SCRIPT_PATH and can be updated using
diff --git a/tools/pinning/pyproject.toml b/tools/pinning/pyproject.toml
index b7257bae2a9..580922c4e3b 100644
--- a/tools/pinning/pyproject.toml
+++ b/tools/pinning/pyproject.toml
@@ -12,8 +12,8 @@ python = "^3.6"
# Any local packages that have dependencies on other local packages must be
# listed below before the package it depends on. For instance, certbot depends
# on acme so certbot must be listed before acme.
-certbot-ci = {path = "../../certbot-ci", extras = ["docs"]}
-certbot-compatibility-test = {path = "../../certbot-compatibility-test", extras = ["docs"]}
+certbot-ci = {path = "../../certbot-ci"}
+certbot-compatibility-test = {path = "../../certbot-compatibility-test"}
certbot-dns-cloudflare = {path = "../../certbot-dns-cloudflare", extras = ["docs"]}
certbot-dns-cloudxns = {path = "../../certbot-dns-cloudxns", extras = ["docs"]}
certbot-dns-digitalocean = {path = "../../certbot-dns-digitalocean", extras = ["docs"]}
@@ -28,10 +28,10 @@ certbot-dns-ovh = {path = "../../certbot-dns-ovh", extras = ["docs"]}
certbot-dns-rfc2136 = {path = "../../certbot-dns-rfc2136", extras = ["docs"]}
certbot-dns-route53 = {path = "../../certbot-dns-route53", extras = ["docs"]}
certbot-dns-sakuracloud = {path = "../../certbot-dns-sakuracloud", extras = ["docs"]}
-certbot-nginx = {path = "../../certbot-nginx", extras = ["docs"]}
+certbot-nginx = {path = "../../certbot-nginx"}
certbot-apache = {path = "../../certbot-apache", extras = ["dev"]}
-certbot = {path = "../../certbot", extras = ["dev", "docs"]}
-acme = {path = "../../acme", extras = ["dev", "docs"]}
+certbot = {path = "../../certbot", extras = ["all"]}
+acme = {path = "../../acme", extras = ["docs", "test"]}
letstest = {path = "../../letstest"}
windows-installer = {path = "../../windows-installer"}
@@ -51,6 +51,12 @@ awscli = ">=1.19.62"
# as a dependency here to ensure a version of cython is pinned for extra
# stability.
cython = "*"
+# mypy 0.900 stopped including stubs containing type information for 3rd party
+# libraries by default. This breaks our tests so let's continue to pin it back
+# for now. See
+# https://mypy-lang.blogspot.com/2021/05/the-upcoming-switch-to-modular-typeshed.html
+# for more info.
+mypy = "<0.900"
# We install mock in our "external-mock" tox environment to test that we didn't
# break Certbot's test API which used to always use mock objects from the 3rd
# party mock library. We list the mock dependency here so that is pinned, but
@@ -58,6 +64,15 @@ cython = "*"
# needed. This dependency can be removed here once Certbot's support for the
# 3rd party mock library has been dropped.
mock = "*"
+# pip's new dependency resolver fails on local packages that depend on each
+# other when those packages are requested with extras such as 'certbot[dev]' so
+# let's pin it back for now. See https://github.com/pypa/pip/issues/9204.
+pip = "20.2.4"
+# poetry 1.2.0+ is required for it to pin pip, setuptools, and wheel. See
+# https://github.com/python-poetry/poetry/issues/1584. This version is required
+# here in addition to certbot/setup.py because otherwise the pre-release
+# version of poetry will not be installed.
+poetry = ">=1.2.0a1"
# We were originally pinning back python-augeas for certbot-auto because we
# found the way older versions of the library linked to Augeas were more
# reliable. That's no longer a concern, however, we continue to pin back the
diff --git a/tools/pip_install.py b/tools/pip_install.py
index 047b93f0895..aa1d06a801e 100755
--- a/tools/pip_install.py
+++ b/tools/pip_install.py
@@ -16,7 +16,6 @@
import merge_requirements as merge_module
import readlink
-import strip_hashes
# Once this code doesn't need to support Python 2, we can simply use
diff --git a/tools/pipstrap.py b/tools/pipstrap.py
index 2b2e3dcbbe9..d2dbfaba95f 100755
--- a/tools/pipstrap.py
+++ b/tools/pipstrap.py
@@ -1,15 +1,10 @@
#!/usr/bin/env python
"""Uses pip to upgrade Python packaging tools to pinned versions."""
-import os
-
import pip_install
-_REQUIREMENTS_PATH = os.path.join(os.path.dirname(__file__), "pipstrap_constraints.txt")
-
def main():
- pip_install_args = '--requirement "{0}"'.format(_REQUIREMENTS_PATH)
- pip_install.pip_install_with_print(pip_install_args)
+ pip_install.main('pip setuptools wheel'.split())
if __name__ == '__main__':
diff --git a/tools/pipstrap_constraints.txt b/tools/pipstrap_constraints.txt
deleted file mode 100644
index 54ab8b4296e..00000000000
--- a/tools/pipstrap_constraints.txt
+++ /dev/null
@@ -1,18 +0,0 @@
-# Constraints for pipstrap.py
-#
-# We include the hashes of the packages here for extra verification of
-# the packages downloaded from PyPI. This is especially valuable in our
-# builds of Certbot that we ship to our users such as our Docker images.
-#
-# An older version of setuptools is currently used here in order to keep
-# compatibility with Python 2 since newer versions of setuptools have dropped
-# support for it.
-pip==20.2.4 \
- --hash=sha256:51f1c7514530bd5c145d8f13ed936ad6b8bfcb8cf74e10403d0890bc986f0033 \
- --hash=sha256:85c99a857ea0fb0aedf23833d9be5c40cf253fe24443f0829c7b472e23c364a1
-setuptools==54.1.2 \
- --hash=sha256:dd20743f36b93cbb8724f4d2ccd970dce8b6e6e823a13aa7e5751bb4e674c20b \
- --hash=sha256:ebd0148faf627b569c8d2a1b20f5d3b09c873f12739d71c7ee88f037d5be82ff
-wheel==0.35.1 \
- --hash=sha256:497add53525d16c173c2c1c733b8f655510e909ea78cc0e29d374243544b77a2 \
- --hash=sha256:99a22d87add3f634ff917310a3d87e499f19e663413a52eb9232c447aa646c9f
diff --git a/tools/requirements.txt b/tools/requirements.txt
index df9c09bfcda..8370ea0a6bc 100644
--- a/tools/requirements.txt
+++ b/tools/requirements.txt
@@ -7,149 +7,150 @@
# for more info.
alabaster==0.7.12; python_version >= "3.6"
apacheconfig==0.3.2; python_version >= "3.6"
-apipkg==1.5; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6"
-appdirs==1.4.4; python_version >= "3.6" and python_full_version < "3.0.0" or python_version >= "3.6" and python_full_version >= "3.5.0"
-appnope==0.1.2
-astroid==2.5.6; python_version >= "3.6" and python_version < "4.0"
+appdirs==1.4.4; python_version >= "3.6" and python_full_version < "3.0.0" and python_version < "4.0" or python_version >= "3.6" and python_version < "4.0" and python_full_version >= "3.4.0"
+appnope==0.1.2; python_version == "3.6" and sys_platform == "darwin" or python_version >= "3.7" and sys_platform == "darwin"
+astroid==2.6.2; python_version >= "3.6" and python_version < "4.0"
atomicwrites==1.4.0; python_version >= "3.6" and python_full_version < "3.0.0" and sys_platform == "win32" or sys_platform == "win32" and python_version >= "3.6" and python_full_version >= "3.4.0"
attrs==21.2.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6"
-awscli==1.19.83; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.6.0")
+awscli==1.19.109; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.6.0")
azure-devops==6.0.0b4; python_version >= "3.6"
babel==2.9.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_version >= "3.6" and python_full_version >= "3.4.0"
-backcall==0.2.0
+backcall==0.2.0; python_version == "3.6" or python_version >= "3.7"
bcrypt==3.2.0; python_version >= "3.6"
-beautifulsoup4==4.9.3; python_version >= "3.6" and python_version < "4.0"
+beautifulsoup4==4.9.3; python_version >= "3.6" and python_version < "4.0" or python_version >= "3.6"
bleach==3.3.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_version >= "3.6" and python_full_version >= "3.5.0"
-boto3==1.17.83; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.6"
-botocore==1.20.83; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.6"
-cachecontrol==0.12.6; python_version >= "3.6" and python_full_version < "3.0.0" or python_version >= "3.6" and python_full_version >= "3.5.0"
+boto3==1.17.109; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.6"
+botocore==1.20.109; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.6"
+cachecontrol==0.12.6; python_version >= "3.6" and python_full_version < "3.0.0" and python_version < "4.0" or python_version >= "3.6" and python_version < "4.0" and python_full_version >= "3.4.0"
cached-property==1.5.2; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6"
cachetools==4.2.2; python_version >= "3.5" and python_version < "4.0" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.6")
-cachy==0.3.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_version >= "3.6" and python_full_version >= "3.5.0"
-certifi==2020.12.5; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6"
-cffi==1.14.5; python_version >= "3.6" and python_full_version < "3.0.0" or python_version >= "3.6" and python_full_version >= "3.4.0"
+cachy==0.3.0; python_version >= "3.6" and python_full_version < "3.0.0" and python_version < "4.0" or python_version >= "3.6" and python_version < "4.0" and python_full_version >= "3.4.0"
+certifi==2021.5.30; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" or python_version >= "3.6"
+cffi==1.14.6; python_version >= "3.6" and python_full_version < "3.0.0" or python_version >= "3.6" and python_full_version >= "3.4.0" or python_version >= "3.6"
chardet==4.0.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6"
-cleo==0.8.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_version >= "3.6" and python_full_version >= "3.5.0"
-clikit==0.6.2; python_version >= "3.6" and python_full_version < "3.0.0" or python_version >= "3.6" and python_full_version >= "3.5.0"
+cleo==1.0.0a3; python_version >= "3.6" and python_version < "4.0"
cloudflare==2.8.15; python_version >= "3.6"
-colorama==0.4.3; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0")
-configargparse==1.4.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6"
+colorama==0.4.3; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.6" or python_version >= "3.6" and python_full_version < "3.0.0" and sys_platform == "win32" or sys_platform == "win32" and python_version >= "3.6" and python_full_version >= "3.5.0" or python_version >= "3.6" and python_full_version < "3.0.0" and python_version < "4.0" and sys_platform == "win32" or python_version >= "3.6" and python_version < "4.0" and sys_platform == "win32" and python_full_version >= "3.5.0" or python_version >= "3.6" and python_full_version < "3.0.0" and platform_system == "Windows" or python_version >= "3.6" and python_full_version >= "3.5.0" and platform_system == "Windows" or python_version >= "3.6" and python_full_version >= "3.5.0" or python_version == "3.6" and python_full_version < "3.0.0" and sys_platform == "win32" or python_version == "3.6" and sys_platform == "win32" and python_full_version >= "3.5.0" or python_version >= "3.7" and python_full_version < "3.0.0" and sys_platform == "win32" or python_version >= "3.7" and sys_platform == "win32" and python_full_version >= "3.5.0"
+configargparse==1.5.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6"
configobj==5.0.6; python_version >= "3.6"
coverage==5.5; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version < "4" and python_version >= "3.6"
-crashtest==0.3.1; python_version >= "3.6" and python_version < "4.0" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_version >= "3.6" and python_full_version >= "3.5.0")
-cryptography==3.4.7; python_version >= "3.6" and python_full_version < "3.0.0" and python_version < "4.0" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_version >= "3.6" and python_full_version >= "3.5.0") and sys_platform == "linux" or python_full_version >= "3.5.0" and python_version >= "3.6" and python_version < "4.0" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_version >= "3.6" and python_full_version >= "3.5.0") and sys_platform == "linux"
+crashtest==0.3.1; python_version >= "3.6" and python_version < "4.0"
+cryptography==3.4.7; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" or python_version >= "3.6" or python_version >= "3.6" and python_version < "4.0" or python_version >= "3.6" and python_version < "4.0" and sys_platform == "linux"
cython==0.29.23; (python_version >= "2.6" and python_full_version < "3.0.0") or (python_full_version >= "3.3.0")
-decorator==5.0.9
+dataclasses==0.8; python_version >= "3.6" and python_version < "3.7"
+decorator==5.0.9; python_version == "3.6" or python_version > "3.6" or python_version >= "3.5" or python_version >= "3.7"
deprecated==1.2.12; python_version >= "3.6" and python_full_version < "3.0.0" or python_version >= "3.6" and python_full_version >= "3.4.0"
-distlib==0.3.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_version >= "3.6" and python_full_version >= "3.5.0"
-distro==1.5.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6"
-dns-lexicon==3.6.0; python_version >= "3.6" and python_version < "4.0"
+distlib==0.3.2; python_version >= "3.6" and python_full_version < "3.0.0" and python_version < "4.0" or python_version >= "3.6" and python_version < "4.0" and python_full_version >= "3.4.0" or python_version >= "3.6"
+distro==1.5.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" or python_version >= "3.6"
+dns-lexicon==3.6.1; python_version >= "3.6" and python_version < "4.0"
dnspython==2.1.0; python_version >= "3.6"
docker-compose==1.26.2; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6"
docker==4.2.2; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6"
dockerpty==0.4.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6"
docopt==0.6.2; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6"
-docutils==0.15.2; (python_version >= "2.6" and python_full_version < "3.0.0") or (python_full_version >= "3.3.0")
-execnet==1.8.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6"
+docutils==0.15.2; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.6" or python_version >= "3.6" and python_full_version >= "3.3.0"
+entrypoints==0.3; python_version >= "3.6" and python_version < "4.0"
+execnet==1.9.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6"
fabric==2.6.0; python_version >= "3.6"
-filelock==3.0.12; python_version >= "3.6" and python_full_version < "3.0.0" and python_version < "4.0" or python_version >= "3.6" and python_full_version >= "3.5.0" and python_version < "4.0"
-google-api-core==1.28.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.6"
-google-api-python-client==2.6.0; python_version >= "3.6"
+filelock==3.0.12; python_version >= "3.6" and python_full_version < "3.0.0" or python_version >= "3.6" and python_full_version >= "3.5.0" or python_version >= "3.6" and python_full_version < "3.0.0" and python_version < "4.0" or python_version >= "3.6" and python_version < "4.0" and python_full_version >= "3.4.0" or python_version >= "3.6" and python_version < "4.0"
+google-api-core==1.31.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.6"
+google-api-python-client==2.12.0; python_version >= "3.6"
google-auth-httplib2==0.1.0; python_version >= "3.6"
-google-auth==1.30.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.6"
+google-auth==1.32.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.6"
googleapis-common-protos==1.53.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.6"
-html5lib==1.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_version >= "3.6" and python_full_version >= "3.5.0"
+html5lib==1.1; python_version >= "3.6" and python_full_version < "3.0.0" and python_version < "4.0" or python_version >= "3.6" and python_version < "4.0" and python_full_version >= "3.5.0"
httplib2==0.19.1; python_version >= "3.6"
-idna==2.10; python_version >= "3.6" and python_full_version < "3.0.0" and python_version < "4.0" or python_full_version >= "3.5.0" and python_version >= "3.6" and python_version < "4.0"
+idna==2.10; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" or python_version >= "3.6" and python_full_version < "3.0.0" and python_version < "4.0" or python_version >= "3.6" and python_version < "4.0" and python_full_version >= "3.4.0"
imagesize==1.2.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_version >= "3.6" and python_full_version >= "3.4.0"
-importlib-metadata==1.7.0; python_version >= "3.6" and python_full_version < "3.0.0" and python_version < "3.8" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_version >= "3.6" and python_full_version >= "3.5.0") or python_version < "3.8" and python_version >= "3.6" and python_full_version >= "3.5.0" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_version >= "3.6" and python_full_version >= "3.5.0")
-importlib-resources==5.1.4; python_version >= "3.6" and python_full_version < "3.0.0" and python_version < "3.7" or python_version >= "3.6" and python_full_version >= "3.5.0" and python_version < "3.7"
+importlib-metadata==1.7.0; python_version >= "3.6" and python_full_version < "3.0.0" and python_version < "3.8" or python_version >= "3.6" and python_version < "3.8" and python_full_version >= "3.5.0"
+importlib-resources==5.2.0; python_version >= "3.6" and python_full_version < "3.0.0" and python_version < "3.7" or python_version >= "3.6" and python_version < "3.7" and python_full_version >= "3.4.0"
iniconfig==1.1.1; python_version >= "3.6"
-invoke==1.5.0; python_version >= "3.6"
-ipdb==0.13.8; python_version >= "3.6"
-ipython-genutils==0.2.0; python_version == "3.6"
+invoke==1.6.0; python_version >= "3.6"
+ipdb==0.13.9; python_version >= "3.6"
+ipython-genutils==0.2.0
ipython==7.16.1; python_version == "3.6"
-ipython==7.24.0; python_version >= "3.7"
+ipython==7.25.0; python_version >= "3.7"
isodate==0.6.0; python_version >= "3.6"
isort==5.8.0; python_version >= "3.6" and python_version < "4.0"
-jedi==0.18.0
-jeepney==0.6.0; python_version >= "3.6" and python_version < "4.0" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_version >= "3.6" and python_full_version >= "3.5.0") and sys_platform == "linux"
-jinja2==3.0.1; python_version >= "3.6"
+jedi==0.18.0; python_version == "3.6" or python_version >= "3.7"
+jeepney==0.6.0; python_version >= "3.6" and python_version < "4.0" and sys_platform == "linux"
+jinja2==3.0.1; python_version >= "3.6" or python_version >= "3.6"
jmespath==0.10.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.6"
josepy==1.8.0; python_version >= "3.6"
jsonlines==2.0.0; python_version >= "3.6"
jsonpickle==2.0.0; python_version >= "3.6"
jsonschema==3.2.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6"
-keyring==21.8.0; python_version >= "3.6" and python_version < "4.0" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_version >= "3.6" and python_full_version >= "3.5.0")
+keyring==22.3.0; python_version >= "3.6" and python_version < "4.0" or python_version >= "3.6"
lazy-object-proxy==1.6.0; python_version >= "3.6" and python_full_version < "3.0.0" and python_version < "4.0" or python_version >= "3.6" and python_version < "4.0" and python_full_version >= "3.6.0"
lockfile==0.12.2
markupsafe==2.0.1; python_version >= "3.6"
matplotlib-inline==0.1.2; python_version >= "3.7"
mccabe==0.6.1; python_version >= "3.6" and python_version < "4.0"
mock==4.0.3; python_version >= "3.6"
-msgpack==1.0.2; python_version >= "3.6" and python_full_version < "3.0.0" or python_version >= "3.6" and python_full_version >= "3.5.0"
+msgpack==1.0.2; python_version >= "3.6" and python_full_version < "3.0.0" and python_version < "4.0" or python_version >= "3.6" and python_version < "4.0" and python_full_version >= "3.4.0"
msrest==0.6.21; python_version >= "3.6"
mypy-extensions==0.4.3; python_version >= "3.6"
-mypy==0.812; python_version >= "3.6"
+mypy==0.812; python_version >= "3.5"
oauth2client==4.1.3; python_version >= "3.6"
-oauthlib==3.1.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_version >= "3.6" and python_full_version >= "3.4.0"
-packaging==20.9; python_version >= "3.6" and python_full_version < "3.0.0" or python_version >= "3.6" and python_full_version >= "3.6.0"
-paramiko==2.7.2; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6"
+oauthlib==3.1.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_version >= "3.6" and python_full_version >= "3.4.0"
+packaging==20.9; python_version >= "3.6" and python_full_version < "3.0.0" and python_version < "4.0" or python_version >= "3.6" and python_version < "4.0" and python_full_version >= "3.4.0" or python_version >= "3.6" and python_full_version < "3.0.0" or python_version >= "3.6" and python_full_version >= "3.4.0" or python_version >= "3.6" and python_full_version >= "3.5.0" or python_full_version >= "3.6.0" and python_version >= "3.6"
+paramiko==2.7.2; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" or python_version >= "3.6"
parsedatetime==2.6; python_version >= "3.6"
parso==0.8.2; python_version == "3.6"
-pastel==0.2.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_version >= "3.6" and python_full_version >= "3.5.0"
-pathlib2==2.3.5; python_version >= "3.6"
-pexpect==4.8.0
-pickleshare==0.7.5
-pkginfo==1.7.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_version >= "3.6" and python_full_version >= "3.5.0"
-pluggy==0.13.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6"
+pathlib2==2.3.6; python_version >= "3.6"
+pexpect==4.8.0; python_version >= "3.6" and python_version < "4.0" or python_version == "3.6" and sys_platform != "win32" or python_version >= "3.7" and sys_platform != "win32"
+pickleshare==0.7.5; python_version == "3.6" or python_version >= "3.7"
+pip==20.2.4; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0")
+pkginfo==1.7.1; python_version >= "3.6" and python_version < "4.0" or python_version >= "3.6"
+pluggy==0.13.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" or python_version >= "3.6" and python_full_version >= "3.5.0"
ply==3.11; python_version >= "3.6"
-poetry-core==1.0.3; python_version >= "3.6" and python_full_version < "3.0.0" or python_version >= "3.6" and python_full_version >= "3.5.0"
-poetry==1.1.6; python_version >= "3.6" and python_full_version < "3.0.0" or python_version >= "3.6" and python_full_version >= "3.5.0"
-prompt-toolkit==3.0.3
-protobuf==3.17.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.6"
-ptyprocess==0.7.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_version >= "3.6" and python_full_version >= "3.5.0"
-py==1.10.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6"
-pyasn1-modules==0.2.8; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.6"
-pyasn1==0.4.8; python_version >= "3.6" and python_version < "4" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.6")
+poetry-core==1.1.0a5; python_version >= "3.6" and python_version < "4.0"
+poetry==1.2.0a1; python_version >= "3.6" and python_version < "4.0"
+prompt-toolkit==3.0.3; python_version == "3.6" or python_version >= "3.7"
+protobuf==3.17.3; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.6"
+ptyprocess==0.7.0; python_version >= "3.6" and python_version < "4.0"
+py==1.10.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" or python_version >= "3.6" and python_full_version >= "3.5.0"
+pyasn1-modules==0.2.8; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.6" or python_version >= "3.6"
+pyasn1==0.4.8; python_version >= "3.5" and python_version < "4" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.6") or python_version >= "3.6"
pycparser==2.20; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6"
pygithub==1.55; python_version >= "3.6"
-pygments==2.9.0
+pygments==2.9.0; python_version >= "3.6" or python_version == "3.6" or python_version >= "3.7"
pyjwt==2.1.0; python_version >= "3.6"
-pylev==1.3.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_version >= "3.6" and python_full_version >= "3.5.0"
-pylint==2.8.2; python_version >= "3.6" and python_version < "4.0"
+pylev==1.4.0; python_version >= "3.6" and python_version < "4.0"
+pylint==2.9.3; python_version >= "3.6" and python_version < "4.0"
pynacl==1.4.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_version >= "3.6" and python_full_version >= "3.4.0"
pynsist==2.7; python_version >= "3.6"
pyopenssl==20.0.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6"
-pyparsing==2.4.7; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6"
+pyparsing==2.4.7; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.6" or python_version >= "3.6" and python_full_version < "3.0.0" and python_version < "4.0" or python_version >= "3.6" and python_version < "4.0" and python_full_version >= "3.4.0"
pypiwin32==223; sys_platform == "win32" and python_version >= "3.6" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6")
pyrfc3339==1.1; python_version >= "3.6"
-pyrsistent==0.17.3; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6"
-pytest-cov==2.12.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6"
+pyrsistent==0.18.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6"
+pytest-cov==2.12.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6"
pytest-forked==1.3.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6"
-pytest-xdist==2.2.1; python_version >= "3.6"
-pytest==6.2.4; python_version >= "3.6" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6")
+pytest-xdist==2.3.0; python_version >= "3.6" or python_version >= "3.6"
+pytest==6.2.4; python_version >= "3.6" or python_version >= "3.6" or python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6"
python-augeas==0.5.0
-python-dateutil==2.8.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.6"
+python-dateutil==2.8.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.6" or python_full_version >= "3.6.0" and python_version >= "3.6"
python-digitalocean==1.16.0; python_version >= "3.6"
-python-dotenv==0.17.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6"
-pytz==2021.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_version >= "3.6" and python_full_version >= "3.6.0"
-pywin32-ctypes==0.2.0; python_version >= "3.6" and python_version < "4.0" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_version >= "3.6" and python_full_version >= "3.5.0") and sys_platform == "win32"
-pywin32==300; sys_platform == "win32" and python_version >= "3.6" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6")
-pyyaml==5.4.1; python_version >= "3.6" and python_full_version < "3.0.0" and python_version < "4.0" or python_full_version >= "3.6.0" and python_version >= "3.6" and python_version < "4.0"
+python-dotenv==0.18.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6"
+pytz==2021.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_version >= "3.6" and python_full_version >= "3.4.0" or python_version >= "3.6" or python_full_version >= "3.6.0" and python_version >= "3.6"
+pywin32-ctypes==0.2.0; python_version >= "3.6" and python_version < "4.0" and sys_platform == "win32"
+pywin32==301; sys_platform == "win32" and python_version >= "3.6" or sys_platform == "win32" and python_version >= "3.6" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6")
+pyyaml==5.4.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.6" or python_version >= "3.6" and python_full_version < "3.0.0" and python_version < "4.0" or python_version >= "3.6" and python_version < "4.0" and python_full_version >= "3.6.0"
readme-renderer==29.0; python_version >= "3.6"
repoze.sphinx.autointerface==0.8; python_version >= "3.6"
requests-download==0.1.2; python_version >= "3.6"
requests-file==1.5.1; python_version >= "3.6" and python_version < "4.0"
requests-oauthlib==1.3.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_version >= "3.6" and python_full_version >= "3.4.0"
-requests-toolbelt==0.9.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_version >= "3.6" and python_full_version >= "3.5.0"
-requests==2.25.1; python_version >= "3.6" and python_full_version < "3.0.0" and python_version < "4.0" or python_version >= "3.6" and python_full_version >= "3.6.0" and python_version < "4.0"
+requests-toolbelt==0.9.1; python_version >= "3.6" and python_version < "4.0" or python_version >= "3.6" or python_version >= "3.6"
+requests==2.25.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" or python_version >= "3.6" and python_full_version < "3.0.0" and python_version < "4.0" or python_version >= "3.6" and python_version < "4.0" and python_full_version >= "3.5.0" or python_full_version >= "3.6.0" and python_version >= "3.6"
rfc3986==1.5.0; python_version >= "3.6"
-rsa==4.7.2; python_version >= "3.6" and python_version < "4" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.6")
+rsa==4.7.2; python_version >= "3.5" and python_version < "4" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.6") or python_version >= "3.6" and python_version < "4"
s3transfer==0.4.2; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.6"
-secretstorage==3.3.1; python_version >= "3.6" and python_version < "4.0" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_version >= "3.6" and python_full_version >= "3.5.0") and sys_platform == "linux"
-shellingham==1.4.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_version >= "3.6" and python_full_version >= "3.5.0"
-six==1.16.0; python_version == "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version == "3.6"
+secretstorage==3.3.1; python_version >= "3.6" and python_version < "4.0" and sys_platform == "linux"
+setuptools==57.1.0; python_version >= "3.6" or python_version >= "3.6" or python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" or python_full_version >= "3.4.0" and python_version >= "3.6" or python_full_version >= "3.6.0" and python_version >= "3.6" or python_version == "3.6" or python_version >= "3.7"
+shellingham==1.4.0; python_version >= "3.6" and python_version < "4.0"
+six==1.16.0; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" or python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" or python_full_version >= "3.5.0" and python_version >= "3.6" or python_full_version >= "3.3.0" and python_version >= "3.6" or python_version >= "3.6" and python_full_version < "3.0.0" and python_version < "4.0" or python_version >= "3.6" and python_version < "4.0" and python_full_version >= "3.5.0" or python_version >= "3.6" and python_version < "4.0" and python_full_version >= "3.4.0" or python_full_version >= "3.6.0" and python_version >= "3.6" or python_version >= "3.6" and python_version < "4.0" and python_full_version >= "3.3.0" or python_version == "3.6" and python_full_version < "3.0.0" or python_version == "3.6" and python_full_version >= "3.3.0"
snowballstemmer==2.1.0; python_version >= "3.6"
soupsieve==2.2.1; python_version >= "3.6"
sphinx-rtd-theme==0.5.2; python_version >= "3.6"
@@ -162,27 +163,25 @@ sphinxcontrib-qthelp==1.0.3; python_version >= "3.6"
sphinxcontrib-serializinghtml==1.1.5; python_version >= "3.6"
texttable==1.6.3; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6"
tldextract==3.1.0; python_version >= "3.6" and python_version < "4.0"
-toml==0.10.2; python_version >= "3.6" and python_full_version < "3.0.0" and python_version < "4.0" or python_version >= "3.6" and python_version < "4.0" and python_full_version >= "3.5.0"
-tomlkit==0.7.2; python_version >= "3.6" and python_full_version < "3.0.0" or python_version >= "3.6" and python_full_version >= "3.5.0"
+toml==0.10.2; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.6" or python_full_version >= "3.5.0" and python_version >= "3.6" or python_version == "3.6" and python_full_version < "3.0.0" or python_version > "3.6" and python_full_version < "3.0.0" or python_version == "3.6" and python_full_version >= "3.3.0" or python_version > "3.6" and python_full_version >= "3.3.0" or python_version >= "3.6" and python_full_version < "3.0.0" and python_version < "4.0" or python_version >= "3.6" and python_version < "4.0" and python_full_version >= "3.3.0"
+tomlkit==0.7.2; python_version >= "3.6" and python_full_version < "3.0.0" and python_version < "4.0" or python_version >= "3.6" and python_version < "4.0" and python_full_version >= "3.5.0"
tox==3.23.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_version >= "3.6" and python_full_version >= "3.5.0"
-tqdm==4.61.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_version >= "3.6" and python_full_version >= "3.4.0"
+tqdm==4.61.2; python_version >= "3.6" and python_full_version < "3.0.0" or python_version >= "3.6" and python_full_version >= "3.4.0"
traitlets==4.3.3
twine==3.3.0; python_version >= "3.6"
-typed-ast==1.4.3; implementation_name == "cpython" and python_version < "3.8" and python_version >= "3.6"
-typing-extensions==3.10.0.0; python_version >= "3.6"
+typed-ast==1.4.3; python_version >= "3.6" or implementation_name == "cpython" and python_version < "3.8" and python_version >= "3.6"
+typing-extensions==3.10.0.0; python_version >= "3.6" or python_version >= "3.6" and python_version < "3.8"
uritemplate==3.0.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6"
-urllib3==1.26.5; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version < "4" and python_version >= "3.6"
-virtualenv==20.4.7; python_version >= "3.6" and python_full_version < "3.0.0" or python_version >= "3.6" and python_full_version >= "3.5.0"
+urllib3==1.26.6; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version < "4" and python_version >= "3.6" or python_full_version >= "3.6.0" and python_version < "4" and python_version >= "3.6"
+virtualenv==20.4.4; python_version >= "3.6" and python_full_version < "3.0.0" and python_version < "4.0" or python_version >= "3.6" and python_version < "4.0" and python_full_version >= "3.4.0" or python_version >= "3.6" and python_full_version < "3.0.0" or python_version >= "3.6" and python_full_version >= "3.5.0"
wcwidth==0.2.5; python_version == "3.6"
-webencodings==0.5.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_version >= "3.6" and python_full_version >= "3.5.0"
-websocket-client==0.59.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6"
-wrapt==1.12.1; python_version >= "3.6" and python_version < "4.0" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_version >= "3.6" and python_full_version >= "3.4.0")
+webencodings==0.5.1; python_version >= "3.6" and python_full_version < "3.0.0" and python_version < "4.0" or python_version >= "3.6" and python_version < "4.0" and python_full_version >= "3.5.0" or python_version >= "3.6" and python_full_version < "3.0.0" or python_version >= "3.6" and python_full_version >= "3.5.0"
+websocket-client==0.59.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" or python_full_version >= "3.5.0" and python_version >= "3.6"
+wheel==0.36.2; python_version >= "3.6" and python_full_version < "3.0.0" or python_version >= "3.6" and python_full_version >= "3.5.0"
+wrapt==1.12.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_version >= "3.6" and python_full_version >= "3.4.0" or python_version >= "3.6" and python_version < "4.0"
yarg==0.1.9; python_version >= "3.6"
-zipp==3.4.1; python_version >= "3.6" and python_full_version < "3.0.0" and python_version < "3.7" or python_version < "3.7" and python_version >= "3.6" and python_full_version >= "3.5.0"
-zope.component==5.0.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6"
+zipp==3.5.0; python_version >= "3.6" and python_full_version < "3.0.0" and python_version < "3.8" or python_version >= "3.6" and python_version < "3.8" and python_full_version >= "3.5.0" or python_version >= "3.6" and python_full_version < "3.0.0" and python_version < "3.7" or python_version >= "3.6" and python_version < "3.7" and python_full_version >= "3.4.0"
+zope.component==5.0.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6"
zope.event==4.5.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6"
zope.hookable==5.0.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6"
zope.interface==5.4.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6"
-pip==20.2.4
-setuptools==54.1.2
-wheel==0.35.1
diff --git a/tools/strip_hashes.py b/tools/strip_hashes.py
deleted file mode 100755
index 988e72eb831..00000000000
--- a/tools/strip_hashes.py
+++ /dev/null
@@ -1,50 +0,0 @@
-#!/usr/bin/env python
-"""Removes hash information from requirement files passed to it as file path
-arguments or simply piped to stdin."""
-
-import re
-import sys
-
-
-def process_entries(entries):
- """Strips off hash strings from dependencies.
-
- :param list entries: List of entries
-
- :returns: list of dependencies without hashes
- :rtype: list
- """
- out_lines = []
- for e in entries:
- e = e.strip()
- search = re.search(r'^(\S*==\S*).*$', e)
- if search:
- out_lines.append(search.group(1))
- return out_lines
-
-def main(*paths):
- """
- Reads dependency definitions from a (list of) file(s) provided on the
- command line. If no command line arguments are present, data is read from
- stdin instead.
-
- Hashes are removed from returned entries.
- """
-
- deps = []
- if paths:
- for path in paths:
- with open(path) as file_h:
- deps += process_entries(file_h.readlines())
- else:
- # Need to check if interactive to avoid blocking if nothing is piped
- if not sys.stdin.isatty():
- stdin_data = []
- for line in sys.stdin:
- stdin_data.append(line)
- deps += process_entries(stdin_data)
-
- return "\n".join(deps)
-
-if __name__ == '__main__':
- print(main(*sys.argv[1:])) # pylint: disable=star-args
diff --git a/tools/venv.py b/tools/venv.py
index f3f5781fae6..8d8b01018be 100755
--- a/tools/venv.py
+++ b/tools/venv.py
@@ -24,8 +24,8 @@
import time
REQUIREMENTS = [
- '-e acme[dev]',
- '-e certbot[dev,docs]',
+ '-e acme[test]',
+ '-e certbot[all]',
'-e certbot-apache',
'-e certbot-dns-cloudflare',
'-e certbot-dns-cloudxns',
diff --git a/tox.ini b/tox.ini
index cfe6ba2c540..7bdb57cffa4 100644
--- a/tox.ini
+++ b/tox.ini
@@ -19,7 +19,7 @@ install_packages = python {toxinidir}/tools/pip_install_editable.py {[base]all_p
# behavior with substitutions that contain line continuations, see
# https://github.com/tox-dev/tox/issues/2069 for more info.
dns_packages = certbot-dns-cloudflare certbot-dns-cloudxns certbot-dns-digitalocean certbot-dns-dnsimple certbot-dns-dnsmadeeasy certbot-dns-gehirn certbot-dns-google certbot-dns-linode certbot-dns-luadns certbot-dns-nsone certbot-dns-ovh certbot-dns-rfc2136 certbot-dns-route53 certbot-dns-sakuracloud
-all_packages = acme[dev] certbot[dev] certbot-apache {[base]dns_packages} certbot-nginx
+all_packages = acme[test] certbot[test] certbot-apache {[base]dns_packages} certbot-nginx
source_paths = acme/acme certbot/certbot certbot-ci/certbot_integration_tests certbot-apache/certbot_apache certbot-compatibility-test/certbot_compatibility_test certbot-dns-cloudflare/certbot_dns_cloudflare certbot-dns-cloudxns/certbot_dns_cloudxns certbot-dns-digitalocean/certbot_dns_digitalocean certbot-dns-dnsimple/certbot_dns_dnsimple certbot-dns-dnsmadeeasy/certbot_dns_dnsmadeeasy certbot-dns-gehirn/certbot_dns_gehirn certbot-dns-google/certbot_dns_google certbot-dns-linode/certbot_dns_linode certbot-dns-luadns/certbot_dns_luadns certbot-dns-nsone/certbot_dns_nsone certbot-dns-ovh/certbot_dns_ovh certbot-dns-rfc2136/certbot_dns_rfc2136 certbot-dns-route53/certbot_dns_route53 certbot-dns-sakuracloud/certbot_dns_sakuracloud certbot-nginx/certbot_nginx tests/lock_test.py
[testenv]
@@ -54,7 +54,7 @@ setenv =
basepython =
{[testenv:oldest]basepython}
commands =
- {[base]install_and_test} acme[dev]
+ {[base]install_and_test} acme[test]
setenv =
{[testenv:oldest]setenv}
@@ -62,7 +62,7 @@ setenv =
basepython =
{[testenv:oldest]basepython}
commands =
- {[base]pip_install} acme[dev] certbot[dev] certbot-apache
+ {[base]pip_install} acme[test] certbot[test] certbot-apache
pytest certbot-apache
setenv =
{[testenv:oldest]setenv}
@@ -71,7 +71,7 @@ setenv =
basepython =
{[testenv:oldest]basepython}
commands =
- {[base]pip_install} acme[dev] certbot[dev] certbot-apache[dev]
+ {[base]pip_install} acme[test] certbot[test] certbot-apache[dev]
pytest certbot-apache
setenv =
{[testenv:oldest]setenv}
@@ -80,7 +80,7 @@ setenv =
basepython =
{[testenv:oldest]basepython}
commands =
- {[base]pip_install} acme[dev] certbot[dev]
+ {[base]pip_install} acme[test] certbot[test]
pytest certbot
setenv =
{[testenv:oldest]setenv}
@@ -89,7 +89,7 @@ setenv =
basepython =
{[testenv:oldest]basepython}
commands =
- {[base]pip_install} acme[dev] certbot[dev] {[base]dns_packages}
+ {[base]pip_install} acme[test] certbot[test] {[base]dns_packages}
pytest {[base]dns_packages}
setenv =
{[testenv:oldest]setenv}
@@ -98,7 +98,7 @@ setenv =
basepython =
{[testenv:oldest]basepython}
commands =
- {[base]pip_install} acme[dev] certbot[dev] certbot-nginx
+ {[base]pip_install} acme[test] certbot[test] certbot-nginx
pytest certbot-nginx
python tests/lock_test.py
setenv =