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 =