From 3186774c2c32f8af41d5c1c323525518f0b90f93 Mon Sep 17 00:00:00 2001 From: Adrien Ferrand Date: Sun, 9 May 2021 09:11:53 +0200 Subject: [PATCH 01/47] Define new ABC classes --- certbot/certbot/interfaces.py | 456 +++++++++++++++++++++++----------- 1 file changed, 310 insertions(+), 146 deletions(-) diff --git a/certbot/certbot/interfaces.py b/certbot/certbot/interfaces.py index de9175def68..0c1ec5f821e 100644 --- a/certbot/certbot/interfaces.py +++ b/certbot/certbot/interfaces.py @@ -1,16 +1,19 @@ """Certbot client interfaces.""" -import abc -from typing import Optional +from abc import ABCMeta, abstractmethod, abstractproperty +from argparse import ArgumentParser +from typing import Optional, Iterable, List, Union, Tuple import zope.interface # pylint: disable=no-self-argument,no-method-argument,inherit-non-class +from acme.challenges import Challenge, 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 +23,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 +33,7 @@ def load(self, account_id): # pragma: no cover """ raise NotImplementedError() - @abc.abstractmethod + @abstractmethod def save(self, account, client): # pragma: no cover """Save account. @@ -41,7 +44,17 @@ def save(self, account, client): # pragma: no cover class IPluginFactory(zope.interface.Interface): - """IPlugin factory. + pass + + +class IPlugin(zope.interface.Interface): + pass + + +@zope.interface.provider(IPluginFactory) +@zope.interface.implementer(IPlugin) +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 +83,21 @@ class IPluginFactory(zope.interface.Interface): """ - description = zope.interface.Attribute("Short plugin description") + description: str = NotImplemented + """Short plugin description""" - def __call__(config, name): # pylint: disable=signature-differs + @abstractmethod + def __init__(self, name: str): """Create new `IPlugin`. :param IConfig config: Configuration. :param str name: Unique plugin name. """ + self.name = name - def inject_parser_options(parser, name): - """Inject argument parser options (flags). - - 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 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 +116,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 +127,29 @@ def more_info(): # type: ignore """ + @classmethod + @abstractmethod + def inject_parser_options(cls, parser: ArgumentParser, name: str) -> None: + """Inject argument parser options (flags). + + 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): + pass + + +@zope.interface.implementer(IAuthenticator) +class Authenticator(Plugin): """Generic Certbot Authenticator. Class represents all possible tools processes that have the @@ -136,7 +157,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 +171,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 +192,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 @@ -185,89 +209,184 @@ def cleanup(achalls): class IConfig(zope.interface.Interface): + pass + + +@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. """ - 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." - ) + + @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 IInstaller(IPlugin): + pass + + +class Installer(Plugin): """Generic Certbot Installer Interface. Represents any server that an X509 certificate can be placed. @@ -282,14 +401,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 +425,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 +442,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 +452,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 +475,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 +495,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 @@ -384,10 +513,17 @@ def restart(): # type: ignore class IDisplay(zope.interface.Interface): + pass + + +@zope.interface.implementer(IDisplay) +class IDisplay(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, wrap: bool = True, + force_interactive: bool = False): """Displays a string message :param str message: Message to display @@ -399,9 +535,11 @@ def notification(message, pause, wrap=True, force_interactive=False): """ - 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 @@ -429,7 +567,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_args: Optional[str] = None, + force_interactive: bool = False) -> Tuple[str, int]: """Accept input from the user. When not setting force_interactive=True, you must provide a @@ -437,6 +577,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_args: 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 +591,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[str] = None, cli_args: 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 +603,10 @@ def yesno(message, yes_label="Yes", no_label="No", default=None, default value. :param str message: question for the user + :param str yes_label: label for Yes button + :param str no_label: label for No button :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 cli_args: 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 +618,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_args: Optional[str] = None, + force_interactive: bool = False) -> Tuple[int, List[str]]: """Allow for multiple selections from a menu. When not setting force_interactive=True, you must provide a @@ -482,7 +630,7 @@ def checklist(message, tags, default=None, cli_args=None, force_interactive=Fals :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 str cli_args: to automate choice from the menu, eg "--domains" :param bool force_interactive: True if it's safe to prompt the user because it won't cause any workflow regressions @@ -496,8 +644,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 @@ -520,16 +670,22 @@ def directory_select(self, message, default=None, class IReporter(zope.interface.Interface): + pass + + +@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 +698,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(object, metaclass=ABCMeta): """Interface to a certificate lineage.""" - @abc.abstractproperty + @property + @abstractmethod def cert_path(self): """Path to the certificate file. @@ -557,7 +715,8 @@ def cert_path(self): """ - @abc.abstractproperty + @property + @abstractmethod def key_path(self): """Path to the private key file. @@ -565,7 +724,8 @@ def key_path(self): """ - @abc.abstractproperty + @property + @abstractmethod def chain_path(self): """Path to the certificate chain file. @@ -573,7 +733,8 @@ def chain_path(self): """ - @abc.abstractproperty + @property + @abstractmethod def fullchain_path(self): """Path to the full chain file. @@ -583,7 +744,8 @@ def fullchain_path(self): """ - @abc.abstractproperty + @property + @abstractmethod def lineagename(self): """Name given to the certificate lineage. @@ -591,7 +753,8 @@ def lineagename(self): """ - @abc.abstractmethod + @property + @abstractmethod def names(self): """What are the subject names of this certificate? @@ -611,7 +774,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 +790,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 +807,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 +818,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 From a8e03bbd5f699e3725b49697ee1c2b274264a357 Mon Sep 17 00:00:00 2001 From: Adrien Ferrand Date: Sun, 9 May 2021 11:53:48 +0200 Subject: [PATCH 02/47] Reimplement services with new ABC classes --- certbot/certbot/_internal/configuration.py | 120 ++++++++++++++++----- certbot/certbot/_internal/reporter.py | 7 +- certbot/certbot/display/util.py | 10 +- certbot/certbot/interfaces.py | 2 +- certbot/certbot/services.py | 38 +++---- 5 files changed, 118 insertions(+), 59 deletions(-) diff --git a/certbot/certbot/_internal/configuration.py b/certbot/certbot/_internal/configuration.py index aee0022b84a..aff0f456cc0 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,43 @@ def __init__(self, namespace): # Check command line parameters sanity, and error out in case of problem. check_config_sanity(self) - def __getattr__(self, name): - return getattr(self.namespace, name) + # Properties that are part of the abstract Config class contract - def __setattr__(self, name, value): - setattr(self.namespace, name, value) + @property + def server(self) -> str: + return self.namespace.service @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 email(self) -> str: + return self.namespace.email @property - def accounts_dir(self): # pylint: disable=missing-function-docstring - return self.accounts_dir_for_server_path(self.server_path) + def rsa_key_size(self) -> int: + return self.namespace.rsa_key_size - 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 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 +109,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 +200,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/reporter.py b/certbot/certbot/_internal/reporter.py index 295a2d4c5a8..a78b2b6c93d 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.interfaces import Reporter as BaseReporter from certbot import util 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/display/util.py b/certbot/certbot/display/util.py index ea1033a7446..5f3af7c678c 100644 --- a/certbot/certbot/display/util.py +++ b/certbot/certbot/display/util.py @@ -14,15 +14,14 @@ import textwrap from typing import List -import zope.interface from certbot import errors -from certbot import interfaces from certbot import services from certbot._internal import constants from certbot._internal.display import completer from certbot.compat import misc from certbot.compat import os +from certbot.interfaces import Display logger = logging.getLogger(__name__) @@ -46,6 +45,7 @@ """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. @@ -109,8 +109,7 @@ def notify(msg: str) -> None: ) -@zope.interface.implementer(interfaces.IDisplay) -class FileDisplay: +class FileDisplay(Display): """File-based display.""" # see https://github.com/certbot/certbot/issues/3915 @@ -476,8 +475,7 @@ 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: +class NoninteractiveDisplay(Display): """An iDisplay implementation that never asks for interactive user input""" def __init__(self, outfile, *unused_args, **unused_kwargs): diff --git a/certbot/certbot/interfaces.py b/certbot/certbot/interfaces.py index 0c1ec5f821e..69573baa327 100644 --- a/certbot/certbot/interfaces.py +++ b/certbot/certbot/interfaces.py @@ -517,7 +517,7 @@ class IDisplay(zope.interface.Interface): @zope.interface.implementer(IDisplay) -class IDisplay(metaclass=ABCMeta): +class Display(metaclass=ABCMeta): """Generic display.""" # see https://github.com/certbot/certbot/issues/3915 diff --git a/certbot/certbot/services.py b/certbot/certbot/services.py index 5b7caa209d4..0cb54e20d86 100644 --- a/certbot/certbot/services.py +++ b/certbot/certbot/services.py @@ -1,32 +1,26 @@ """This module contains global singletons used throughout Certbot.""" from typing import Optional, Any -from certbot.interfaces import IConfig +from certbot.interfaces import IConfig, Config, Display, Reporter from certbot.interfaces import IDisplay from certbot.interfaces import IReporter class _Services: def __init__(self): - self.display: Optional[IDisplay] = None - self.reporter: Optional[IReporter] = None - self.config: Optional[IConfig] = None + self.display: Optional[Display] = None + self.reporter: Optional[Reporter] = None + self.config: Optional[Config] = None _services = _Services() -# The 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_config() -> Any: +def get_config() -> Config: """Get the Certbot configuration. :return: the Certbot configuration - :rtype: IConfig + :rtype: Config :raise: ValueError if the Certbot configuration is not set """ @@ -35,11 +29,11 @@ def get_config() -> Any: return _services.config -def get_display() -> Any: +def get_display() -> Display: """Get the display utility. :return: the display utility - :rtype: IDisplay + :rtype: Display :raise: ValueError if the display utility is not set """ @@ -48,11 +42,11 @@ def get_display() -> Any: return _services.display -def get_reporter() -> Any: +def get_reporter() -> Reporter: """Get the reporter utility :return: the reporter utility - :rtype: IReporter + :rtype: Reporter :raise: ValueError if the reporter utility is not set """ @@ -61,28 +55,28 @@ def get_reporter() -> Any: return _services.reporter -def set_config(config: Any) -> None: +def set_config(config: Config) -> None: """Set the Certbot configuration. - :param IConfig config: the Certbot configuration + :param Config config: the Certbot configuration """ _services.config = config -def set_display(display: Any) -> None: +def set_display(display: Display) -> None: """Set the display utility. - :param IDisplay display: the display utility + :param Display display: the display utility """ _services.display = display -def set_reporter(reporter: Any) -> None: +def set_reporter(reporter: Reporter) -> None: """Set the reporter utility. - :param IReporter reporter: the reporter utility + :param Reporter reporter: the reporter utility """ _services.reporter = reporter From 809b8722354ceaa6a267bda9795e29d1af84c7a5 Mon Sep 17 00:00:00 2001 From: Adrien Ferrand Date: Sun, 9 May 2021 13:49:15 +0200 Subject: [PATCH 03/47] Adapt plugins discovery and selection --- certbot/certbot/_internal/plugins/disco.py | 45 ++- .../certbot/_internal/plugins/selection.py | 7 +- certbot/certbot/interfaces.py | 331 +++++++++--------- 3 files changed, 199 insertions(+), 184 deletions(-) diff --git a/certbot/certbot/_internal/plugins/disco.py b/certbot/certbot/_internal/plugins/disco.py index 29746ace873..c9524c324e6 100644 --- a/certbot/certbot/_internal/plugins/disco.py +++ b/certbot/certbot/_internal/plugins/disco.py @@ -3,7 +3,7 @@ import itertools import logging import sys -from typing import Dict +from typing import Dict, Type, cast from typing import Optional from typing import Union @@ -49,10 +49,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 @@ -108,7 +108,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) @@ -121,9 +121,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): @@ -131,14 +131,19 @@ 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) - return False + # 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 during init(). + if issubclass(iface, zope.interface.Interface): + 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) + return False + return True @property @@ -260,7 +265,8 @@ 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 (issubclass(plugin_ep.plugin_cls, interfaces.Plugin) + or interfaces.IPluginFactory.providedBy(plugin_ep.plugin_cls)): plugins[plugin_ep.name] = plugin_ep else: # pragma: no cover logger.warning( @@ -339,3 +345,10 @@ 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 _implements(target_class: Type[interfaces.Plugin], iface: Type) -> bool: + if issubclass(iface, zope.interface.Interface): + return cast(iface, zope.interface.Interface).implementedBy(target_class) + + return issubclass(target_class, iface) diff --git a/certbot/certbot/_internal/plugins/selection.py b/certbot/certbot/_internal/plugins/selection.py index d4ec6029e07..68770f19f78 100644 --- a/certbot/certbot/_internal/plugins/selection.py +++ b/certbot/certbot/_internal/plugins/selection.py @@ -18,14 +18,15 @@ def pick_configurator( """Pick configurator plugin.""" return pick_plugin( config, default, plugins, question, - (interfaces.IAuthenticator, interfaces.IInstaller)) + [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.IInstaller, interfaces.Installer]) def pick_authenticator( @@ -33,7 +34,7 @@ def pick_authenticator( "like to authenticate with the ACME CA?"): """Pick authentication plugin.""" return pick_plugin( - config, default, plugins, question, (interfaces.IAuthenticator,)) + config, default, plugins, question, [interfaces.IAuthenticator, interfaces.Authenticator]) def get_unprepared_installer(config, plugins): diff --git a/certbot/certbot/interfaces.py b/certbot/certbot/interfaces.py index 69573baa327..95f8f2d3900 100644 --- a/certbot/certbot/interfaces.py +++ b/certbot/certbot/interfaces.py @@ -43,171 +43,6 @@ def save(self, account, client): # pragma: no cover raise NotImplementedError() -class IPluginFactory(zope.interface.Interface): - pass - - -class IPlugin(zope.interface.Interface): - pass - - -@zope.interface.provider(IPluginFactory) -@zope.interface.implementer(IPlugin) -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 - for your plugin, e.g (excerpt from ``setup.py`` script):: - - setup( - ... - entry_points={ - 'certbot.plugins': [ - 'name=example_project.plugin[plugin_deps]', - ], - }, - extras_require={ - 'plugin_deps': ['dep1', 'dep2'], - } - ) - - Therefore, make sure such objects are importable and usable without - extras. This is necessary, because CLI does the following operations - (in order): - - - loads an entry point, - - calls `inject_parser_options`, - - requires an entry point, - - creates plugin instance (`__call__`). - - """ - - description: str = NotImplemented - """Short plugin description""" - - @abstractmethod - def __init__(self, name: str): - """Create new `IPlugin`. - - :param IConfig config: Configuration. - :param str name: Unique plugin name. - - """ - self.name = name - - @abstractmethod - def prepare(self) -> None: - """Prepare the plugin. - - Finish up any additional initialization. - - :raises .PluginError: - when full initialization cannot be completed. - :raises .MisconfigurationError: - when full initialization cannot be completed. Plugin will - be displayed on a list of available plugins. - :raises .NoInstallationError: - when the necessary programs/files cannot be located. Plugin - will NOT be displayed on a list of available plugins. - :raises .NotSupportedError: - when the installation is recognized, but the version is not - currently supported. - - """ - - @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 - decide which plugin to use. - - :rtype str: - - """ - - @classmethod - @abstractmethod - def inject_parser_options(cls, parser: ArgumentParser, name: str) -> None: - """Inject argument parser options (flags). - - 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): - pass - - -@zope.interface.implementer(IAuthenticator) -class Authenticator(Plugin): - """Generic Certbot Authenticator. - - Class represents all possible tools processes that have the - ability to perform challenges and attain a certificate. - - """ - - @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. - - :returns: `collections.Iterable` of challenge types (subclasses of - :class:`acme.challenges.Challenge`) with the most - preferred challenges first. If a type is not specified, it means the - Authenticator cannot perform the challenge. - :rtype: `collections.Iterable` - - """ - - @abstractmethod - def perform(self, achalls: List[AnnotatedChallenge]) -> Iterable[ChallengeResponse]: - """Perform the given challenge. - - :param list achalls: Non-empty (guaranteed) list of - :class:`~certbot.achallenges.AnnotatedChallenge` - instances, such that it contains types found within - :func:`get_chall_pref` only. - - :returns: `collections.Iterable` of ACME - :class:`~acme.challenges.ChallengeResponse` instances corresponding to each provided - :class:`~acme.challenges.Challenge`. - :rtype: :class:`collections.Iterable` of - :class:`acme.challenges.ChallengeResponse`, - where responses are required to be returned in - the same order as corresponding input challenges - - :raises .PluginError: If some or all challenges cannot be performed - - """ - - @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 - perform, even if perform exited abnormally. - - :param list achalls: Non-empty (guaranteed) list of - :class:`~certbot.achallenges.AnnotatedChallenge` - instances, a subset of those previously passed to :func:`perform`. - - :raises PluginError: if original configuration cannot be restored - - """ - - class IConfig(zope.interface.Interface): pass @@ -382,6 +217,172 @@ def preferred_chain(self) -> str: """ +class IPluginFactory(zope.interface.Interface): + pass + + +class IPlugin(zope.interface.Interface): + pass + + +@zope.interface.provider(IPluginFactory) +@zope.interface.implementer(IPlugin) +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 + for your plugin, e.g (excerpt from ``setup.py`` script):: + + setup( + ... + entry_points={ + 'certbot.plugins': [ + 'name=example_project.plugin[plugin_deps]', + ], + }, + extras_require={ + 'plugin_deps': ['dep1', 'dep2'], + } + ) + + Therefore, make sure such objects are importable and usable without + extras. This is necessary, because CLI does the following operations + (in order): + + - loads an entry point, + - calls `inject_parser_options`, + - requires an entry point, + - creates plugin instance (`__call__`). + + """ + + description: str = NotImplemented + """Short plugin description""" + + @abstractmethod + def __init__(self, config: Config, name: str): + """Create new `Plugin`. + + :param Config config: Configuration. + :param str name: Unique plugin name. + + """ + self.config = config + self.name = name + + @abstractmethod + def prepare(self) -> None: + """Prepare the plugin. + + Finish up any additional initialization. + + :raises .PluginError: + when full initialization cannot be completed. + :raises .MisconfigurationError: + when full initialization cannot be completed. Plugin will + be displayed on a list of available plugins. + :raises .NoInstallationError: + when the necessary programs/files cannot be located. Plugin + will NOT be displayed on a list of available plugins. + :raises .NotSupportedError: + when the installation is recognized, but the version is not + currently supported. + + """ + + @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 + decide which plugin to use. + + :rtype str: + + """ + + @classmethod + @abstractmethod + def inject_parser_options(cls, parser: ArgumentParser, name: str) -> None: + """Inject argument parser options (flags). + + 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): + pass + + +@zope.interface.implementer(IAuthenticator) +class Authenticator(Plugin): + """Generic Certbot Authenticator. + + Class represents all possible tools processes that have the + ability to perform challenges and attain a certificate. + + """ + + @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. + + :returns: `collections.Iterable` of challenge types (subclasses of + :class:`acme.challenges.Challenge`) with the most + preferred challenges first. If a type is not specified, it means the + Authenticator cannot perform the challenge. + :rtype: `collections.Iterable` + + """ + + @abstractmethod + def perform(self, achalls: List[AnnotatedChallenge]) -> Iterable[ChallengeResponse]: + """Perform the given challenge. + + :param list achalls: Non-empty (guaranteed) list of + :class:`~certbot.achallenges.AnnotatedChallenge` + instances, such that it contains types found within + :func:`get_chall_pref` only. + + :returns: `collections.Iterable` of ACME + :class:`~acme.challenges.ChallengeResponse` instances corresponding to each provided + :class:`~acme.challenges.Challenge`. + :rtype: :class:`collections.Iterable` of + :class:`acme.challenges.ChallengeResponse`, + where responses are required to be returned in + the same order as corresponding input challenges + + :raises .PluginError: If some or all challenges cannot be performed + + """ + + @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 + perform, even if perform exited abnormally. + + :param list achalls: Non-empty (guaranteed) list of + :class:`~certbot.achallenges.AnnotatedChallenge` + instances, a subset of those previously passed to :func:`perform`. + + :raises PluginError: if original configuration cannot be restored + + """ + + class IInstaller(IPlugin): pass From b114f86402db92a10901a821254eed5f616a5303 Mon Sep 17 00:00:00 2001 From: Adrien Ferrand Date: Sun, 9 May 2021 14:14:53 +0200 Subject: [PATCH 04/47] Remove zope interfaces from plugins --- certbot-apache/certbot_apache/_internal/configurator.py | 7 +------ .../certbot_dns_cloudflare/_internal/dns_cloudflare.py | 4 ---- .../certbot_dns_cloudxns/_internal/dns_cloudxns.py | 4 ---- .../_internal/dns_digitalocean.py | 4 ---- .../certbot_dns_dnsimple/_internal/dns_dnsimple.py | 4 ---- .../certbot_dns_dnsmadeeasy/_internal/dns_dnsmadeeasy.py | 4 ---- .../certbot_dns_gehirn/_internal/dns_gehirn.py | 5 +---- .../certbot_dns_google/_internal/dns_google.py | 4 ---- .../certbot_dns_linode/_internal/dns_linode.py | 5 +---- .../certbot_dns_luadns/_internal/dns_luadns.py | 4 ---- .../certbot_dns_nsone/_internal/dns_nsone.py | 4 ---- certbot-dns-ovh/certbot_dns_ovh/_internal/dns_ovh.py | 4 ---- .../certbot_dns_rfc2136/_internal/dns_rfc2136.py | 5 +---- .../certbot_dns_route53/_internal/dns_route53.py | 4 ---- certbot-dns-route53/certbot_dns_route53/authenticator.py | 5 ----- .../certbot_dns_sakuracloud/_internal/dns_sakuracloud.py | 4 ---- certbot-nginx/certbot_nginx/_internal/configurator.py | 4 +--- certbot/certbot/_internal/cli/cli_utils.py | 6 +++--- certbot/certbot/_internal/plugins/manual.py | 5 ----- certbot/certbot/_internal/plugins/null.py | 9 ++++----- certbot/certbot/_internal/plugins/standalone.py | 5 +---- certbot/certbot/_internal/plugins/webroot.py | 4 ---- certbot/certbot/plugins/common.py | 9 ++++----- certbot/certbot/plugins/dns_common.py | 6 +----- 24 files changed, 18 insertions(+), 101 deletions(-) diff --git a/certbot-apache/certbot_apache/_internal/configurator.py b/certbot-apache/certbot_apache/_internal/configurator.py index 0185a7675b1..cd372a72b4e 100644 --- a/certbot-apache/certbot_apache/_internal/configurator.py +++ b/certbot-apache/certbot_apache/_internal/configurator.py @@ -15,8 +15,6 @@ from typing import Set from typing import Union -import zope.interface - from acme import challenges from certbot import errors from certbot import interfaces @@ -119,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(interfaces.Authenticator, common.Installer): """Apache configurator. :ivar config: Configuration. 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-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-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-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-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-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-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-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-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-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-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-rfc2136/certbot_dns_rfc2136/_internal/dns_rfc2136.py b/certbot-dns-rfc2136/certbot_dns_rfc2136/_internal/dns_rfc2136.py index 28fd27eec07..9fcd7c5b5ad 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,8 +20,7 @@ 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 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-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-nginx/certbot_nginx/_internal/configurator.py b/certbot-nginx/certbot_nginx/_internal/configurator.py index 9fb81efaabf..a7a6be094a2 100644 --- a/certbot-nginx/certbot_nginx/_internal/configurator.py +++ b/certbot-nginx/certbot_nginx/_internal/configurator.py @@ -43,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(interfaces.Authenticator, common.Installer): """Nginx configurator. .. todo:: Add proper support for comments in the config. Currently, diff --git a/certbot/certbot/_internal/cli/cli_utils.py b/certbot/certbot/_internal/cli/cli_utils.py index 8060b5e2148..e0e460fefbe 100644 --- a/certbot/certbot/_internal/cli/cli_utils.py +++ b/certbot/certbot/_internal/cli/cli_utils.py @@ -1,6 +1,7 @@ """Certbot command line util function""" import argparse import copy +import inspect import zope.interface.interface # pylint: disable=unused-import @@ -59,11 +60,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/plugins/manual.py b/certbot/certbot/_internal/plugins/manual.py index cc878dee512..a965f1bc369 100644 --- a/certbot/certbot/_internal/plugins/manual.py +++ b/certbot/certbot/_internal/plugins/manual.py @@ -1,12 +1,9 @@ """Manual authenticator plugin""" from typing import Dict -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 services from certbot import util @@ -16,8 +13,6 @@ from certbot.plugins import common -@zope.interface.implementer(interfaces.IAuthenticator) -@zope.interface.provider(interfaces.IPluginFactory) class Authenticator(common.Plugin): """Manual authenticator diff --git a/certbot/certbot/_internal/plugins/null.py b/certbot/certbot/_internal/plugins/null.py index 5ab2f4a04df..e3872205b37 100644 --- a/certbot/certbot/_internal/plugins/null.py +++ b/certbot/certbot/_internal/plugins/null.py @@ -1,22 +1,21 @@ """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): """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/standalone.py b/certbot/certbot/_internal/plugins/standalone.py index cc3cbdb69b2..5882fabb0af 100644 --- a/certbot/certbot/_internal/plugins/standalone.py +++ b/certbot/certbot/_internal/plugins/standalone.py @@ -10,13 +10,11 @@ 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 import services from certbot.plugins import common @@ -28,6 +26,7 @@ Set[achallenges.KeyAuthorizationAnnotatedChallenge] ] + class ServerManager: """Standalone servers manager. @@ -106,8 +105,6 @@ def running(self): return self._instances.copy() -@zope.interface.implementer(interfaces.IAuthenticator) -@zope.interface.provider(interfaces.IPluginFactory) class Authenticator(common.Plugin): """Standalone Authenticator. diff --git a/certbot/certbot/_internal/plugins/webroot.py b/certbot/certbot/_internal/plugins/webroot.py index 08bae388af3..c801134a767 100644 --- a/certbot/certbot/_internal/plugins/webroot.py +++ b/certbot/certbot/_internal/plugins/webroot.py @@ -8,11 +8,9 @@ from typing import List from typing import Set -import zope.interface from acme import challenges from certbot import errors -from certbot import interfaces from certbot import services from certbot._internal import cli from certbot.achallenges import KeyAuthorizationAnnotatedChallenge as AnnotatedChallenge @@ -27,8 +25,6 @@ logger = logging.getLogger(__name__) -@zope.interface.implementer(interfaces.IAuthenticator) -@zope.interface.provider(interfaces.IPluginFactory) class Authenticator(common.Plugin): """Webroot Authenticator.""" diff --git a/certbot/certbot/plugins/common.py b/certbot/certbot/plugins/common.py index 4c33acbab37..2cd0aba5126 100644 --- a/certbot/certbot/plugins/common.py +++ b/certbot/certbot/plugins/common.py @@ -3,21 +3,21 @@ import re import shutil import tempfile +from abc import ABCMeta from typing import List 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.plugins.storage import PluginStorage +from certbot.interfaces import Plugin as AbstractPlugin logger = logging.getLogger(__name__) @@ -39,8 +39,7 @@ 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): """Generic plugin.""" # provider is not inherited, subclasses must define it on their own # @zope.interface.provider(interfaces.IPluginFactory) @@ -98,7 +97,7 @@ def conf(self, var): return getattr(self.config, self.dest(var)) -class Installer(Plugin): +class Installer(Plugin, metaclass=ABCMeta): """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 4459ac0fa58..1d5219073a2 100644 --- a/certbot/certbot/plugins/dns_common.py +++ b/certbot/certbot/plugins/dns_common.py @@ -5,11 +5,9 @@ from time import sleep import configobj -import zope.interface from acme import challenges from certbot import errors -from certbot import interfaces from certbot.compat import filesystem from certbot.compat import os from certbot.display import ops @@ -19,9 +17,7 @@ logger = logging.getLogger(__name__) -@zope.interface.implementer(interfaces.IAuthenticator) -@zope.interface.provider(interfaces.IPluginFactory) -class DNSAuthenticator(common.Plugin): +class DNSAuthenticator(common.Plugin, metaclass=abc.ABCMeta): """Base class for DNS Authenticators""" def __init__(self, config, name): From 960c571e2e8ea37453c52126f73bc4bd2d1ad4ba Mon Sep 17 00:00:00 2001 From: Adrien Ferrand Date: Sun, 9 May 2021 14:21:12 +0200 Subject: [PATCH 05/47] Re-enable delegation for simplicity --- certbot/certbot/_internal/configuration.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/certbot/certbot/_internal/configuration.py b/certbot/certbot/_internal/configuration.py index aff0f456cc0..1da8364a984 100644 --- a/certbot/certbot/_internal/configuration.py +++ b/certbot/certbot/_internal/configuration.py @@ -50,6 +50,14 @@ 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 From 2543e7895056f5d981c1a78627bf65de00301ffa Mon Sep 17 00:00:00 2001 From: Adrien Ferrand Date: Sun, 9 May 2021 14:24:39 +0200 Subject: [PATCH 06/47] Fix interfaces declaration --- certbot/certbot/_internal/plugins/disco.py | 7 ++++--- certbot/certbot/_internal/plugins/manual.py | 4 ++-- certbot/certbot/_internal/plugins/null.py | 3 ++- .../certbot/_internal/plugins/selection.py | 20 ++++++++++--------- .../certbot/_internal/plugins/standalone.py | 4 ++-- certbot/certbot/_internal/plugins/webroot.py | 4 ++-- certbot/certbot/interfaces.py | 1 + certbot/certbot/plugins/common.py | 6 +++--- certbot/certbot/plugins/dns_common.py | 4 ++-- 9 files changed, 29 insertions(+), 24 deletions(-) diff --git a/certbot/certbot/_internal/plugins/disco.py b/certbot/certbot/_internal/plugins/disco.py index c9524c324e6..a7d127260e5 100644 --- a/certbot/certbot/_internal/plugins/disco.py +++ b/certbot/certbot/_internal/plugins/disco.py @@ -133,7 +133,7 @@ def verify(self, ifaces): for iface in ifaces: # zope.interface.providedBy(plugin) # 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 during init(). + # So effectively the checks have been done when the plugin has been initialized. if issubclass(iface, zope.interface.Interface): try: zope.interface.verify.verifyObject(iface, self.init()) @@ -201,8 +201,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), ] diff --git a/certbot/certbot/_internal/plugins/manual.py b/certbot/certbot/_internal/plugins/manual.py index a965f1bc369..f2f612226af 100644 --- a/certbot/certbot/_internal/plugins/manual.py +++ b/certbot/certbot/_internal/plugins/manual.py @@ -2,7 +2,7 @@ from typing import Dict from acme import challenges -from certbot import achallenges +from certbot import achallenges, interfaces from certbot import errors from certbot import reverter from certbot import services @@ -13,7 +13,7 @@ from certbot.plugins import common -class Authenticator(common.Plugin): +class Authenticator(interfaces.Authenticator, common.Plugin): """Manual authenticator This plugin allows the user to perform the domain validation diff --git a/certbot/certbot/_internal/plugins/null.py b/certbot/certbot/_internal/plugins/null.py index e3872205b37..ac42b46695d 100644 --- a/certbot/certbot/_internal/plugins/null.py +++ b/certbot/certbot/_internal/plugins/null.py @@ -1,12 +1,13 @@ """Null plugin.""" import logging +from certbot import interfaces from certbot.plugins import common logger = logging.getLogger(__name__) -class Installer(common.Plugin): +class Installer(interfaces.Installer, common.Plugin): """Null installer.""" description = "Null Installer" diff --git a/certbot/certbot/_internal/plugins/selection.py b/certbot/certbot/_internal/plugins/selection.py index 68770f19f78..1170e1639f4 100644 --- a/certbot/certbot/_internal/plugins/selection.py +++ b/certbot/certbot/_internal/plugins/selection.py @@ -11,30 +11,32 @@ logger = logging.getLogger(__name__) +# TODO: Replace the Zope interfaces (once removed) by ABCs in pick_* functions. + + 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]) + (interfaces.IAuthenticator, interfaces.IInstaller)) 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, interfaces.Installer]) + config, default, plugins, question, (interfaces.IInstaller,)) 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, interfaces.Authenticator]) + config, default, plugins, question, (interfaces.IAuthenticator,)) def get_unprepared_installer(config, plugins): diff --git a/certbot/certbot/_internal/plugins/standalone.py b/certbot/certbot/_internal/plugins/standalone.py index 5882fabb0af..32dbc3c3921 100644 --- a/certbot/certbot/_internal/plugins/standalone.py +++ b/certbot/certbot/_internal/plugins/standalone.py @@ -13,7 +13,7 @@ from acme import challenges from acme import standalone as acme_standalone -from certbot import achallenges +from certbot import achallenges, interfaces from certbot import errors from certbot import services from certbot.plugins import common @@ -105,7 +105,7 @@ def running(self): return self._instances.copy() -class Authenticator(common.Plugin): +class Authenticator(interfaces.Authenticator, common.Plugin): """Standalone Authenticator. This authenticator creates its own ephemeral TCP listener on the diff --git a/certbot/certbot/_internal/plugins/webroot.py b/certbot/certbot/_internal/plugins/webroot.py index c801134a767..b62e359f77f 100644 --- a/certbot/certbot/_internal/plugins/webroot.py +++ b/certbot/certbot/_internal/plugins/webroot.py @@ -10,7 +10,7 @@ from acme import challenges -from certbot import errors +from certbot import errors, interfaces from certbot import services from certbot._internal import cli from certbot.achallenges import KeyAuthorizationAnnotatedChallenge as AnnotatedChallenge @@ -25,7 +25,7 @@ logger = logging.getLogger(__name__) -class Authenticator(common.Plugin): +class Authenticator(interfaces.Authenticator, common.Plugin): """Webroot Authenticator.""" description = "Place files in webroot directory" diff --git a/certbot/certbot/interfaces.py b/certbot/certbot/interfaces.py index 95f8f2d3900..8968d63414a 100644 --- a/certbot/certbot/interfaces.py +++ b/certbot/certbot/interfaces.py @@ -387,6 +387,7 @@ class IInstaller(IPlugin): pass +@zope.interface.implementer(IInstaller) class Installer(Plugin): """Generic Certbot Installer Interface. diff --git a/certbot/certbot/plugins/common.py b/certbot/certbot/plugins/common.py index 2cd0aba5126..78dce5d718c 100644 --- a/certbot/certbot/plugins/common.py +++ b/certbot/certbot/plugins/common.py @@ -9,7 +9,7 @@ from josepy import util as jose_util import pkg_resources -from certbot import achallenges +from certbot import achallenges, interfaces from certbot import crypto_util from certbot import errors from certbot import reverter @@ -17,7 +17,7 @@ from certbot.compat import filesystem from certbot.compat import os from certbot.plugins.storage import PluginStorage -from certbot.interfaces import Plugin as AbstractPlugin +from certbot.interfaces import Plugin as AbstractPlugin, Installer as AbstractInstaller logger = logging.getLogger(__name__) @@ -97,7 +97,7 @@ def conf(self, var): return getattr(self.config, self.dest(var)) -class Installer(Plugin, metaclass=ABCMeta): +class Installer(AbstractInstaller, Plugin, metaclass=ABCMeta): """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 1d5219073a2..0b0b2028ca6 100644 --- a/certbot/certbot/plugins/dns_common.py +++ b/certbot/certbot/plugins/dns_common.py @@ -7,7 +7,7 @@ import configobj from acme import challenges -from certbot import errors +from certbot import errors, interfaces from certbot.compat import filesystem from certbot.compat import os from certbot.display import ops @@ -17,7 +17,7 @@ logger = logging.getLogger(__name__) -class DNSAuthenticator(common.Plugin, metaclass=abc.ABCMeta): +class DNSAuthenticator(interfaces.Authenticator, common.Plugin, metaclass=abc.ABCMeta): """Base class for DNS Authenticators""" def __init__(self, config, name): From bc05d704c531f9546364312dbfed0035b0ac1ffb Mon Sep 17 00:00:00 2001 From: Adrien Ferrand Date: Sun, 9 May 2021 15:16:44 +0200 Subject: [PATCH 07/47] Remove interface implementer --- .../certbot_apache/_internal/override_arch.py | 4 ---- .../certbot_apache/_internal/override_centos.py | 4 ---- .../certbot_apache/_internal/override_darwin.py | 4 ---- .../certbot_apache/_internal/override_debian.py | 4 ---- .../certbot_apache/_internal/override_fedora.py | 4 ---- .../certbot_apache/_internal/override_gentoo.py | 4 ---- .../certbot_apache/_internal/override_suse.py | 4 ---- certbot/examples/plugins/certbot_example_plugins.py | 10 ++-------- 8 files changed, 2 insertions(+), 36 deletions(-) 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..516bafcf28b 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""" 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 7f12f4bbce7..d112de125b0 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..20e9660ce5a 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""" diff --git a/certbot-apache/certbot_apache/_internal/override_gentoo.py b/certbot-apache/certbot_apache/_internal/override_gentoo.py index 1b86c925ed1..5557d67c44e 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""" 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/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" From edc5acfc8d804d61c56ed614f2d7b4ac2dd72596 Mon Sep 17 00:00:00 2001 From: Adrien Ferrand Date: Sun, 9 May 2021 15:18:54 +0200 Subject: [PATCH 08/47] Interfaces ordering --- certbot-apache/certbot_apache/_internal/configurator.py | 2 +- certbot-nginx/certbot_nginx/_internal/configurator.py | 2 +- certbot/certbot/_internal/plugins/manual.py | 2 +- certbot/certbot/_internal/plugins/null.py | 2 +- certbot/certbot/_internal/plugins/standalone.py | 2 +- certbot/certbot/_internal/plugins/webroot.py | 2 +- certbot/certbot/plugins/dns_common.py | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/certbot-apache/certbot_apache/_internal/configurator.py b/certbot-apache/certbot_apache/_internal/configurator.py index cd372a72b4e..afd8e6cb31f 100644 --- a/certbot-apache/certbot_apache/_internal/configurator.py +++ b/certbot-apache/certbot_apache/_internal/configurator.py @@ -117,7 +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() -class ApacheConfigurator(interfaces.Authenticator, common.Installer): +class ApacheConfigurator(common.Installer, interfaces.Authenticator): """Apache configurator. :ivar config: Configuration. diff --git a/certbot-nginx/certbot_nginx/_internal/configurator.py b/certbot-nginx/certbot_nginx/_internal/configurator.py index a7a6be094a2..71ad370e9d2 100644 --- a/certbot-nginx/certbot_nginx/_internal/configurator.py +++ b/certbot-nginx/certbot_nginx/_internal/configurator.py @@ -43,7 +43,7 @@ logger = logging.getLogger(__name__) -class NginxConfigurator(interfaces.Authenticator, common.Installer): +class NginxConfigurator(common.Installer, interfaces.Authenticator): """Nginx configurator. .. todo:: Add proper support for comments in the config. Currently, diff --git a/certbot/certbot/_internal/plugins/manual.py b/certbot/certbot/_internal/plugins/manual.py index f2f612226af..d71df2d2ace 100644 --- a/certbot/certbot/_internal/plugins/manual.py +++ b/certbot/certbot/_internal/plugins/manual.py @@ -13,7 +13,7 @@ from certbot.plugins import common -class Authenticator(interfaces.Authenticator, common.Plugin): +class Authenticator(common.Plugin, interfaces.Authenticator): """Manual authenticator This plugin allows the user to perform the domain validation diff --git a/certbot/certbot/_internal/plugins/null.py b/certbot/certbot/_internal/plugins/null.py index ac42b46695d..b800c5c39de 100644 --- a/certbot/certbot/_internal/plugins/null.py +++ b/certbot/certbot/_internal/plugins/null.py @@ -7,7 +7,7 @@ logger = logging.getLogger(__name__) -class Installer(interfaces.Installer, common.Plugin): +class Installer(common.Plugin, interfaces.Installer): """Null installer.""" description = "Null Installer" diff --git a/certbot/certbot/_internal/plugins/standalone.py b/certbot/certbot/_internal/plugins/standalone.py index 32dbc3c3921..eaddcce1706 100644 --- a/certbot/certbot/_internal/plugins/standalone.py +++ b/certbot/certbot/_internal/plugins/standalone.py @@ -105,7 +105,7 @@ def running(self): return self._instances.copy() -class Authenticator(interfaces.Authenticator, common.Plugin): +class Authenticator(common.Plugin, interfaces.Authenticator): """Standalone Authenticator. This authenticator creates its own ephemeral TCP listener on the diff --git a/certbot/certbot/_internal/plugins/webroot.py b/certbot/certbot/_internal/plugins/webroot.py index b62e359f77f..7585850aa22 100644 --- a/certbot/certbot/_internal/plugins/webroot.py +++ b/certbot/certbot/_internal/plugins/webroot.py @@ -25,7 +25,7 @@ logger = logging.getLogger(__name__) -class Authenticator(interfaces.Authenticator, common.Plugin): +class Authenticator(common.Plugin, interfaces.Authenticator): """Webroot Authenticator.""" description = "Place files in webroot directory" diff --git a/certbot/certbot/plugins/dns_common.py b/certbot/certbot/plugins/dns_common.py index 0b0b2028ca6..b371e9538db 100644 --- a/certbot/certbot/plugins/dns_common.py +++ b/certbot/certbot/plugins/dns_common.py @@ -17,7 +17,7 @@ logger = logging.getLogger(__name__) -class DNSAuthenticator(interfaces.Authenticator, common.Plugin, metaclass=abc.ABCMeta): +class DNSAuthenticator(common.Plugin, interfaces.Authenticator, metaclass=abc.ABCMeta): """Base class for DNS Authenticators""" def __init__(self, config, name): From f836a40cbb1292765962a07f6b4ebc6e28229275 Mon Sep 17 00:00:00 2001 From: Adrien Ferrand Date: Sun, 9 May 2021 15:52:52 +0200 Subject: [PATCH 09/47] Extract zope logic from discovery --- certbot/certbot/_internal/plugins/disco.py | 84 +++++++++++++++---- .../certbot/_internal/plugins/selection.py | 8 +- certbot/certbot/interfaces.py | 7 -- 3 files changed, 70 insertions(+), 29 deletions(-) diff --git a/certbot/certbot/_internal/plugins/disco.py b/certbot/certbot/_internal/plugins/disco.py index a7d127260e5..b3ae376b412 100644 --- a/certbot/certbot/_internal/plugins/disco.py +++ b/certbot/certbot/_internal/plugins/disco.py @@ -1,4 +1,5 @@ """Utilities for plugins discovery and selection.""" +import warnings from collections.abc import Mapping import itertools import logging @@ -16,6 +17,8 @@ from certbot._internal import constants from certbot.compat import os from certbot.errors import Error +from certbot.interfaces import IPlugin, IAuthenticator, IInstaller, IPluginFactory +from zope.interface import Interface logger = logging.getLogger(__name__) @@ -131,18 +134,8 @@ def verify(self, ifaces): if not self.initialized: raise ValueError("Plugin is not initialized.") for iface in ifaces: # zope.interface.providedBy(plugin) - # 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. - if issubclass(iface, zope.interface.Interface): - 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) - return False + if not _verify(self.init(), self.plugin_cls, iface): + return False return True @@ -266,12 +259,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 (issubclass(plugin_ep.plugin_cls, interfaces.Plugin) - or 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 @@ -348,8 +340,64 @@ def __str__(self): 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 + + def _implements(target_class: Type[interfaces.Plugin], iface: Type) -> bool: - if issubclass(iface, zope.interface.Interface): - return cast(iface, zope.interface.Interface).implementedBy(target_class) + if issubclass(target_class, iface): + return True - return issubclass(target_class, iface) + 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[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/selection.py b/certbot/certbot/_internal/plugins/selection.py index 1170e1639f4..1edcf6b8ab6 100644 --- a/certbot/certbot/_internal/plugins/selection.py +++ b/certbot/certbot/_internal/plugins/selection.py @@ -21,14 +21,14 @@ def pick_configurator( """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( @@ -36,7 +36,7 @@ def pick_authenticator( "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 +56,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 " diff --git a/certbot/certbot/interfaces.py b/certbot/certbot/interfaces.py index 8968d63414a..451c6ff85bc 100644 --- a/certbot/certbot/interfaces.py +++ b/certbot/certbot/interfaces.py @@ -47,7 +47,6 @@ class IConfig(zope.interface.Interface): pass -@zope.interface.implementer(IConfig) class Config(metaclass=ABCMeta): """Certbot user-supplied configuration. @@ -225,8 +224,6 @@ class IPlugin(zope.interface.Interface): pass -@zope.interface.provider(IPluginFactory) -@zope.interface.implementer(IPlugin) class Plugin(metaclass=ABCMeta): """Certbot plugin. @@ -323,7 +320,6 @@ class IAuthenticator(IPlugin): pass -@zope.interface.implementer(IAuthenticator) class Authenticator(Plugin): """Generic Certbot Authenticator. @@ -387,7 +383,6 @@ class IInstaller(IPlugin): pass -@zope.interface.implementer(IInstaller) class Installer(Plugin): """Generic Certbot Installer Interface. @@ -518,7 +513,6 @@ class IDisplay(zope.interface.Interface): pass -@zope.interface.implementer(IDisplay) class Display(metaclass=ABCMeta): """Generic display.""" # see https://github.com/certbot/certbot/issues/3915 @@ -675,7 +669,6 @@ class IReporter(zope.interface.Interface): pass -@zope.interface.implementer(IReporter) class Reporter(metaclass=ABCMeta): """Interface to collect and display information to the user.""" From 1235febe668a6d77a87b0d6cc3ed17c225442444 Mon Sep 17 00:00:00 2001 From: Adrien Ferrand Date: Sun, 9 May 2021 15:56:44 +0200 Subject: [PATCH 10/47] Cleanup imports --- certbot-nginx/certbot_nginx/_internal/configurator.py | 1 - certbot/certbot/_internal/cli/cli_utils.py | 2 -- certbot/certbot/_internal/plugins/disco.py | 3 +-- certbot/certbot/_internal/plugins/webroot.py | 1 - 4 files changed, 1 insertion(+), 6 deletions(-) diff --git a/certbot-nginx/certbot_nginx/_internal/configurator.py b/certbot-nginx/certbot_nginx/_internal/configurator.py index 71ad370e9d2..97e0142f5d6 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 diff --git a/certbot/certbot/_internal/cli/cli_utils.py b/certbot/certbot/_internal/cli/cli_utils.py index e0e460fefbe..50e093310da 100644 --- a/certbot/certbot/_internal/cli/cli_utils.py +++ b/certbot/certbot/_internal/cli/cli_utils.py @@ -3,8 +3,6 @@ import copy import inspect -import zope.interface.interface # pylint: disable=unused-import - from acme import challenges from certbot import errors from certbot import interfaces diff --git a/certbot/certbot/_internal/plugins/disco.py b/certbot/certbot/_internal/plugins/disco.py index b3ae376b412..6285f171c2c 100644 --- a/certbot/certbot/_internal/plugins/disco.py +++ b/certbot/certbot/_internal/plugins/disco.py @@ -4,7 +4,7 @@ import itertools import logging import sys -from typing import Dict, Type, cast +from typing import Dict, Type from typing import Optional from typing import Union @@ -17,7 +17,6 @@ from certbot._internal import constants from certbot.compat import os from certbot.errors import Error -from certbot.interfaces import IPlugin, IAuthenticator, IInstaller, IPluginFactory from zope.interface import Interface logger = logging.getLogger(__name__) diff --git a/certbot/certbot/_internal/plugins/webroot.py b/certbot/certbot/_internal/plugins/webroot.py index 7585850aa22..59d1a5c7e2e 100644 --- a/certbot/certbot/_internal/plugins/webroot.py +++ b/certbot/certbot/_internal/plugins/webroot.py @@ -8,7 +8,6 @@ from typing import List from typing import Set - from acme import challenges from certbot import errors, interfaces from certbot import services From 376f735dd4cbaea62bc2774c8d4bbe5cc8304de3 Mon Sep 17 00:00:00 2001 From: Adrien Ferrand Date: Sun, 9 May 2021 19:50:36 +0200 Subject: [PATCH 11/47] Fixing tests --- certbot/certbot/tests/util.py | 39 +++++++++++++++++++++++++ certbot/tests/plugins/common_test.py | 11 +++++-- certbot/tests/plugins/disco_test.py | 18 ++++++------ certbot/tests/plugins/selection_test.py | 6 ++-- certbot/tests/plugins/storage_test.py | 5 ++-- 5 files changed, 62 insertions(+), 17 deletions(-) diff --git a/certbot/certbot/tests/util.py b/certbot/certbot/tests/util.py index 730c0bd3a1f..a41ff54b920 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 @@ -41,6 +45,41 @@ 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.""" return pkg_resources.resource_filename( diff --git a/certbot/tests/plugins/common_test.py b/certbot/tests/plugins/common_test.py index 6eb5dbfce19..d41aed27551 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) @@ -90,9 +97,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..3b87695280d 100644 --- a/certbot/tests/plugins/disco_test.py +++ b/certbot/tests/plugins/disco_test.py @@ -95,10 +95,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 +135,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/selection_test.py b/certbot/tests/plugins/selection_test.py index fda0214cf8b..60f3285577a 100644 --- a/certbot/tests/plugins/selection_test.py +++ b/certbot/tests/plugins/selection_test.py @@ -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): diff --git a/certbot/tests/plugins/storage_test.py b/certbot/tests/plugins/storage_test.py index d01845510f2..e57f438e553 100644 --- a/certbot/tests/plugins/storage_test.py +++ b/certbot/tests/plugins/storage_test.py @@ -1,6 +1,7 @@ """Tests for certbot.plugins.storage.PluginStorage""" import json import unittest +from typing import Optional, List, Iterable try: import mock @@ -10,7 +11,6 @@ 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 @@ -19,7 +19,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 +101,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 From e738f81be1da31cc4cb6e0af599aae31edd0efd2 Mon Sep 17 00:00:00 2001 From: Adrien Ferrand Date: Sun, 9 May 2021 20:02:57 +0200 Subject: [PATCH 12/47] Fix main_test --- certbot/certbot/tests/util.py | 18 ++++++++++++------ certbot/tests/main_test.py | 8 ++++---- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/certbot/certbot/tests/util.py b/certbot/certbot/tests/util.py index a41ff54b920..15d19d83a8d 100644 --- a/certbot/certbot/tests/util.py +++ b/certbot/certbot/tests/util.py @@ -1,4 +1,5 @@ """Test utilities.""" +import inspect from importlib import reload as reload_module import io import logging @@ -324,10 +325,12 @@ def __setattr__(self, name, value): def _create_display_service_mock(): display = FreezableMock() # Use pylint code for disable to keep on single line under line length limit - for name in interfaces.IDisplay.names(): # pylint: E1120 - 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) @@ -348,14 +351,17 @@ def mock_method(*args, **kwargs): display = FreezableMock() # Use pylint code for disable to keep on single line under line length limit - for name in interfaces.IDisplay.names(): # pylint: E1120 - 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: + print(method) + if method == 'notification': frozen_mock = FreezableMock(frozen=True, func=_write_msg) 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) diff --git a/certbot/tests/main_test.py b/certbot/tests/main_test.py index e6b08451b32..8238f745273 100644 --- a/certbot/tests/main_test.py +++ b/certbot/tests/main_test.py @@ -857,7 +857,7 @@ 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() @@ -872,7 +872,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): @@ -894,7 +894,7 @@ 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() @@ -912,7 +912,7 @@ 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() From e0afe58a252aff634988717f61dcfe850d592574 Mon Sep 17 00:00:00 2001 From: Adrien Ferrand Date: Sun, 9 May 2021 20:13:36 +0200 Subject: [PATCH 13/47] Finish certbot unit tests --- certbot/certbot/_internal/configuration.py | 2 +- certbot/tests/renewal_test.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/certbot/certbot/_internal/configuration.py b/certbot/certbot/_internal/configuration.py index 1da8364a984..989475c5fae 100644 --- a/certbot/certbot/_internal/configuration.py +++ b/certbot/certbot/_internal/configuration.py @@ -62,7 +62,7 @@ def __setattr__(self, name, value): @property def server(self) -> str: - return self.namespace.service + return self.namespace.server @property def email(self) -> str: diff --git a/certbot/tests/renewal_test.py b/certbot/tests/renewal_test.py index 8e2d2ef2497..62f04a82291 100644 --- a/certbot/tests/renewal_test.py +++ b/certbot/tests/renewal_test.py @@ -178,6 +178,8 @@ def test_ancient_server_renewal_conf(self, mock_set_by_cli): from certbot._internal import constants self.config.server = None mock_set_by_cli.return_value = False + print(self.config) + print(self.config.server) self._call(self.config, {'server': constants.V1_URI}) self.assertEqual(self.config.server, constants.CLI_DEFAULTS['server']) From 8ae22f55802da73c33a8a84bfc06e4196f624576 Mon Sep 17 00:00:00 2001 From: Adrien Ferrand Date: Sun, 9 May 2021 21:08:36 +0200 Subject: [PATCH 14/47] Fix lint --- certbot/certbot/_internal/plugins/disco.py | 12 +++-- certbot/certbot/display/util.py | 60 ++++++++++++++-------- certbot/certbot/interfaces.py | 60 ++++++++++++---------- certbot/certbot/plugins/common.py | 13 ++--- certbot/certbot/services.py | 8 +-- certbot/certbot/tests/util.py | 1 - 6 files changed, 89 insertions(+), 65 deletions(-) diff --git a/certbot/certbot/_internal/plugins/disco.py b/certbot/certbot/_internal/plugins/disco.py index 6285f171c2c..448848fe7bc 100644 --- a/certbot/certbot/_internal/plugins/disco.py +++ b/certbot/certbot/_internal/plugins/disco.py @@ -1,15 +1,17 @@ """Utilities for plugins discovery and selection.""" -import warnings from collections.abc import Mapping import itertools import logging import sys -from typing import Dict, Type +from typing import Dict from typing import Optional +from typing import Type from typing import Union +import warnings import pkg_resources import zope.interface +from zope.interface import Interface import zope.interface.verify from certbot import errors @@ -17,7 +19,6 @@ from certbot._internal import constants from certbot.compat import os from certbot.errors import Error -from zope.interface import Interface logger = logging.getLogger(__name__) @@ -348,6 +349,8 @@ def _provides(target_class: Type[interfaces.Plugin], iface: Type) -> bool: "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): @@ -399,4 +402,5 @@ def _verify(target_instance: interfaces.Plugin, target_class: Type[interfaces.Pl logger.debug( "%s implements %s but object does not verify: %s", target_class, zope_iface.__name__, error, exc_info=True) - return False + + return False diff --git a/certbot/certbot/display/util.py b/certbot/certbot/display/util.py index 5f3af7c678c..46a781b4c4b 100644 --- a/certbot/certbot/display/util.py +++ b/certbot/certbot/display/util.py @@ -153,9 +153,8 @@ def notification(self, message, pause=True, 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): + 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 @@ -165,6 +164,9 @@ def menu(self, message, choices, ok_label=None, cancel_label=None, # pylint: di :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 @@ -187,7 +189,7 @@ def menu(self, message, choices, ok_label=None, cancel_label=None, # pylint: di return code, selection - 1 def input(self, message, default=None, - cli_flag=None, force_interactive=False, **unused_kwargs): + cli_flag=None, force_interactive=False): """Accept input from the user. :param str message: message to display to the user @@ -214,7 +216,7 @@ def input(self, message, default=None, return OK, ans def yesno(self, message, yes_label="Yes", no_label="No", default=None, - cli_flag=None, force_interactive=False, **unused_kwargs): + 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 @@ -256,7 +258,7 @@ def yesno(self, message, yes_label="Yes", no_label="No", default=None, return False def checklist(self, message, tags, default=None, - cli_flag=None, force_interactive=False, **unused_kwargs): + cli_flag=None, force_interactive=False): """Display a checklist. :param str message: Message to display to user @@ -348,7 +350,7 @@ def _can_interact(self, force_interactive): return False def directory_select(self, message, default=None, cli_flag=None, - force_interactive=False, **unused_kwargs): + force_interactive=False): """Display a directory selection screen. :param str message: prompt to give the user @@ -483,7 +485,7 @@ def __init__(self, outfile, *unused_args, **unused_kwargs): self.outfile = outfile def _interaction_fail(self, message, cli_flag, extra=""): - "Error out in case of an attempt to interact in noninteractive mode" + """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: @@ -492,12 +494,14 @@ def _interaction_fail(self, message, cli_flag, extra=""): 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 + 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: The NoninteractiveDisplay waits for no keyboard + :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 """ @@ -515,7 +519,7 @@ def notification(self, message, pause=False, wrap=True, decorate=True, **unused_ self.outfile.flush() def menu(self, message, choices, ok_label=None, cancel_label=None, - help_label=None, default=None, cli_flag=None, **unused_kwargs): + help_label=None, default=None, cli_flag=None, force_interactive=False): # pylint: disable=unused-argument """Avoid displaying a menu. @@ -523,8 +527,12 @@ def menu(self, message, choices, ok_label=None, cancel_label=None, :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 dict kwargs: absorbs various irrelevant labelling arguments + :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 @@ -538,10 +546,13 @@ def menu(self, message, choices, ok_label=None, cancel_label=None, return OK, default - def input(self, message, default=None, cli_flag=None, **unused_kwargs): + 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 @@ -554,12 +565,16 @@ def input(self, message, default=None, cli_flag=None, **unused_kwargs): 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): + 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 dict kwargs: absorbs yes_label, no_label + :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" @@ -571,12 +586,14 @@ def yesno(self, message, yes_label=None, no_label=None, # pylint: disable=unuse return default def checklist(self, message, tags, default=None, - cli_flag=None, **unused_kwargs): + 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 dict kwargs: absorbs default_status arg + :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 @@ -589,7 +606,7 @@ def checklist(self, message, tags, default=None, return OK, default def directory_select(self, message, default=None, - cli_flag=None, **unused_kwargs): + cli_flag=None, force_interactive=False): """Simulate prompting the user for a directory. This function returns default if it is not ``None``, otherwise, @@ -600,6 +617,7 @@ def directory_select(self, message, default=None, :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 diff --git a/certbot/certbot/interfaces.py b/certbot/certbot/interfaces.py index 451c6ff85bc..b84cce27c15 100644 --- a/certbot/certbot/interfaces.py +++ b/certbot/certbot/interfaces.py @@ -1,12 +1,17 @@ """Certbot client interfaces.""" -from abc import ABCMeta, abstractmethod, abstractproperty +from abc import ABCMeta +from abc import abstractmethod from argparse import ArgumentParser -from typing import Optional, Iterable, List, Union, Tuple +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, ChallengeResponse +from acme.challenges import Challenge +from acme.challenges import ChallengeResponse from certbot.achallenges import AnnotatedChallenge @@ -43,8 +48,8 @@ def save(self, account, client): # pragma: no cover raise NotImplementedError() -class IConfig(zope.interface.Interface): - pass +class IConfig(zope.interface.Interface): # pylint: disable=inherit-non-class + """Deprecated, use certbot.interfaces.Config as ABC instead.""" class Config(metaclass=ABCMeta): @@ -216,12 +221,12 @@ def preferred_chain(self) -> str: """ -class IPluginFactory(zope.interface.Interface): - pass +class IPluginFactory(zope.interface.Interface): # pylint: disable=inherit-non-class + """Deprecated, use certbot.interfaces.Plugin as ABC instead.""" -class IPlugin(zope.interface.Interface): - pass +class IPlugin(zope.interface.Interface): # pylint: disable=inherit-non-class + """Deprecated, use certbot.interfaces.Plugin as ABC instead.""" class Plugin(metaclass=ABCMeta): @@ -316,8 +321,8 @@ def inject_parser_options(cls, parser: ArgumentParser, name: str) -> None: """ -class IAuthenticator(IPlugin): - pass +class IAuthenticator(IPlugin): # pylint: disable=inherit-non-class + """Deprecated, use certbot.interfaces.Authenticator as ABC instead.""" class Authenticator(Plugin): @@ -379,8 +384,8 @@ def cleanup(self, achalls: List[AnnotatedChallenge]) -> None: """ -class IInstaller(IPlugin): - pass +class IInstaller(IPlugin): # pylint: disable=inherit-non-class + """Deprecated, use certbot.interfaces.Installer as ABC instead.""" class Installer(Plugin): @@ -509,8 +514,8 @@ def restart(self) -> None: """ -class IDisplay(zope.interface.Interface): - pass +class IDisplay(zope.interface.Interface): # pylint: disable=inherit-non-class + """Deprecated, use certbot.interfaces.Display as ABC instead.""" class Display(metaclass=ABCMeta): @@ -519,7 +524,7 @@ class Display(metaclass=ABCMeta): @abstractmethod def notification(self, message: str, pause: bool, wrap: bool = True, - force_interactive: bool = False): + force_interactive: bool = False, decorate: bool = True): """Displays a string message :param str message: Message to display @@ -528,6 +533,8 @@ def notification(self, message: str, pause: bool, wrap: bool = True, :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 """ @@ -564,7 +571,7 @@ def menu(self, message: str, choices: Union[List[str], Tuple[str], str], """ @abstractmethod - def input(self, message: str, default: Optional[str] = None, cli_args: Optional[str] = None, + def input(self, message: str, default: Optional[str] = None, cli_flag: Optional[str] = None, force_interactive: bool = False) -> Tuple[str, int]: """Accept input from the user. @@ -573,7 +580,7 @@ def input(self, message: str, default: Optional[str] = None, cli_args: Optional[ :param str message: message to display to the user :param str default: default (non-interactive) response to prompt - :param str cli_args: to automate choice from the menu, eg "--redirect / --no-redirect" + :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 @@ -589,7 +596,7 @@ def input(self, message: str, default: Optional[str] = None, cli_args: Optional[ @abstractmethod def yesno(self, message: str, yes_label: str = "Yes", no_label: str = "No", - default: Optional[str] = None, cli_args: Optional[str] = None, + default: Optional[str] = None, cli_flag: Optional[str] = None, force_interactive: bool = False) -> bool: """Query the user with a yes/no question. @@ -602,7 +609,7 @@ def yesno(self, message: str, yes_label: str = "Yes", no_label: str = "No", :param str yes_label: label for Yes button :param str no_label: label for No button :param str default: default (non-interactive) choice from the menu - :param str cli_args: to automate choice from the menu, eg "--agree-tos" + :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 @@ -616,7 +623,7 @@ def yesno(self, message: str, yes_label: str = "Yes", no_label: str = "No", @abstractmethod def checklist(self, message: str, tags: List[str], default: Optional[str] = None, - cli_args: Optional[str] = None, + cli_flag: Optional[str] = None, force_interactive: bool = False) -> Tuple[int, List[str]]: """Allow for multiple selections from a menu. @@ -626,7 +633,7 @@ def checklist(self, message: str, tags: List[str], default: Optional[str] = None :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_args: to automate choice from the menu, eg "--domains" + :param str cli_flag: to automate choice from the menu, eg "--domains" :param bool force_interactive: True if it's safe to prompt the user because it won't cause any workflow regressions @@ -665,8 +672,8 @@ def directory_select(self, message: str, default: Optional[str] = None, """ -class IReporter(zope.interface.Interface): - pass +class IReporter(zope.interface.Interface): # pylint: disable=inherit-non-class + """Deprecated, use certbot.interfaces.Reporter as ABC instead.""" class Reporter(metaclass=ABCMeta): @@ -698,7 +705,7 @@ def print_messages(self) -> str: """Prints messages to the user and clears the message queue.""" -class RenewableCert(object, metaclass=ABCMeta): +class RenewableCert(metaclass=ABCMeta): """Interface to a certificate lineage.""" @property @@ -748,7 +755,6 @@ def lineagename(self): """ - @property @abstractmethod def names(self): """What are the subject names of this certificate? diff --git a/certbot/certbot/plugins/common.py b/certbot/certbot/plugins/common.py index 78dce5d718c..6b72220bb6b 100644 --- a/certbot/certbot/plugins/common.py +++ b/certbot/certbot/plugins/common.py @@ -9,7 +9,7 @@ from josepy import util as jose_util import pkg_resources -from certbot import achallenges, interfaces +from certbot import achallenges from certbot import crypto_util from certbot import errors from certbot import reverter @@ -39,14 +39,11 @@ def dest_namespace(name): r"^(([a-z0-9]|[a-z0-9][a-z0-9\-]*[a-z0-9])\.)*[a-z]+$", re.IGNORECASE) -class Plugin(AbstractPlugin, metaclass=ABCMeta): +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): @@ -97,7 +94,7 @@ def conf(self, var): return getattr(self.config, self.dest(var)) -class Installer(AbstractInstaller, Plugin, metaclass=ABCMeta): +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/services.py b/certbot/certbot/services.py index 0cb54e20d86..8aa9ecac012 100644 --- a/certbot/certbot/services.py +++ b/certbot/certbot/services.py @@ -1,9 +1,9 @@ """This module contains global singletons used throughout Certbot.""" -from typing import Optional, Any +from typing import Optional -from certbot.interfaces import IConfig, Config, Display, Reporter -from certbot.interfaces import IDisplay -from certbot.interfaces import IReporter +from certbot.interfaces import Config +from certbot.interfaces import Display +from certbot.interfaces import Reporter class _Services: diff --git a/certbot/certbot/tests/util.py b/certbot/certbot/tests/util.py index 15d19d83a8d..12c02dee325 100644 --- a/certbot/certbot/tests/util.py +++ b/certbot/certbot/tests/util.py @@ -1,5 +1,4 @@ """Test utilities.""" -import inspect from importlib import reload as reload_module import io import logging From a879e32198a44e37ca7896f253221d0184e5465c Mon Sep 17 00:00:00 2001 From: Adrien Ferrand Date: Sun, 9 May 2021 21:53:09 +0200 Subject: [PATCH 15/47] Various fixes thanks to mypy --- certbot/certbot/_internal/auth_handler.py | 2 +- certbot/certbot/_internal/cert_manager.py | 8 +++----- certbot/certbot/_internal/plugins/disco.py | 5 +---- certbot/certbot/_internal/plugins/manual.py | 4 ++-- certbot/certbot/_internal/renewal.py | 2 +- certbot/certbot/interfaces.py | 10 +++++----- certbot/certbot/tests/util.py | 16 ++++++++-------- 7 files changed, 21 insertions(+), 26 deletions(-) diff --git a/certbot/certbot/_internal/auth_handler.py b/certbot/certbot/_internal/auth_handler.py index beff2f31b31..807ba69dad6 100644 --- a/certbot/certbot/_internal/auth_handler.py +++ b/certbot/certbot/_internal/auth_handler.py @@ -70,7 +70,7 @@ def handle_authorizations(self, orderr, best_effort=False, max_retries=30): # If debug is on, wait for user input before starting the verification process. logger.info('Waiting for verification...') config = services.get_config() - if config.debug_challenges: + if getattr(config, "debug_challenges", False): notify = services.get_display().notification notify('Challenges loaded. Press continue to submit to CA. ' 'Pass "-v" for more info about challenges.', pause=True) diff --git a/certbot/certbot/_internal/cert_manager.py b/certbot/certbot/_internal/cert_manager.py index 320e817f52c..3ac4140e4a9 100644 --- a/certbot/certbot/_internal/cert_manager.py +++ b/certbot/certbot/_internal/cert_manager.py @@ -3,7 +3,7 @@ import logging import re import traceback -from typing import List +from typing import List, Union import pytz @@ -51,8 +51,7 @@ def rename_lineage(config): new_certname = config.new_certname if not new_certname: code, new_certname = disp.input( - "Enter the new name for certificate {0}".format(certname), - flag="--updated-cert-name", force_interactive=True) + "Enter the new name for certificate {0}".format(certname), force_interactive=True) if code != display_util.OK or not new_certname: raise errors.Error("User ended interaction.") @@ -309,8 +308,7 @@ 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] diff --git a/certbot/certbot/_internal/plugins/disco.py b/certbot/certbot/_internal/plugins/disco.py index 448848fe7bc..bcc30bdcb46 100644 --- a/certbot/certbot/_internal/plugins/disco.py +++ b/certbot/certbot/_internal/plugins/disco.py @@ -90,10 +90,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): diff --git a/certbot/certbot/_internal/plugins/manual.py b/certbot/certbot/_internal/plugins/manual.py index d71df2d2ace..c2506acbb5f 100644 --- a/certbot/certbot/_internal/plugins/manual.py +++ b/certbot/certbot/_internal/plugins/manual.py @@ -99,7 +99,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( @@ -107,7 +107,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: diff --git a/certbot/certbot/_internal/renewal.py b/certbot/certbot/_internal/renewal.py index 60e85f56074..f90a32475cb 100644 --- a/certbot/certbot/_internal/renewal.py +++ b/certbot/certbot/_internal/renewal.py @@ -185,7 +185,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): diff --git a/certbot/certbot/interfaces.py b/certbot/certbot/interfaces.py index b84cce27c15..34eafbbb4c7 100644 --- a/certbot/certbot/interfaces.py +++ b/certbot/certbot/interfaces.py @@ -523,7 +523,7 @@ class Display(metaclass=ABCMeta): # see https://github.com/certbot/certbot/issues/3915 @abstractmethod - def notification(self, message: str, pause: bool, wrap: bool = True, + def notification(self, message: str, pause: bool = False, wrap: bool = True, force_interactive: bool = False, decorate: bool = True): """Displays a string message @@ -596,7 +596,7 @@ def input(self, message: str, default: Optional[str] = None, cli_flag: Optional[ @abstractmethod def yesno(self, message: str, yes_label: str = "Yes", no_label: str = "No", - default: Optional[str] = None, cli_flag: Optional[str] = None, + default: Optional[bool] = None, cli_flag: Optional[str] = None, force_interactive: bool = False) -> bool: """Query the user with a yes/no question. @@ -608,7 +608,7 @@ def yesno(self, message: str, yes_label: str = "Yes", no_label: str = "No", :param str message: question for the user :param str yes_label: label for Yes button :param str no_label: label for No button - :param str default: default (non-interactive) choice from the menu + :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 @@ -624,7 +624,7 @@ def yesno(self, message: str, yes_label: str = "Yes", no_label: str = "No", @abstractmethod def checklist(self, message: str, tags: List[str], default: Optional[str] = None, cli_flag: Optional[str] = None, - force_interactive: bool = False) -> Tuple[int, List[str]]: + 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 @@ -638,7 +638,7 @@ def checklist(self, message: str, tags: List[str], default: Optional[str] = None 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 diff --git a/certbot/certbot/tests/util.py b/certbot/certbot/tests/util.py index 12c02dee325..23eea99ea2d 100644 --- a/certbot/certbot/tests/util.py +++ b/certbot/certbot/tests/util.py @@ -405,14 +405,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): From f8d58921fe77011a87a98c5f2be7ed76728c85df Mon Sep 17 00:00:00 2001 From: Adrien Ferrand Date: Sun, 9 May 2021 22:03:16 +0200 Subject: [PATCH 16/47] Fix lint --- certbot/certbot/_internal/cert_manager.py | 2 +- certbot/certbot/_internal/plugins/disco.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/certbot/certbot/_internal/cert_manager.py b/certbot/certbot/_internal/cert_manager.py index 3ac4140e4a9..0aec024e681 100644 --- a/certbot/certbot/_internal/cert_manager.py +++ b/certbot/certbot/_internal/cert_manager.py @@ -3,7 +3,7 @@ import logging import re import traceback -from typing import List, Union +from typing import List import pytz diff --git a/certbot/certbot/_internal/plugins/disco.py b/certbot/certbot/_internal/plugins/disco.py index bcc30bdcb46..511ddb7919a 100644 --- a/certbot/certbot/_internal/plugins/disco.py +++ b/certbot/certbot/_internal/plugins/disco.py @@ -11,7 +11,6 @@ import pkg_resources import zope.interface -from zope.interface import Interface import zope.interface.verify from certbot import errors @@ -379,7 +378,7 @@ def _verify(target_instance: interfaces.Plugin, target_class: Type[interfaces.Pl # So effectively the checks have been done when the plugin has been initialized. return True - zope_iface: Optional[Type[Interface]] = None + zope_iface: Optional[Type[zope.interface.Interface]] = None if iface == interfaces.Plugin: zope_iface = interfaces.IPlugin From d21eced46cf575f3d92f2bc348cc1d6ad9f0beb3 Mon Sep 17 00:00:00 2001 From: Adrien Ferrand Date: Sun, 9 May 2021 22:58:04 +0200 Subject: [PATCH 17/47] Order imports --- certbot/certbot/_internal/plugins/manual.py | 3 ++- certbot/certbot/_internal/plugins/selection.py | 3 --- certbot/certbot/_internal/plugins/standalone.py | 3 ++- certbot/certbot/_internal/plugins/webroot.py | 3 ++- certbot/certbot/_internal/reporter.py | 2 +- certbot/certbot/plugins/common.py | 5 +++-- certbot/certbot/plugins/dns_common.py | 3 ++- certbot/tests/display/util_test.py | 1 - certbot/tests/error_handler_test.py | 7 +++++-- certbot/tests/log_test.py | 2 +- certbot/tests/main_test.py | 2 +- certbot/tests/ocsp_test.py | 8 +++++--- certbot/tests/plugins/disco_test.py | 12 +++++++----- certbot/tests/plugins/storage_test.py | 13 ++++++++----- certbot/tests/renewal_test.py | 14 ++++++-------- 15 files changed, 45 insertions(+), 36 deletions(-) diff --git a/certbot/certbot/_internal/plugins/manual.py b/certbot/certbot/_internal/plugins/manual.py index c2506acbb5f..3876467bc0f 100644 --- a/certbot/certbot/_internal/plugins/manual.py +++ b/certbot/certbot/_internal/plugins/manual.py @@ -2,8 +2,9 @@ from typing import Dict from acme import challenges -from certbot import achallenges, interfaces +from certbot import achallenges from certbot import errors +from certbot import interfaces from certbot import reverter from certbot import services from certbot import util diff --git a/certbot/certbot/_internal/plugins/selection.py b/certbot/certbot/_internal/plugins/selection.py index 1edcf6b8ab6..6d3e876b501 100644 --- a/certbot/certbot/_internal/plugins/selection.py +++ b/certbot/certbot/_internal/plugins/selection.py @@ -11,9 +11,6 @@ logger = logging.getLogger(__name__) -# TODO: Replace the Zope interfaces (once removed) by ABCs in pick_* functions. - - def pick_configurator( config, default, plugins, question="How would you like to authenticate and install " diff --git a/certbot/certbot/_internal/plugins/standalone.py b/certbot/certbot/_internal/plugins/standalone.py index eaddcce1706..02caa164a9e 100644 --- a/certbot/certbot/_internal/plugins/standalone.py +++ b/certbot/certbot/_internal/plugins/standalone.py @@ -13,8 +13,9 @@ from acme import challenges from acme import standalone as acme_standalone -from certbot import achallenges, interfaces +from certbot import achallenges from certbot import errors +from certbot import interfaces from certbot import services from certbot.plugins import common diff --git a/certbot/certbot/_internal/plugins/webroot.py b/certbot/certbot/_internal/plugins/webroot.py index 59d1a5c7e2e..47945aeb5e4 100644 --- a/certbot/certbot/_internal/plugins/webroot.py +++ b/certbot/certbot/_internal/plugins/webroot.py @@ -9,7 +9,8 @@ from typing import Set from acme import challenges -from certbot import errors, interfaces +from certbot import errors +from certbot import interfaces from certbot import services from certbot._internal import cli from certbot.achallenges import KeyAuthorizationAnnotatedChallenge as AnnotatedChallenge diff --git a/certbot/certbot/_internal/reporter.py b/certbot/certbot/_internal/reporter.py index a78b2b6c93d..ffea53c70fa 100644 --- a/certbot/certbot/_internal/reporter.py +++ b/certbot/certbot/_internal/reporter.py @@ -5,8 +5,8 @@ import sys import textwrap -from certbot.interfaces import Reporter as BaseReporter from certbot import util +from certbot.interfaces import Reporter as BaseReporter logger = logging.getLogger(__name__) diff --git a/certbot/certbot/plugins/common.py b/certbot/certbot/plugins/common.py index 6b72220bb6b..2463c9040f5 100644 --- a/certbot/certbot/plugins/common.py +++ b/certbot/certbot/plugins/common.py @@ -1,9 +1,9 @@ """Plugin common functions.""" +from abc import ABCMeta import logging import re import shutil import tempfile -from abc import ABCMeta from typing import List from josepy import util as jose_util @@ -16,8 +16,9 @@ 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 -from certbot.interfaces import Plugin as AbstractPlugin, Installer as AbstractInstaller logger = logging.getLogger(__name__) diff --git a/certbot/certbot/plugins/dns_common.py b/certbot/certbot/plugins/dns_common.py index b371e9538db..d7984fcbec4 100644 --- a/certbot/certbot/plugins/dns_common.py +++ b/certbot/certbot/plugins/dns_common.py @@ -7,7 +7,8 @@ import configobj from acme import challenges -from certbot import errors, interfaces +from certbot import errors +from certbot import interfaces from certbot.compat import filesystem from certbot.compat import os from certbot.display import ops diff --git a/certbot/tests/display/util_test.py b/certbot/tests/display/util_test.py index 882e9c3a91a..9e73c079e0e 100644 --- a/certbot/tests/display/util_test.py +++ b/certbot/tests/display/util_test.py @@ -5,7 +5,6 @@ import tempfile import unittest - from certbot import errors from certbot import interfaces from certbot.display import util as display_util 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 12daf0000bb..e2d3860438c 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 diff --git a/certbot/tests/main_test.py b/certbot/tests/main_test.py index 8238f745273..24ae5ac033f 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 diff --git a/certbot/tests/ocsp_test.py b/certbot/tests/ocsp_test.py index 72a39e2d125..5e81a1b3d37 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/disco_test.py b/certbot/tests/plugins/disco_test.py index 3b87695280d..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",), diff --git a/certbot/tests/plugins/storage_test.py b/certbot/tests/plugins/storage_test.py index e57f438e553..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 typing import Optional, List, Iterable + +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.tests import util as test_util class PluginStorageTest(test_util.ConfigTestCase): diff --git a/certbot/tests/renewal_test.py b/certbot/tests/renewal_test.py index 62f04a82291..5aecd4b34c1 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') @@ -178,8 +178,6 @@ def test_ancient_server_renewal_conf(self, mock_set_by_cli): from certbot._internal import constants self.config.server = None mock_set_by_cli.return_value = False - print(self.config) - print(self.config.server) self._call(self.config, {'server': constants.V1_URI}) self.assertEqual(self.config.server, constants.CLI_DEFAULTS['server']) From 8bfa92703fe1968237dea1f18cbc6bf57859e8a1 Mon Sep 17 00:00:00 2001 From: Adrien Ferrand Date: Mon, 31 May 2021 20:12:53 +0200 Subject: [PATCH 18/47] Align zope interfaces on ABC --- certbot/certbot/display/util.py | 15 +++++---------- certbot/certbot/interfaces.py | 9 ++++++++- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/certbot/certbot/display/util.py b/certbot/certbot/display/util.py index 2066c1df59f..c003fb51c9f 100644 --- a/certbot/certbot/display/util.py +++ b/certbot/certbot/display/util.py @@ -11,12 +11,12 @@ """ import logging import sys -from typing import Any 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 interfaces @@ -196,16 +196,11 @@ def directory_select(message: str, default: Optional[str] = None, cli_flag: Opti force_interactive=force_interactive) -# 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: +def get_display() -> interfaces.Display: """Get the display utility. :return: the display utility - :rtype: IDisplay + :rtype: interfaces.Display :raise: ValueError if the display utility is not set """ @@ -215,10 +210,10 @@ def get_display() -> Any: return _SERVICE.display -def set_display(display: Any) -> None: +def set_display(display: interfaces.Display) -> None: """Set the display service. - :param IDisplay display: the display service + :param interfaces.Display display: the display service """ # This call is done only for retro-compatibility purposes. diff --git a/certbot/certbot/interfaces.py b/certbot/certbot/interfaces.py index 34eafbbb4c7..80b72088a90 100644 --- a/certbot/certbot/interfaces.py +++ b/certbot/certbot/interfaces.py @@ -52,6 +52,7 @@ 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. @@ -229,6 +230,8 @@ 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. @@ -325,6 +328,7 @@ 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. @@ -388,6 +392,7 @@ class IInstaller(IPlugin): # pylint: disable=inherit-non-class """Deprecated, use certbot.interfaces.Installer as ABC instead.""" +@zope.interface.implementer(IInstaller) class Installer(Plugin): """Generic Certbot Installer Interface. @@ -518,6 +523,7 @@ 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 @@ -572,7 +578,7 @@ def menu(self, message: str, choices: Union[List[str], Tuple[str], str], @abstractmethod def input(self, message: str, default: Optional[str] = None, cli_flag: Optional[str] = None, - force_interactive: bool = False) -> Tuple[str, int]: + force_interactive: bool = False) -> Tuple[str, str]: """Accept input from the user. When not setting force_interactive=True, you must provide a @@ -676,6 +682,7 @@ 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.""" From 29db0dc30f1fe4a27d30fbc8ef728f7d47ab0d15 Mon Sep 17 00:00:00 2001 From: Adrien Ferrand Date: Mon, 31 May 2021 20:21:44 +0200 Subject: [PATCH 19/47] Various fixes --- certbot/certbot/display/obj.py | 1 - certbot/certbot/display/util.py | 2 +- certbot/certbot/interfaces.py | 7 ++++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/certbot/certbot/display/obj.py b/certbot/certbot/display/obj.py index b1d40588b7d..804ee606a46 100644 --- a/certbot/certbot/display/obj.py +++ b/certbot/certbot/display/obj.py @@ -542,4 +542,3 @@ def _parens_around_char(label): """ return "({first}){rest}".format(first=label[0], rest=label[1:]) - diff --git a/certbot/certbot/display/util.py b/certbot/certbot/display/util.py index c003fb51c9f..d094eefa782 100644 --- a/certbot/certbot/display/util.py +++ b/certbot/certbot/display/util.py @@ -50,7 +50,7 @@ class _DisplayService: def __init__(self): - self.display: Optional[interfaces.IDisplay] = None + self.display: Optional[interfaces.Display] = None _SERVICE = _DisplayService() diff --git a/certbot/certbot/interfaces.py b/certbot/certbot/interfaces.py index 80b72088a90..854bd72dc20 100644 --- a/certbot/certbot/interfaces.py +++ b/certbot/certbot/interfaces.py @@ -545,7 +545,7 @@ def notification(self, message: str, pause: bool = False, wrap: bool = True, """ @abstractmethod - def menu(self, message: str, choices: Union[List[str], Tuple[str], str], + 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]: @@ -556,8 +556,9 @@ def menu(self, message: str, choices: Union[List[str], Tuple[str], str], :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) From 7c0a676a64a888316d197db5753e66f61d4917bb Mon Sep 17 00:00:00 2001 From: Adrien Ferrand Date: Mon, 31 May 2021 20:26:21 +0200 Subject: [PATCH 20/47] Fix type --- certbot/certbot/display/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot/certbot/display/util.py b/certbot/certbot/display/util.py index d094eefa782..a9a009ae2b0 100644 --- a/certbot/certbot/display/util.py +++ b/certbot/certbot/display/util.py @@ -83,7 +83,7 @@ def notification(message: str, pause: bool = True, wrap: bool = True, force_interactive=force_interactive, decorate=decorate) -def menu(message: str, choices: Union[List[Tuple[str, str]], List[str]], +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. From 2ed4e0a17e784eed8f20bb5c05122f012a5e3c85 Mon Sep 17 00:00:00 2001 From: Jonathan Griffin Date: Thu, 3 Jun 2021 14:42:56 -0600 Subject: [PATCH 21/47] Fixed typo in common.py (#8881) Fixed typo: exterally -> externally --- certbot/certbot/plugins/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot/certbot/plugins/common.py b/certbot/certbot/plugins/common.py index 3181d7b5002..3bdeba8f416 100644 --- a/certbot/certbot/plugins/common.py +++ b/certbot/certbot/plugins/common.py @@ -119,7 +119,7 @@ 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)) From 78261dbae2571d9a52d6fee7ffc8802f1104aec9 Mon Sep 17 00:00:00 2001 From: Michel Le Bihan Date: Sun, 6 Jun 2021 01:55:24 +0200 Subject: [PATCH 22/47] Fix typo of fulfill in dns_rfc2136 plugin (#8886) --- .../certbot_dns_rfc2136/_internal/dns_rfc2136.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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..1bbf0064ccc 100644 --- a/certbot-dns-rfc2136/certbot_dns_rfc2136/_internal/dns_rfc2136.py +++ b/certbot-dns-rfc2136/certbot_dns_rfc2136/_internal/dns_rfc2136.py @@ -27,7 +27,7 @@ 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 = { From d7b26c1bb2115b923d8e076fe7de0750ea3c01ce Mon Sep 17 00:00:00 2001 From: alexzorin Date: Thu, 10 Jun 2021 07:31:15 +1000 Subject: [PATCH 23/47] cli: dont use argv[0] in user-facing messages (#8857) --- certbot/certbot/_internal/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/certbot/certbot/_internal/main.py b/certbot/certbot/_internal/main.py index 2bf6570f5bd..4747053bea2 100644 --- a/certbot/certbot/_internal/main.py +++ b/certbot/certbot/_internal/main.py @@ -202,7 +202,7 @@ def _handle_subset_cert_request(config: configuration.NamespaceConfig, "--duplicate option.{br}{br}" "For example:{br}{br}{1} --duplicate {2}".format( existing, - sys.argv[0], " ".join(sys.argv[1:]), + cli.cli_command, " ".join(sys.argv[1:]), br=os.linesep )) raise errors.Error(USER_CANCELLED) @@ -1052,7 +1052,7 @@ def enhance(config, plugins): if not enhancements.are_requested(config) and not oldstyle_enh: msg = ("Please specify one or more enhancement types to configure. To list " "the available enhancement types, run:\n\n%s --help enhance\n") - logger.error(msg, sys.argv[0]) + logger.error(msg, cli.cli_command) raise errors.MisconfigurationError("No enhancements requested, exiting.") try: From 0b08a80dcea2586d9f1d2499f058a91195b8568b Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 9 Jun 2021 17:01:54 -0700 Subject: [PATCH 24/47] Pin pip & co like our other dependencies (#8868) * use poetry 1.2.0a1 * pin pip normally * use normal constraints file with pipstrap * remove unused STRIP_HASHES var * Check for old poetry versions * keep pip, setuptools, and wheel pinned in oldest * remove strip hashes * pin back pip * fix new lint error --- acme/acme/client.py | 5 +- certbot/setup.py | 7 +- tools/dev_constraints.txt | 2 + tools/pinning/pin.sh | 13 +-- tools/pinning/pyproject.toml | 9 ++ tools/pip_install.py | 1 - tools/pipstrap.py | 7 +- tools/pipstrap_constraints.txt | 18 ---- tools/requirements.txt | 156 ++++++++++++++++----------------- tools/strip_hashes.py | 50 ----------- 10 files changed, 100 insertions(+), 168 deletions(-) delete mode 100644 tools/pipstrap_constraints.txt delete mode 100755 tools/strip_hashes.py diff --git a/acme/acme/client.py b/acme/acme/client.py index 548c3d54877..da34026bec2 100644 --- a/acme/acme/client.py +++ b/acme/acme/client.py @@ -658,7 +658,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, diff --git a/certbot/setup.py b/certbot/setup.py index eba0db3d7d6..c6b04bdf082 100644 --- a/certbot/setup.py +++ b/certbot/setup.py @@ -73,9 +73,10 @@ def read_file(filename, encoding='utf8'): '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', + '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', 'pylint', 'pytest', 'pytest-cov', 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..c6f216a0151 100644 --- a/tools/pinning/pyproject.toml +++ b/tools/pinning/pyproject.toml @@ -58,6 +58,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..20038ca0973 100644 --- a/tools/requirements.txt +++ b/tools/requirements.txt @@ -8,148 +8,150 @@ 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.5.7; 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.85; (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.85; 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.85; 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.5; 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") +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.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" 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" +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.0; 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") +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.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" 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" +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.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" +google-api-python-client==2.7.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" 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.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_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" +ipython-genutils==0.2.0 ipython==7.16.1; python_version == "3.6" ipython==7.24.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" 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" +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.0; 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 +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.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") +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" +pylev==1.4.0; python_version >= "3.6" and python_version < "4.0" pylint==2.8.2; 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" +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.2.1; 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" +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.0.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 +164,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" 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; implementation_name == "cpython" and python_version < "3.8" and python_version >= "3.6" or 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.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" 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" +zipp==3.4.1; 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.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.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 From 0c637860cd2193d1b988d3752c8abef328a08e65 Mon Sep 17 00:00:00 2001 From: alexzorin Date: Fri, 11 Jun 2021 08:58:11 +1000 Subject: [PATCH 25/47] cli: improve error messages for enhance errors (#8884) * cli: improve error messages for enhance errors * remove status message after enhance config revert --- .../certbot_nginx/_internal/configurator.py | 7 ++--- certbot/certbot/_internal/client.py | 26 ++++++++----------- certbot/tests/client_test.py | 2 +- 3 files changed, 16 insertions(+), 19 deletions(-) diff --git a/certbot-nginx/certbot_nginx/_internal/configurator.py b/certbot-nginx/certbot_nginx/_internal/configurator.py index 07397bfe8a7..ec9f9053d88 100644 --- a/certbot-nginx/certbot_nginx/_internal/configurator.py +++ b/certbot-nginx/certbot_nginx/_internal/configurator.py @@ -769,9 +769,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 +788,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/certbot/_internal/client.py b/certbot/certbot/_internal/client.py index 5c0bed22062..1ae65aa469b 100644 --- a/certbot/certbot/_internal/client.py +++ b/certbot/certbot/_internal/client.py @@ -2,7 +2,7 @@ import datetime import logging import platform -from typing import Optional +from typing import List, Optional, Union from cryptography.hazmat.backends import default_backend # See https://github.com/pyca/cryptography/issues/4275 @@ -598,7 +598,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 +613,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/tests/client_test.py b/certbot/tests/client_test.py index 51c6767f68e..2c2d50abcda 100644 --- a/certbot/tests/client_test.py +++ b/certbot/tests/client_test.py @@ -712,7 +712,7 @@ def _test_error(self, enhance_error=False, restart_error=False): 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...') From b48e336554301cf30b134d9b5c38c3173d49ebbd Mon Sep 17 00:00:00 2001 From: ohemorange Date: Thu, 10 Jun 2021 16:21:52 -0700 Subject: [PATCH 26/47] Allow nginx parser to handle empty file (#8895) * Allow parsing empty files * add unit test * lint * update parser_test * Update configurator_test * update changelog --- certbot-nginx/certbot_nginx/_internal/nginxparser.py | 3 +-- certbot-nginx/tests/configurator_test.py | 2 +- certbot-nginx/tests/nginxparser_test.py | 4 ++++ certbot-nginx/tests/parser_test.py | 4 ++-- certbot/CHANGELOG.md | 5 ++++- 5 files changed, 12 insertions(+), 6 deletions(-) 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/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/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/CHANGELOG.md b/certbot/CHANGELOG.md index 04326ef305e..2bd843e31fa 100644 --- a/certbot/CHANGELOG.md +++ b/certbot/CHANGELOG.md @@ -19,7 +19,10 @@ Certbot adheres to [Semantic Versioning](https://semver.org/). ### 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. From c8255dded5d3c86fe8c5f4f084ab7b7c7352c779 Mon Sep 17 00:00:00 2001 From: ohemorange Date: Thu, 10 Jun 2021 16:45:07 -0700 Subject: [PATCH 27/47] Add --verbose-level flag and fix logging level calculations (#8900) Also, update `dev-cli.ini` example to use new flag. Although https://github.com/bw2/ConfigArgParse/pull/216 allowed setting a `count` action value in a config file, our default detection system won't let us use that functionality. While we should eventually fix that, for now, let developers have a cli.ini with a higher logging level by adding this flag. Note that this flag is intended to work the same way adding `-vvv`s does; that is, as a modifier to the pre-set level, rather than setting the absolute level. The number it is set to is equivalent to the number of `v`s that would otherwise have been passed, with "2" as the current maximum effective number of levels (warning --> info --> debug). * Add --verbose-level flag for devs to set in cli.ini * Update dev-cli.ini to use new flag --- certbot/certbot/_internal/cli/__init__.py | 6 ++++++ certbot/certbot/_internal/constants.py | 6 +++++- certbot/certbot/_internal/log.py | 5 ++++- certbot/examples/dev-cli.ini | 4 +--- certbot/tests/log_test.py | 2 +- 5 files changed, 17 insertions(+), 6 deletions(-) diff --git a/certbot/certbot/_internal/cli/__init__.py b/certbot/certbot/_internal/cli/__init__.py index 7d53ad6499b..69192eda81b 100644 --- a/certbot/certbot/_internal/cli/__init__.py +++ b/certbot/certbot/_internal/cli/__init__.py @@ -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/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/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/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/tests/log_test.py b/certbot/tests/log_test.py index 9b3b31030d1..cb52c1fffbb 100644 --- a/certbot/tests/log_test.py +++ b/certbot/tests/log_test.py @@ -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 From 18ea72faf12a64d7efd5206022799594e6f6268e Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 11 Jun 2021 13:17:50 -0700 Subject: [PATCH 28/47] Split out testing extras (#8893) * split out test extras * update extras and regenerate pinnings * pin back mypy --- acme/setup.py | 13 ++++++------- certbot/setup.py | 27 +++++++++++++++++---------- letstest/scripts/test_apache2.sh | 2 +- tools/pinning/pyproject.toml | 16 +++++++++++----- tools/requirements.txt | 28 ++++++++++++++-------------- tools/venv.py | 4 ++-- tox.ini | 14 +++++++------- 7 files changed, 58 insertions(+), 46 deletions(-) diff --git a/acme/setup.py b/acme/setup.py index 38e9208b6af..ed8abce6679 100644 --- a/acme/setup.py +++ b/acme/setup.py @@ -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/certbot/setup.py b/certbot/setup.py index c6b04bdf082..99c92a0b8d4 100644 --- a/certbot/setup.py +++ b/certbot/setup.py @@ -67,23 +67,13 @@ def read_file(filename, encoding='utf8'): ] dev_extras = [ - 'astroid', 'azure-devops', - 'coverage', 'ipdb', - 'mypy', 'PyGithub', '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', - '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', 'tox', 'twine', 'wheel', @@ -97,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, @@ -133,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/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/tools/pinning/pyproject.toml b/tools/pinning/pyproject.toml index c6f216a0151..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 diff --git a/tools/requirements.txt b/tools/requirements.txt index 20038ca0973..7190c9c81ce 100644 --- a/tools/requirements.txt +++ b/tools/requirements.txt @@ -10,18 +10,18 @@ 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" 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.5.7; python_version >= "3.6" and python_version < "4.0" +astroid==2.5.6; 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.85; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.6.0") +awscli==1.19.91; (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; 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" 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.85; 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.85; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.6" +boto3==1.17.91; 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.91; 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") @@ -54,10 +54,10 @@ entrypoints==0.3; python_version >= "3.6" and python_version < "4.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" fabric==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.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.7.0; python_version >= "3.6" +google-api-core==1.30.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.8.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.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" 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" 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" @@ -67,10 +67,10 @@ importlib-metadata==1.7.0; python_version >= "3.6" and python_full_version < "3. 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_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" +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.24.1; 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; python_version == "3.6" or python_version >= "3.7" @@ -91,7 +91,7 @@ mock==4.0.3; python_version >= "3.6" 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.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" @@ -108,7 +108,7 @@ ply==3.11; 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.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.6" +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" @@ -118,7 +118,7 @@ pygithub==1.55; python_version >= "3.6" 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.4.0; python_version >= "3.6" and python_version < "4.0" -pylint==2.8.2; python_version >= "3.6" and python_version < "4.0" +pylint==2.8.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" @@ -170,8 +170,8 @@ tox==3.23.1; python_version >= "3.6" and python_full_version < "3.0.0" or python 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" 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" or python_version >= "3.6" -typing-extensions==3.10.0.0; python_version >= "3.6" or python_version >= "3.6" and python_version < "3.8" +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" 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.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" 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 = From d3555623babf9b5edcded705803c3461d14317f4 Mon Sep 17 00:00:00 2001 From: kartikynwa Date: Sat, 12 Jun 2021 12:32:16 +0530 Subject: [PATCH 29/47] certbot-apache: Add Void Linux overrides (#8891) * certbot-apache: Add Void Linux overrides * certbot-apache: Correct distro name to Void Linux --- .../certbot_apache/_internal/entrypoint.py | 2 ++ .../certbot_apache/_internal/override_void.py | 23 +++++++++++++++++++ certbot/CHANGELOG.md | 2 +- 3 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 certbot-apache/certbot_apache/_internal/override_void.py 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/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/CHANGELOG.md b/certbot/CHANGELOG.md index 2bd843e31fa..e536ec5ba8a 100644 --- a/certbot/CHANGELOG.md +++ b/certbot/CHANGELOG.md @@ -6,7 +6,7 @@ Certbot adheres to [Semantic Versioning](https://semver.org/). ### Added -* +* Add Void Linux overrides for certbot-apache. ### Changed From 1b025e84e80f697a61614af6d48d90d69c3eb155 Mon Sep 17 00:00:00 2001 From: chaptergy Date: Mon, 14 Jun 2021 22:23:35 +0200 Subject: [PATCH 30/47] Adds njalla, DuckDNS and Porkbun 3rd party plugins (#8907) --- certbot/docs/using.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/certbot/docs/using.rst b/certbot/docs/using.rst index 1c68c0ac107..764dea3d23b 100644 --- a/certbot/docs/using.rst +++ b/certbot/docs/using.rst @@ -286,6 +286,9 @@ 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 ================== ==== ==== =============================================================== .. _haproxy: https://github.com/greenhost/certbot-haproxy @@ -302,6 +305,9 @@ 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 If you're interested, you can also :ref:`write your own plugin `. From 60a91eb688ab3329dcc8af17cedd5921c887c0ea Mon Sep 17 00:00:00 2001 From: alexzorin Date: Tue, 15 Jun 2021 07:25:43 +1000 Subject: [PATCH 31/47] certonly: hide "NEXT STEPS" for dry-runs (#8901) * certonly: hide "NEXT STEPS" for dry-runs * add a test --- certbot/certbot/_internal/main.py | 5 +++-- certbot/tests/main_test.py | 15 +++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/certbot/certbot/_internal/main.py b/certbot/certbot/_internal/main.py index 4747053bea2..b208a19abd6 100644 --- a/certbot/certbot/_internal/main.py +++ b/certbot/certbot/_internal/main.py @@ -1398,7 +1398,7 @@ def certonly(config, plugins): if config.csr: cert_path, chain_path, fullchain_path = _csr_get_and_save_cert(config, le_client) _csr_report_new_cert(config, cert_path, chain_path, fullchain_path) - _report_next_steps(config, None, None) + _report_next_steps(config, None, None, new_or_renewed_cert=not config.dry_run) _suggest_donation_if_appropriate(config) eff.handle_subscription(config, le_client.account) return @@ -1417,7 +1417,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) diff --git a/certbot/tests/main_test.py b/certbot/tests/main_test.py index 166b29dea2a..bfe5ee0da51 100644 --- a/certbot/tests/main_test.py +++ b/certbot/tests/main_test.py @@ -271,6 +271,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.""" From bc7c953bcc53fe231b720e2591dcc9278fe3f09e Mon Sep 17 00:00:00 2001 From: alexzorin Date: Fri, 18 Jun 2021 09:36:54 +1000 Subject: [PATCH 32/47] cli: vary renewal advice for hookless manual certs (#8914) * cli: vary renewal advice for hookless manual certs 1. Don't print that the certificate will be automatically renewed, because it won't be. 2. Add a "NEXT STEP" telling the user that they will need to manually re-issue the certificate in order to renew it. * kill superfluous comma Co-authored-by: ohemorange * clarify wording of the next step * fix the test Co-authored-by: ohemorange --- certbot/certbot/_internal/main.py | 25 +++++++++++- certbot/tests/main_test.py | 65 +++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 2 deletions(-) diff --git a/certbot/certbot/_internal/main.py b/certbot/certbot/_internal/main.py index b208a19abd6..c04ce462d72 100644 --- a/certbot/certbot/_internal/main.py +++ b/certbot/certbot/_internal/main.py @@ -507,6 +507,13 @@ def _report_next_steps(config: interfaces.IConfig, installer_err: Optional[error "Certificates created using --csr will not be renewed automatically by Certbot. " "You will need to renew the certificate before it expires, by running the same " "Certbot command again.") + elif _is_interactive_only_auth(config): + steps.append( + "This certificate will not be renewed automatically. Autorenewal of " + "--manual certificates requires the use of an authentication hook script " + "(--manual-auth-hook) but one was not provided. To renew this certificate, repeat " + f"this same {cli.cli_command} command before the certificate's expiry date." + ) elif not config.preconfigured_renewal: steps.append( "The certificate will need to be renewed before it expires. Certbot can " @@ -556,6 +563,11 @@ def _report_new_cert(config, cert_path, fullchain_path, key_path=None): assert cert_path and fullchain_path, "No certificates saved to report." + renewal_msg = "" + if config.preconfigured_renewal and not _is_interactive_only_auth(config): + renewal_msg = ("\nCertbot has set up a scheduled task to automatically renew this " + "certificate in the background.") + display_util.notify( ("\nSuccessfully received certificate.\n" "Certificate is saved at: {cert_path}\n{key_msg}" @@ -564,13 +576,22 @@ def _report_new_cert(config, cert_path, fullchain_path, key_path=None): cert_path=fullchain_path, expiry=crypto_util.notAfter(cert_path).date(), key_msg="Key is saved at: {}\n".format(key_path) if key_path else "", - renewal_msg="\nCertbot has set up a scheduled task to automatically renew this " - "certificate in the background." if config.preconfigured_renewal else "", + renewal_msg=renewal_msg, nl="\n" if config.verb == "run" else "" # Normalize spacing across verbs ) ) +def _is_interactive_only_auth(config: interfaces.IConfig) -> bool: + """ Whether the current authenticator params only support interactive renewal. + """ + # --manual without --manual-auth-hook can never autorenew + if config.authenticator == "manual" and config.manual_auth_hook is None: + return True + + return False + + def _csr_report_new_cert(config: interfaces.IConfig, cert_path: Optional[str], chain_path: Optional[str], fullchain_path: Optional[str]): """ --csr variant of _report_new_cert. diff --git a/certbot/tests/main_test.py b/certbot/tests/main_test.py index bfe5ee0da51..183941e6d17 100644 --- a/certbot/tests/main_test.py +++ b/certbot/tests/main_test.py @@ -1901,6 +1901,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""" From 1e114b4ef81bef47cbbfe736a716c86f115447c4 Mon Sep 17 00:00:00 2001 From: alexzorin Date: Mon, 21 Jun 2021 21:18:29 +1000 Subject: [PATCH 33/47] apache: configure nameless vhosts during auth (#8898) In the apache2 package on Debian-based distros, the default 000-default.conf virtual host does not include a ServerName. Depending on the FQDN hostname of the machine and DNS setup, Apache assigns a name to this unnamed vhost at runtime. As a result, the Apache config end up with vhosts that have duplicative names. Previously, Certbot did not identify that the nameless vhost could be a match for the requested identifier, which would, depending on configuration load order, cause the authenticator to fail. This change causes Certbot to include all unnamed vhosts on top of matched vhosts, during authentication. If no vhosts matched, the existing behavior remains the same. * apache: configure nameless vhosts during auth * vhost is only unnamed if ServerName is not set * also fix test to only match ServerName Co-authored-by: Brad Warren --- .../certbot_apache/_internal/http_01.py | 20 +++++++++++++------ certbot-apache/tests/http_01_test.py | 12 +++++++++++ certbot/CHANGELOG.md | 3 +++ 3 files changed, 29 insertions(+), 6 deletions(-) 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/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/CHANGELOG.md b/certbot/CHANGELOG.md index e536ec5ba8a..7d82559b6fc 100644 --- a/certbot/CHANGELOG.md +++ b/certbot/CHANGELOG.md @@ -16,6 +16,9 @@ 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 From e5c41e76c5e8edd732555a996f6e27f2eb74d68f Mon Sep 17 00:00:00 2001 From: Alex Zorin Date: Sat, 19 Jun 2021 09:54:40 +1000 Subject: [PATCH 34/47] standalone: add an auth_hint --- certbot/certbot/_internal/plugins/standalone.py | 9 +++++++++ certbot/tests/plugins/standalone_test.py | 7 +++++++ 2 files changed, 16 insertions(+) diff --git a/certbot/certbot/_internal/plugins/standalone.py b/certbot/certbot/_internal/plugins/standalone.py index 5fb29671f71..db03744e8df 100644 --- a/certbot/certbot/_internal/plugins/standalone.py +++ b/certbot/certbot/_internal/plugins/standalone.py @@ -5,6 +5,7 @@ import socket from typing import DefaultDict from typing import Dict +from typing import List from typing import Set from typing import Tuple from typing import TYPE_CHECKING @@ -184,6 +185,14 @@ 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: diff --git a/certbot/tests/plugins/standalone_test.py b/certbot/tests/plugins/standalone_test.py index 6f2ae91ba8c..7f55b892df7 100644 --- a/certbot/tests/plugins/standalone_test.py +++ b/certbot/tests/plugins/standalone_test.py @@ -177,6 +177,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 From f137d8424e84591f37532d7e7c2dd7e5daedcd42 Mon Sep 17 00:00:00 2001 From: Alex Zorin Date: Sat, 19 Jun 2021 09:55:02 +1000 Subject: [PATCH 35/47] acme.standalone: expose original socket.error --- acme/acme/standalone.py | 12 ++++++++++-- acme/tests/standalone_test.py | 16 +++++++++++----- 2 files changed, 21 insertions(+), 7 deletions(-) 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/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 From 8b610239bfcf7aac06f6e36d09a5abba3ba87047 Mon Sep 17 00:00:00 2001 From: Rene Luria Date: Fri, 25 Jun 2021 20:46:37 +0200 Subject: [PATCH 36/47] Adds Infonaniak 3rd party plugin (#8923) --- certbot/docs/using.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/certbot/docs/using.rst b/certbot/docs/using.rst index 764dea3d23b..f5de7858699 100644 --- a/certbot/docs/using.rst +++ b/certbot/docs/using.rst @@ -289,6 +289,7 @@ 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 @@ -308,6 +309,7 @@ Porkbun_ Y N DNS Authentication for Porkbun .. _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 `. From 667750f3fffc60cec74a387c292e9cdc12dc18af Mon Sep 17 00:00:00 2001 From: alexzorin Date: Tue, 29 Jun 2021 09:40:24 +1000 Subject: [PATCH 37/47] docs: explain the situation with --manual renewal (#8911) * docs: explain the situation with --manual renewal * note that the non-hook command can't be cronned * add xref to #renewing-certificates * update manual description in the plugins table * redirect manual users towards other plugins * refer to authentication hook scripts in table --- certbot/docs/using.rst | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/certbot/docs/using.rst b/certbot/docs/using.rst index f5de7858699..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: @@ -530,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 @@ -718,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. From d9a1850eaa2b6223f1a0ec3f48fd8f2914464bab Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 6 Jul 2021 08:41:16 -0700 Subject: [PATCH 38/47] Update changelog for 1.17.0 release --- certbot/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certbot/CHANGELOG.md b/certbot/CHANGELOG.md index 7d82559b6fc..c882512cc94 100644 --- a/certbot/CHANGELOG.md +++ b/certbot/CHANGELOG.md @@ -2,7 +2,7 @@ Certbot adheres to [Semantic Versioning](https://semver.org/). -## 1.17.0 - master +## 1.17.0 - 2021-07-06 ### Added From 952a296e209780082aa5223e297706df46d0459a Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 6 Jul 2021 08:42:49 -0700 Subject: [PATCH 39/47] Release 1.17.0 --- acme/setup.py | 2 +- certbot-apache/setup.py | 2 +- certbot-compatibility-test/setup.py | 2 +- certbot-dns-cloudflare/setup.py | 2 +- certbot-dns-cloudxns/setup.py | 2 +- certbot-dns-digitalocean/setup.py | 2 +- certbot-dns-dnsimple/setup.py | 2 +- certbot-dns-dnsmadeeasy/setup.py | 2 +- certbot-dns-gehirn/setup.py | 2 +- certbot-dns-google/setup.py | 2 +- certbot-dns-linode/setup.py | 2 +- certbot-dns-luadns/setup.py | 2 +- certbot-dns-nsone/setup.py | 2 +- certbot-dns-ovh/setup.py | 2 +- certbot-dns-rfc2136/setup.py | 2 +- certbot-dns-route53/setup.py | 2 +- certbot-dns-sakuracloud/setup.py | 2 +- certbot-nginx/setup.py | 2 +- certbot/certbot/__init__.py | 2 +- certbot/docs/cli-help.txt | 4 ++-- 20 files changed, 21 insertions(+), 21 deletions(-) diff --git a/acme/setup.py b/acme/setup.py index ed8abce6679..70ebad743d8 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.17.0' install_requires = [ 'cryptography>=2.1.4', diff --git a/certbot-apache/setup.py b/certbot-apache/setup.py index 3397671a6ab..7be150c8b13 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.17.0' install_requires = [ # We specify the minimum acme and certbot version as the current plugin diff --git a/certbot-compatibility-test/setup.py b/certbot-compatibility-test/setup.py index 879dff40c47..71550aa36eb 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.17.0' install_requires = [ 'certbot', diff --git a/certbot-dns-cloudflare/setup.py b/certbot-dns-cloudflare/setup.py index 3e5cade987a..78ecbb47688 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.17.0' install_requires = [ 'cloudflare>=1.5.1', diff --git a/certbot-dns-cloudxns/setup.py b/certbot-dns-cloudxns/setup.py index da667657a45..ce342920977 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.17.0' install_requires = [ 'dns-lexicon>=3.1.0', # Changed `rtype` parameter name diff --git a/certbot-dns-digitalocean/setup.py b/certbot-dns-digitalocean/setup.py index 5b59ab4acd2..7c7255fb189 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.17.0' install_requires = [ 'python-digitalocean>=1.11', # 1.15.0 or newer is recommended for TTL support diff --git a/certbot-dns-dnsimple/setup.py b/certbot-dns-dnsimple/setup.py index 5a1b13f4406..0f82ee112bc 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.17.0' install_requires = [ 'setuptools>=39.0.1', diff --git a/certbot-dns-dnsmadeeasy/setup.py b/certbot-dns-dnsmadeeasy/setup.py index e0ff95a3ba1..a6e958072fb 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.17.0' install_requires = [ 'dns-lexicon>=3.1.0', # Changed `rtype` parameter name diff --git a/certbot-dns-gehirn/setup.py b/certbot-dns-gehirn/setup.py index 997cd4cca0e..5dcba57bb99 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.17.0' install_requires = [ 'dns-lexicon>=3.1.0', # Changed `rtype` parameter name diff --git a/certbot-dns-google/setup.py b/certbot-dns-google/setup.py index fd3bb45595a..5a8a80a1938 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.17.0' install_requires = [ 'google-api-python-client>=1.5.5', diff --git a/certbot-dns-linode/setup.py b/certbot-dns-linode/setup.py index 2c21ceecbf9..307f542792f 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.17.0' install_requires = [ 'dns-lexicon>=3.1.0', # Changed `rtype` parameter name diff --git a/certbot-dns-luadns/setup.py b/certbot-dns-luadns/setup.py index 024b1100bb9..b0a3819cc0a 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.17.0' install_requires = [ 'dns-lexicon>=3.1.0', # Changed `rtype` parameter name diff --git a/certbot-dns-nsone/setup.py b/certbot-dns-nsone/setup.py index 959c3876dd9..ea133dae9d7 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.17.0' install_requires = [ 'dns-lexicon>=3.1.0', # Changed `rtype` parameter name diff --git a/certbot-dns-ovh/setup.py b/certbot-dns-ovh/setup.py index 52dca456bd1..3c3260d6c99 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.17.0' install_requires = [ 'dns-lexicon>=3.1.0', # Changed `rtype` parameter name diff --git a/certbot-dns-rfc2136/setup.py b/certbot-dns-rfc2136/setup.py index 25f0f2bfea2..1aa80105ce0 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.17.0' install_requires = [ 'dnspython', diff --git a/certbot-dns-route53/setup.py b/certbot-dns-route53/setup.py index 90069b3e237..0e9e21f7fbf 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.17.0' install_requires = [ 'boto3', diff --git a/certbot-dns-sakuracloud/setup.py b/certbot-dns-sakuracloud/setup.py index 8f98058f0cd..05c9d09c709 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.17.0' install_requires = [ 'dns-lexicon>=3.1.0', # Changed `rtype` parameter name diff --git a/certbot-nginx/setup.py b/certbot-nginx/setup.py index 371df2a3b9d..acfb46d84f0 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.17.0' install_requires = [ # We specify the minimum acme and certbot version as the current plugin diff --git a/certbot/certbot/__init__.py b/certbot/certbot/__init__.py index 4d97c9fdab1..e3824a20a5b 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.17.0' 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, From d94cf0e1d643b272bedd351bfb7114d9976cf85d Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 6 Jul 2021 08:42:51 -0700 Subject: [PATCH 40/47] Add contents to certbot/CHANGELOG.md for next version --- certbot/CHANGELOG.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/certbot/CHANGELOG.md b/certbot/CHANGELOG.md index c882512cc94..f77a36c791e 100644 --- a/certbot/CHANGELOG.md +++ b/certbot/CHANGELOG.md @@ -2,6 +2,22 @@ Certbot adheres to [Semantic Versioning](https://semver.org/). +## 1.18.0 - master + +### Added + +* + +### Changed + +* + +### Fixed + +* + +More details about these changes can be found on our GitHub repo. + ## 1.17.0 - 2021-07-06 ### Added From 915459258b9509e92ad3dff54ebe58bf78573699 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 6 Jul 2021 08:42:52 -0700 Subject: [PATCH 41/47] Bump version to 1.18.0 --- acme/setup.py | 2 +- certbot-apache/setup.py | 2 +- certbot-compatibility-test/setup.py | 2 +- certbot-dns-cloudflare/setup.py | 2 +- certbot-dns-cloudxns/setup.py | 2 +- certbot-dns-digitalocean/setup.py | 2 +- certbot-dns-dnsimple/setup.py | 2 +- certbot-dns-dnsmadeeasy/setup.py | 2 +- certbot-dns-gehirn/setup.py | 2 +- certbot-dns-google/setup.py | 2 +- certbot-dns-linode/setup.py | 2 +- certbot-dns-luadns/setup.py | 2 +- certbot-dns-nsone/setup.py | 2 +- certbot-dns-ovh/setup.py | 2 +- certbot-dns-rfc2136/setup.py | 2 +- certbot-dns-route53/setup.py | 2 +- certbot-dns-sakuracloud/setup.py | 2 +- certbot-nginx/setup.py | 2 +- certbot/certbot/__init__.py | 2 +- 19 files changed, 19 insertions(+), 19 deletions(-) diff --git a/acme/setup.py b/acme/setup.py index 70ebad743d8..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' +version = '1.18.0.dev0' install_requires = [ 'cryptography>=2.1.4', diff --git a/certbot-apache/setup.py b/certbot-apache/setup.py index 7be150c8b13..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' +version = '1.18.0.dev0' install_requires = [ # We specify the minimum acme and certbot version as the current plugin diff --git a/certbot-compatibility-test/setup.py b/certbot-compatibility-test/setup.py index 71550aa36eb..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' +version = '1.18.0.dev0' install_requires = [ 'certbot', diff --git a/certbot-dns-cloudflare/setup.py b/certbot-dns-cloudflare/setup.py index 78ecbb47688..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' +version = '1.18.0.dev0' install_requires = [ 'cloudflare>=1.5.1', diff --git a/certbot-dns-cloudxns/setup.py b/certbot-dns-cloudxns/setup.py index ce342920977..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' +version = '1.18.0.dev0' install_requires = [ 'dns-lexicon>=3.1.0', # Changed `rtype` parameter name diff --git a/certbot-dns-digitalocean/setup.py b/certbot-dns-digitalocean/setup.py index 7c7255fb189..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' +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-dnsimple/setup.py b/certbot-dns-dnsimple/setup.py index 0f82ee112bc..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' +version = '1.18.0.dev0' install_requires = [ 'setuptools>=39.0.1', diff --git a/certbot-dns-dnsmadeeasy/setup.py b/certbot-dns-dnsmadeeasy/setup.py index a6e958072fb..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' +version = '1.18.0.dev0' install_requires = [ 'dns-lexicon>=3.1.0', # Changed `rtype` parameter name diff --git a/certbot-dns-gehirn/setup.py b/certbot-dns-gehirn/setup.py index 5dcba57bb99..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' +version = '1.18.0.dev0' install_requires = [ 'dns-lexicon>=3.1.0', # Changed `rtype` parameter name diff --git a/certbot-dns-google/setup.py b/certbot-dns-google/setup.py index 5a8a80a1938..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' +version = '1.18.0.dev0' install_requires = [ 'google-api-python-client>=1.5.5', diff --git a/certbot-dns-linode/setup.py b/certbot-dns-linode/setup.py index 307f542792f..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' +version = '1.18.0.dev0' install_requires = [ 'dns-lexicon>=3.1.0', # Changed `rtype` parameter name diff --git a/certbot-dns-luadns/setup.py b/certbot-dns-luadns/setup.py index b0a3819cc0a..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' +version = '1.18.0.dev0' install_requires = [ 'dns-lexicon>=3.1.0', # Changed `rtype` parameter name diff --git a/certbot-dns-nsone/setup.py b/certbot-dns-nsone/setup.py index ea133dae9d7..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' +version = '1.18.0.dev0' install_requires = [ 'dns-lexicon>=3.1.0', # Changed `rtype` parameter name diff --git a/certbot-dns-ovh/setup.py b/certbot-dns-ovh/setup.py index 3c3260d6c99..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' +version = '1.18.0.dev0' install_requires = [ 'dns-lexicon>=3.1.0', # Changed `rtype` parameter name diff --git a/certbot-dns-rfc2136/setup.py b/certbot-dns-rfc2136/setup.py index 1aa80105ce0..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' +version = '1.18.0.dev0' install_requires = [ 'dnspython', diff --git a/certbot-dns-route53/setup.py b/certbot-dns-route53/setup.py index 0e9e21f7fbf..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' +version = '1.18.0.dev0' install_requires = [ 'boto3', diff --git a/certbot-dns-sakuracloud/setup.py b/certbot-dns-sakuracloud/setup.py index 05c9d09c709..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' +version = '1.18.0.dev0' install_requires = [ 'dns-lexicon>=3.1.0', # Changed `rtype` parameter name diff --git a/certbot-nginx/setup.py b/certbot-nginx/setup.py index acfb46d84f0..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' +version = '1.18.0.dev0' install_requires = [ # We specify the minimum acme and certbot version as the current plugin diff --git a/certbot/certbot/__init__.py b/certbot/certbot/__init__.py index e3824a20a5b..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' +__version__ = '1.18.0.dev0' From 2ab7857fa58797ff9cad26b5de067d0f6210a97c Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 14 Jul 2021 14:11:50 -0700 Subject: [PATCH 42/47] Do not guess HTTP-01 response encoding (#8942) * fix http-01 encoding * improve comment --- acme/acme/challenges.py | 9 +++++++++ certbot/CHANGELOG.md | 6 +++++- 2 files changed, 14 insertions(+), 1 deletion(-) 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/certbot/CHANGELOG.md b/certbot/CHANGELOG.md index f77a36c791e..865de631eda 100644 --- a/certbot/CHANGELOG.md +++ b/certbot/CHANGELOG.md @@ -10,7 +10,11 @@ Certbot adheres to [Semantic Versioning](https://semver.org/). ### Changed -* +* When self-validating HTTP-01 challenges using + acme.challenges.HTTP01Response.simple_verify, we now assume that the response + is composed of only ASCII characters. Previously we were relying on the + default behavior of the requests library which tries to guess the encoding of + the response which was error prone. ### Fixed From 117791b582fcf78d7f5cedf7921f856f5274ab6a Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 14 Jul 2021 14:34:54 -0700 Subject: [PATCH 43/47] Remove unneeded certbot-auto files (#8938) --- .../templates/jobs/extended-tests-jobs.yml | 3 + .../templates/jobs/standard-tests-jobs.yml | 5 - certbot-auto | 1988 ----------------- letsencrypt-auto | 1988 ----------------- letsencrypt-auto-source/certbot-auto.asc | 11 - tests/modification-check.py | 11 - 6 files changed, 3 insertions(+), 4003 deletions(-) delete mode 100755 certbot-auto delete mode 100755 letsencrypt-auto delete mode 100644 letsencrypt-auto-source/certbot-auto.asc 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/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/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/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', } From 8e29063ba7e110ecf8e43372f2bde8fbd1e82afe Mon Sep 17 00:00:00 2001 From: alexzorin Date: Fri, 16 Jul 2021 04:03:39 +1000 Subject: [PATCH 44/47] pylint: upgrade pinned verson and fix new lints (#8936) While bumping pinned packages in #8928, we came across a new version of pylint (2.9.3). Upgrading to this version requires some changes to Certbot's code, which is what this change is about. * pylint: upgrade pinned verson and fix new lints * maxsplit should be 1, not -1, for rsplit --- acme/acme/messages.py | 2 +- .../_internal/override_centos.py | 4 +- .../_internal/override_fedora.py | 4 +- .../_internal/override_gentoo.py | 4 +- .../certbot_tests/test_main.py | 9 ++-- .../utils/acme_server.py | 2 +- .../utils/dns_server.py | 1 + .../configurators/nginx/common.py | 15 +++--- .../certbot_nginx/_internal/parser.py | 14 +++-- certbot/certbot/_internal/account.py | 4 +- certbot/certbot/_internal/cli/__init__.py | 2 +- certbot/certbot/_internal/error_handler.py | 4 +- certbot/certbot/_internal/storage.py | 3 +- certbot/certbot/_internal/updater.py | 2 +- certbot/certbot/display/ops.py | 4 +- certbot/certbot/interfaces.py | 15 ++++-- certbot/certbot/plugins/dns_common.py | 6 +-- tools/requirements.txt | 53 +++++++++---------- 18 files changed, 78 insertions(+), 70 deletions(-) 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/certbot-apache/certbot_apache/_internal/override_centos.py b/certbot-apache/certbot_apache/_internal/override_centos.py index c1a69885c63..5d920e34383 100644 --- a/certbot-apache/certbot_apache/_internal/override_centos.py +++ b/certbot-apache/certbot_apache/_internal/override_centos.py @@ -177,8 +177,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_fedora.py b/certbot-apache/certbot_apache/_internal/override_fedora.py index 3b947a8232e..f4e89ca710b 100644 --- a/certbot-apache/certbot_apache/_internal/override_fedora.py +++ b/certbot-apache/certbot_apache/_internal/override_fedora.py @@ -87,5 +87,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..806a8039550 100644 --- a/certbot-apache/certbot_apache/_internal/override_gentoo.py +++ b/certbot-apache/certbot_apache/_internal/override_gentoo.py @@ -53,8 +53,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-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-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/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/cli/__init__.py b/certbot/certbot/_internal/cli/__init__.py index 69192eda81b..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__) 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/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/display/ops.py b/certbot/certbot/display/ops.py index c2051d3d215..c1c0255b597 100644 --- a/certbot/certbot/display/ops.py +++ b/certbot/certbot/display/ops.py @@ -217,9 +217,9 @@ def _choose_names_manually(prompt_prefix=""): retry_message = ( "One or more of the entered domain names was not valid:" "{0}{0}").format(os.linesep) - for domain in invalid_domains: + for invalid_domain, err in invalid_domains.items(): retry_message = retry_message + "{1}: {2}{0}".format( - os.linesep, domain, invalid_domains[domain]) + os.linesep, invalid_domain, err) retry_message = retry_message + ( "{0}Would you like to re-enter the names?{0}").format( os.linesep) diff --git a/certbot/certbot/interfaces.py b/certbot/certbot/interfaces.py index de9175def68..ab814b28fcb 100644 --- a/certbot/certbot/interfaces.py +++ b/certbot/certbot/interfaces.py @@ -549,7 +549,8 @@ def print_messages(self): class RenewableCert(object, metaclass=abc.ABCMeta): """Interface to a certificate lineage.""" - @abc.abstractproperty + @property + @abc.abstractmethod def cert_path(self): """Path to the certificate file. @@ -557,7 +558,8 @@ def cert_path(self): """ - @abc.abstractproperty + @property + @abc.abstractmethod def key_path(self): """Path to the private key file. @@ -565,7 +567,8 @@ def key_path(self): """ - @abc.abstractproperty + @property + @abc.abstractmethod def chain_path(self): """Path to the certificate chain file. @@ -573,7 +576,8 @@ def chain_path(self): """ - @abc.abstractproperty + @property + @abc.abstractmethod def fullchain_path(self): """Path to the full chain file. @@ -583,7 +587,8 @@ def fullchain_path(self): """ - @abc.abstractproperty + @property + @abc.abstractmethod def lineagename(self): """Name given to the certificate lineage. diff --git a/certbot/certbot/plugins/dns_common.py b/certbot/certbot/plugins/dns_common.py index 23e25554434..e27d85b7ea1 100644 --- a/certbot/certbot/plugins/dns_common.py +++ b/certbot/certbot/plugins/dns_common.py @@ -169,7 +169,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 +199,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 +225,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/tools/requirements.txt b/tools/requirements.txt index 7190c9c81ce..8370ea0a6bc 100644 --- a/tools/requirements.txt +++ b/tools/requirements.txt @@ -7,32 +7,31 @@ # 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" 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.5.6; python_version >= "3.6" and python_version < "4.0" +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.91; (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; 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" 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.91; 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.91; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.6" +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" 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.5; 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" +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==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 >= "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.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" +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" @@ -43,7 +42,7 @@ decorator==5.0.9; python_version == "3.6" or python_version > "3.6" or python_ve 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.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.0; python_version >= "3.6" and python_version < "4.0" +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" @@ -51,26 +50,26 @@ dockerpty==0.4.1; python_version >= "3.6" and python_full_version < "3.0.0" or p 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 >= "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.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" +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" 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.30.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.8.0; python_version >= "3.6" +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.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-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" 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" 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" or python_version >= "3.6" and python_version < "3.8" 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_version < "3.7" and python_full_version >= "3.4.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" +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.1; 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; python_version == "3.6" or python_version >= "3.7" @@ -98,11 +97,11 @@ packaging==20.9; python_version >= "3.6" and python_full_version < "3.0.0" and p 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" -pathlib2==2.3.5; 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.0; python_version >= "3.6" and python_version < "4.0" or python_version >= "3.6" +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.1.0a5; python_version >= "3.6" and python_version < "4.0" @@ -118,22 +117,22 @@ pygithub==1.55; python_version >= "3.6" 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.4.0; python_version >= "3.6" and python_version < "4.0" -pylint==2.8.3; 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.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" +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" or 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.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" +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") @@ -149,7 +148,7 @@ rfc3986==1.5.0; 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 sys_platform == "linux" -setuptools==57.0.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" +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" @@ -167,13 +166,13 @@ 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" 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; 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" +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.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" +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" 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" @@ -181,8 +180,8 @@ websocket-client==0.59.0; python_version >= "3.6" and python_full_version < "3.0 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.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.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" From a105b587ac042ea2b129bed42cf47586b59818c6 Mon Sep 17 00:00:00 2001 From: alexzorin Date: Fri, 16 Jul 2021 04:12:14 +1000 Subject: [PATCH 45/47] apache: fix crash when authenticating empty vhosts (#8941) Fixes #8940. --- certbot-apache/certbot_apache/_internal/parser.py | 6 +++++- certbot-apache/tests/parser_test.py | 5 +++++ .../multiple_vhosts/apache2/sites-available/empty.conf | 0 .../apache2/sites-available/no-directives.conf | 5 +++++ certbot/CHANGELOG.md | 3 ++- 5 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 certbot-apache/tests/testdata/debian_apache_2_4/multiple_vhosts/apache2/sites-available/empty.conf create mode 100644 certbot-apache/tests/testdata/debian_apache_2_4/multiple_vhosts/apache2/sites-available/no-directives.conf 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/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/CHANGELOG.md b/certbot/CHANGELOG.md index 865de631eda..8dcc5049fe9 100644 --- a/certbot/CHANGELOG.md +++ b/certbot/CHANGELOG.md @@ -18,7 +18,8 @@ Certbot adheres to [Semantic Versioning](https://semver.org/). ### 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. From bebd3994885dc26509e047dbde6b33de8efab26c Mon Sep 17 00:00:00 2001 From: alexzorin Date: Fri, 16 Jul 2021 08:50:16 +1000 Subject: [PATCH 46/47] acme: deprecate ACMEv1 client classes (#8931) * acme: deprecate ACMEv1 client classes Adds pending deprecations to: - acme.client.Client - acme.client.BackwardsCompatibleClientV2 Adds a warning to Certbot when a v1 server is detected. * move thsi change from 1.17 to 1.18 * revert some whitespace changes --- acme/acme/client.py | 11 +++++++++++ certbot/CHANGELOG.md | 2 ++ certbot/certbot/_internal/client.py | 15 +++++++++++++-- 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/acme/acme/client.py b/acme/acme/client.py index da34026bec2..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) @@ -805,6 +811,9 @@ class BackwardsCompatibleClientV2: """ACME client wrapper that tends towards V2-style calls, but supports V1 servers. + .. deprecated:: 1.18.0 + Use :class:`ClientV2` instead. + .. note:: While this class handles the majority of the differences between versions of the ACME protocol, if you need to support an ACME server based on version 3 or older of the IETF ACME draft @@ -821,6 +830,8 @@ class BackwardsCompatibleClientV2: """ def __init__(self, net, key, server): + warnings.warn("acme.client.BackwardsCompatibleClientV2 is deprecated, use " + "acme.client.ClientV2 instead.", PendingDeprecationWarning) directory = messages.Directory.from_json(net.get(server).json()) self.acme_version = self._acme_version_from_directory(directory) self.client: Union[Client, ClientV2] diff --git a/certbot/CHANGELOG.md b/certbot/CHANGELOG.md index 8dcc5049fe9..fe7642cda05 100644 --- a/certbot/CHANGELOG.md +++ b/certbot/CHANGELOG.md @@ -15,6 +15,8 @@ Certbot adheres to [Semantic Versioning](https://semver.org/). is composed of only ASCII characters. Previously we were relying on the default behavior of the requests library which tries to guess the encoding of the response which was error prone. +* `acme`: the `.client.Client` and `.client.BackwardsCompatibleClientV2` classes + are now deprecated in favor of `.client.ClientV2`. ### Fixed diff --git a/certbot/certbot/_internal/client.py b/certbot/certbot/_internal/client.py index 1ae65aa469b..7fcaf91c579 100644 --- a/certbot/certbot/_internal/client.py +++ b/certbot/certbot/_internal/client.py @@ -3,6 +3,7 @@ import logging import platform from typing import List, Optional, Union +import warnings from cryptography.hazmat.backends import default_backend # See https://github.com/pyca/cryptography/issues/4275 @@ -32,13 +33,23 @@ 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): From 10eecf9c978ce909f6965de5d82cea39a720facc Mon Sep 17 00:00:00 2001 From: Adrien Ferrand Date: Tue, 20 Jul 2021 02:09:06 +0200 Subject: [PATCH 47/47] Deprecate zope.component in favor of an direct calls to functions from `certbot.display.util` module (#8835) * Implement certbot services * Various fixes * Local oldest requirements * Clean imports * Add unit tests for certbot.services * Clean code * Protect against nullity of global services * Fix CLI * Fix tests * Consistent test behavior * Various fixes * Clean code * Remove reporter service, migrate display service in certbot.display.util. * Fix test * Fix apache compatibility test * Fix oldest test * Setup certbot.display.service module * Reintegrate in util * Fix imports * Fix tests and documentation * Refactor * Cleanup * Cleanup * Clean imports * Add unit tests * Borrow sphinx build fix from #8863 * Fix type * Add comment * Do not reuse existing display service, which never exist at that time * Make get_display() private * Fix lint * Make display internal * Fix circular dependencies * Fixing circular dependencies * Rename patch methods and update docstring * Update deprecation messages * Update certbot/certbot/_internal/display/obj.py Co-authored-by: Brad Warren * Update certbot/certbot/tests/util.py Co-authored-by: Brad Warren * Update certbot/certbot/tests/util.py Co-authored-by: Brad Warren * Update certbot/certbot/tests/util.py Co-authored-by: Brad Warren * Update certbot/certbot/tests/util.py Co-authored-by: Brad Warren * Add links * Avoid relying on internal certbot packages from certbot-apache * Keep same behavior for patch_get_utility* * Better diff * Add changelog * Update certbot/certbot/tests/util.py Co-authored-by: Brad Warren Co-authored-by: Brad Warren --- .../certbot_apache/_internal/configurator.py | 3 +- .../certbot_apache/_internal/display_ops.py | 12 +- certbot-apache/tests/configurator_test.py | 4 +- certbot-apache/tests/debian_test.py | 31 +- certbot-apache/tests/display_ops_test.py | 22 +- certbot-apache/tests/util.py | 11 +- .../certbot_compatibility_test/test_driver.py | 4 +- .../tests/dns_cloudflare_test.py | 4 +- .../tests/dns_digitalocean_test.py | 2 +- certbot-dns-google/tests/dns_google_test.py | 4 +- certbot-dns-rfc2136/tests/dns_rfc2136_test.py | 4 +- .../certbot_nginx/_internal/display_ops.py | 8 +- certbot-nginx/tests/display_ops_test.py | 4 +- certbot-nginx/tests/test_util.py | 4 +- certbot/CHANGELOG.md | 7 +- certbot/certbot/_internal/auth_handler.py | 9 +- certbot/certbot/_internal/cert_manager.py | 25 +- certbot/certbot/_internal/cli/helpful.py | 14 +- certbot/certbot/_internal/display/obj.py | 579 +++++++++++++++ certbot/certbot/_internal/eff.py | 5 +- certbot/certbot/_internal/main.py | 46 +- certbot/certbot/_internal/plugins/disco.py | 3 +- certbot/certbot/_internal/plugins/manual.py | 8 +- .../certbot/_internal/plugins/selection.py | 12 +- .../certbot/_internal/plugins/standalone.py | 6 +- certbot/certbot/_internal/plugins/webroot.py | 4 +- certbot/certbot/_internal/renewal.py | 3 +- certbot/certbot/crypto_util.py | 3 +- certbot/certbot/display/ops.py | 37 +- certbot/certbot/display/util.py | 666 ++++-------------- .../plugins/dns_test_common_lexicon.py | 2 +- certbot/certbot/tests/util.py | 93 ++- certbot/tests/auth_handler_test.py | 13 +- certbot/tests/cert_manager_test.py | 34 +- certbot/tests/cli_test.py | 6 +- certbot/tests/client_test.py | 36 +- certbot/tests/display/obj_test.py | 373 ++++++++++ certbot/tests/display/ops_test.py | 96 +-- certbot/tests/display/util_test.py | 466 +++--------- certbot/tests/eff_test.py | 12 +- certbot/tests/main_test.py | 92 ++- certbot/tests/plugins/dns_common_test.py | 12 +- certbot/tests/plugins/enhancements_test.py | 2 +- certbot/tests/plugins/manual_test.py | 14 +- certbot/tests/plugins/selection_test.py | 24 +- certbot/tests/plugins/standalone_test.py | 18 +- certbot/tests/plugins/webroot_test.py | 10 +- certbot/tests/renewal_test.py | 2 +- certbot/tests/renewupdater_test.py | 2 +- 49 files changed, 1566 insertions(+), 1285 deletions(-) create mode 100644 certbot/certbot/_internal/display/obj.py create mode 100644 certbot/tests/display/obj_test.py diff --git a/certbot-apache/certbot_apache/_internal/configurator.py b/certbot-apache/certbot_apache/_internal/configurator.py index 80dd8520be5..4b01ccc07f8 100644 --- a/certbot-apache/certbot_apache/_internal/configurator.py +++ b/certbot-apache/certbot_apache/_internal/configurator.py @@ -15,7 +15,6 @@ from typing import Set from typing import Union -import zope.component import zope.interface from acme import challenges @@ -884,7 +883,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/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/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-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-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-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-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-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-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/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/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 fe7642cda05..c13b42d3895 100644 --- a/certbot/CHANGELOG.md +++ b/certbot/CHANGELOG.md @@ -6,7 +6,9 @@ Certbot adheres to [Semantic Versioning](https://semver.org/). ### 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 @@ -17,6 +19,9 @@ Certbot adheres to [Semantic Versioning](https://semver.org/). 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 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..1442998c813 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: @@ -316,7 +313,6 @@ def get_certnames(config, verb, allow_multiple=False, custom_prompt=None): 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 +322,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 +332,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 +378,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/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/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/main.py b/certbot/certbot/_internal/main.py index c04ce462d72..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 " @@ -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.") @@ -652,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 " @@ -702,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 @@ -788,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." @@ -1034,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 @@ -1428,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) @@ -1563,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..29746ace873 100644 --- a/certbot/certbot/_internal/plugins/disco.py +++ b/certbot/certbot/_internal/plugins/disco.py @@ -1,8 +1,8 @@ """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 Union @@ -10,6 +10,7 @@ import pkg_resources import zope.interface import zope.interface.verify + from certbot import errors from certbot import interfaces from certbot._internal import constants diff --git a/certbot/certbot/_internal/plugins/manual.py b/certbot/certbot/_internal/plugins/manual.py index 3f41024fba7..f49be9edaff 100644 --- a/certbot/certbot/_internal/plugins/manual.py +++ b/certbot/certbot/_internal/plugins/manual.py @@ -2,7 +2,6 @@ import logging from typing import Dict -import zope.component import zope.interface from acme import challenges @@ -11,15 +10,17 @@ 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): @@ -225,8 +226,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/selection.py b/certbot/certbot/_internal/plugins/selection.py index cdf2c235582..08f849aed93 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,7 +11,7 @@ from certbot.display import util as display_util logger = logging.getLogger(__name__) -z_util = zope.component.getUtility + def pick_configurator( config, default, plugins, @@ -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 db03744e8df..907eccd36ae 100644 --- a/certbot/certbot/_internal/plugins/standalone.py +++ b/certbot/certbot/_internal/plugins/standalone.py @@ -18,6 +18,7 @@ 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__) @@ -28,6 +29,7 @@ Set[achallenges.KeyAuthorizationAnnotatedChallenge] ] + class ServerManager: """Standalone servers manager. @@ -202,14 +204,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..426b69e89ec 100644 --- a/certbot/certbot/_internal/plugins/webroot.py +++ b/certbot/certbot/_internal/plugins/webroot.py @@ -8,7 +8,6 @@ from typing import List from typing import Set -import zope.component import zope.interface from acme import challenges @@ -126,11 +125,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..cb7c67643af 100644 --- a/certbot/certbot/_internal/renewal.py +++ b/certbot/certbot/_internal/renewal.py @@ -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/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/ops.py b/certbot/certbot/display/ops.py index c1c0255b597..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)", @@ -226,8 +220,7 @@ def _choose_names_manually(prompt_prefix=""): 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/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..4fea6676a24 100644 --- a/certbot/certbot/tests/util.py +++ b/certbot/certbot/tests/util.py @@ -37,9 +37,8 @@ "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 def vector_path(*names): @@ -150,10 +149,7 @@ 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 @@ -161,31 +157,89 @@ def patch_get_utility(target='zope.component.getUtility'): :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,7 +310,7 @@ 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(): @@ -267,7 +321,7 @@ def _create_get_utility_mock(): 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 +335,17 @@ 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': 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, name, frozen_mock) display.freeze() - return FreezableMock(frozen=True, return_value=display) 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 2c2d50abcda..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,7 +712,7 @@ 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) 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/main_test.py b/certbot/tests/main_test.py index 183941e6d17..cf22d69d930 100644 --- a/certbot/tests/main_test.py +++ b/certbot/tests/main_test.py @@ -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') @@ -432,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 @@ -452,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 @@ -469,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, @@ -489,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): @@ -507,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): @@ -527,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): @@ -563,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) @@ -661,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() @@ -896,7 +895,7 @@ def test_plugins_no_args(self, _det, mock_disco): 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() @@ -917,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) @@ -933,7 +932,7 @@ def test_plugins_init(self, _det, mock_disco): 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() @@ -951,7 +950,7 @@ def test_plugins_prepare(self, _det, mock_disco): 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() @@ -1040,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']) @@ -1049,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')) @@ -1114,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 @@ -1156,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') @@ -1182,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:") @@ -1392,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') @@ -1421,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) @@ -1439,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]) @@ -1455,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) @@ -1552,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): @@ -1633,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) @@ -1743,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"] @@ -1757,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"] @@ -1978,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/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..4861d23656e 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_*.""" @@ -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 7f55b892df7..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 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..a64d23e225b 100644 --- a/certbot/tests/renewal_test.py +++ b/certbot/tests/renewal_test.py @@ -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