diff --git a/AUTHORS.md b/AUTHORS.md index cb60bfd87..d82ddcb76 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -138,6 +138,7 @@ Authors * [Joubin Jabbari](https://github.com/joubin) * [Juho Juopperi](https://github.com/jkjuopperi) * [Kane York](https://github.com/riking) +* [Katsuyoshi Ozaki](https://github.com/moratori) * [Kenichi Maehashi](https://github.com/kmaehashi) * [Kenneth Skovhede](https://github.com/kenkendk) * [Kevin Burke](https://github.com/kevinburke) diff --git a/acme/acme/messages.py b/acme/acme/messages.py index 2f3ad838b..1eb29e9e5 100644 --- a/acme/acme/messages.py +++ b/acme/acme/messages.py @@ -343,7 +343,7 @@ class Registration(ResourceBody): @classmethod def from_data(cls, phone: Optional[str] = None, email: Optional[str] = None, - external_account_binding: Optional[ExternalAccountBinding] = None, + external_account_binding: Optional[Dict[str, Any]] = None, **kwargs: Any) -> 'Registration': """ Create registration resource from contact details. diff --git a/acme/setup.py b/acme/setup.py index 57458cf1f..1fd8f802e 100644 --- a/acme/setup.py +++ b/acme/setup.py @@ -3,10 +3,10 @@ import sys from setuptools import find_packages from setuptools import setup -version = '1.22.0.dev0' +version = '1.23.0.dev0' install_requires = [ - 'cryptography>=2.1.4', + 'cryptography>=2.5.0', 'josepy>=1.10.0', 'PyOpenSSL>=17.3.0', 'pyrfc3339', diff --git a/certbot-apache/certbot_apache/_internal/apache_util.py b/certbot-apache/certbot_apache/_internal/apache_util.py index 3d06814b6..adbfc06bc 100644 --- a/certbot-apache/certbot_apache/_internal/apache_util.py +++ b/certbot-apache/certbot_apache/_internal/apache_util.py @@ -242,6 +242,7 @@ def _get_runtime_cfg(command): return stdout + def find_ssl_apache_conf(prefix): """ Find a TLS Apache config file in the dedicated storage. diff --git a/certbot-apache/certbot_apache/_internal/assertions.py b/certbot-apache/certbot_apache/_internal/assertions.py index 46bed3265..ea625d020 100644 --- a/certbot-apache/certbot_apache/_internal/assertions.py +++ b/certbot-apache/certbot_apache/_internal/assertions.py @@ -29,6 +29,7 @@ def assertEqual(first, second): # (but identical) directory structures. assert first.filepath == second.filepath + def assertEqualComment(first, second): # pragma: no cover """ Equality assertion for CommentNode """ @@ -38,6 +39,7 @@ def assertEqualComment(first, second): # pragma: no cover if not isPass(first.comment) and not isPass(second.comment): # type: ignore assert first.comment == second.comment # type: ignore + def _assertEqualDirectiveComponents(first, second): # pragma: no cover """ Handles assertion for instance variables for DirectiveNode and BlockNode""" @@ -50,6 +52,7 @@ def _assertEqualDirectiveComponents(first, second): # pragma: no cover if not isPass(first.parameters) and not isPass(second.parameters): assert first.parameters == second.parameters + def assertEqualDirective(first, second): """ Equality assertion for DirectiveNode """ @@ -57,12 +60,14 @@ def assertEqualDirective(first, second): assert isinstance(second, interfaces.DirectiveNode) _assertEqualDirectiveComponents(first, second) + def isPass(value): # pragma: no cover """Checks if the value is set to PASS""" if isinstance(value, bool): return True return PASS in value + def isPassDirective(block): """ Checks if BlockNode or DirectiveNode should pass the assertion """ @@ -74,6 +79,7 @@ def isPassDirective(block): return True return False + def isPassComment(comment): """ Checks if CommentNode should pass the assertion """ @@ -83,6 +89,7 @@ def isPassComment(comment): return True return False + def isPassNodeList(nodelist): # pragma: no cover """ Checks if a ParserNode in the nodelist should pass the assertion, this function is used for results of find_* methods. Unimplemented find_* @@ -101,11 +108,13 @@ def isPassNodeList(nodelist): # pragma: no cover return isPassDirective(node) return isPassComment(node) + def assertEqualSimple(first, second): """ Simple assertion """ if not isPass(first) and not isPass(second): assert first == second + def isEqualVirtualHost(first, second): """ Checks that two VirtualHost objects are similar. There are some built @@ -126,6 +135,7 @@ def isEqualVirtualHost(first, second): first.ancestor == second.ancestor ) + def assertEqualPathsList(first, second): # pragma: no cover """ Checks that the two lists of file paths match. This assertion allows for wildcard diff --git a/certbot-apache/certbot_apache/_internal/augeasparser.py b/certbot-apache/certbot_apache/_internal/augeasparser.py index 896e17cf8..e3b30c0a0 100644 --- a/certbot-apache/certbot_apache/_internal/augeasparser.py +++ b/certbot-apache/certbot_apache/_internal/augeasparser.py @@ -160,8 +160,7 @@ class AugeasParserNode(interfaces.ParserNode): # remove [...], it's not allowed in Apache configuration and is used # for indexing within Augeas - name = name.split("[")[0] - return name + return name.split("[")[0] class AugeasCommentNode(AugeasParserNode): @@ -170,7 +169,6 @@ class AugeasCommentNode(AugeasParserNode): def __init__(self, **kwargs): comment, kwargs = util.commentnode_kwargs(kwargs) # pylint: disable=unused-variable super().__init__(**kwargs) - # self.comment = comment self.comment = comment def __eq__(self, other): @@ -278,13 +276,14 @@ class AugeasBlockNode(AugeasDirectiveNode): ) # Parameters will be set at the initialization of the new object - new_block = AugeasBlockNode(name=name, - parameters=parameters, - enabled=enabled, - ancestor=assertions.PASS, - filepath=apache_util.get_file_path(realpath), - metadata=new_metadata) - return new_block + return AugeasBlockNode( + name=name, + parameters=parameters, + enabled=enabled, + ancestor=assertions.PASS, + filepath=apache_util.get_file_path(realpath), + metadata=new_metadata, + ) # pylint: disable=unused-argument def add_child_directive(self, name, parameters=None, position=None): # pragma: no cover @@ -308,13 +307,14 @@ class AugeasBlockNode(AugeasDirectiveNode): apache_util.get_file_path(realpath) ) - new_dir = AugeasDirectiveNode(name=name, - parameters=parameters, - enabled=enabled, - ancestor=assertions.PASS, - filepath=apache_util.get_file_path(realpath), - metadata=new_metadata) - return new_dir + return AugeasDirectiveNode( + name=name, + parameters=parameters, + enabled=enabled, + ancestor=assertions.PASS, + filepath=apache_util.get_file_path(realpath), + metadata=new_metadata, + ) def add_child_comment(self, comment="", position=None): """Adds a new CommentNode to the sequence of children""" @@ -330,11 +330,12 @@ class AugeasBlockNode(AugeasDirectiveNode): # Set the comment content self.parser.aug.set(realpath, comment) - new_comment = AugeasCommentNode(comment=comment, - ancestor=assertions.PASS, - filepath=apache_util.get_file_path(realpath), - metadata=new_metadata) - return new_comment + return AugeasCommentNode( + comment=comment, + ancestor=assertions.PASS, + filepath=apache_util.get_file_path(realpath), + metadata=new_metadata, + ) def find_blocks(self, name, exclude=True): """Recursive search of BlockNodes from the sequence of children""" diff --git a/certbot-apache/certbot_apache/_internal/configurator.py b/certbot-apache/certbot_apache/_internal/configurator.py index e6ccdbabf..1decd976c 100644 --- a/certbot-apache/certbot_apache/_internal/configurator.py +++ b/certbot-apache/certbot_apache/_internal/configurator.py @@ -16,9 +16,8 @@ from typing import Union from acme import challenges from certbot import errors -from certbot import interfaces from certbot import util -from certbot.achallenges import KeyAuthorizationAnnotatedChallenge # pylint: disable=unused-import +from certbot.achallenges import KeyAuthorizationAnnotatedChallenge from certbot.compat import filesystem from certbot.compat import os from certbot.display import util as display_util @@ -116,7 +115,7 @@ class OsOptions: # 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(common.Installer, interfaces.Authenticator): +class ApacheConfigurator(common.Configurator): """Apache configurator. :ivar config: Configuration. @@ -486,7 +485,7 @@ class ApacheConfigurator(common.Installer, interfaces.Authenticator): name=assertions.PASS, ancestor=None, filepath=self.parser.loc["root"], - metadata=metadata + metadata=metadata, ) def deploy_cert(self, domain, cert_path, key_path, diff --git a/certbot-apache/certbot_apache/_internal/display_ops.py b/certbot-apache/certbot_apache/_internal/display_ops.py index 86f696173..43de2f995 100644 --- a/certbot-apache/certbot_apache/_internal/display_ops.py +++ b/certbot-apache/certbot_apache/_internal/display_ops.py @@ -103,7 +103,7 @@ def _vhost_menu(domain, vhosts): https="HTTPS" if vhost.ssl else "", active="Enabled" if vhost.enabled else "", fn_size=filename_size, - name_size=disp_name_size) + name_size=disp_name_size), ) try: diff --git a/certbot-apache/certbot_apache/_internal/dualparser.py b/certbot-apache/certbot_apache/_internal/dualparser.py index c89ff95be..7559e3839 100644 --- a/certbot-apache/certbot_apache/_internal/dualparser.py +++ b/certbot-apache/certbot_apache/_internal/dualparser.py @@ -185,8 +185,7 @@ class DualBlockNode(DualNodeBase): primary_new = self.primary.add_child_block(name, parameters, position) secondary_new = self.secondary.add_child_block(name, parameters, position) assertions.assertEqual(primary_new, secondary_new) - new_block = DualBlockNode(primary=primary_new, secondary=secondary_new) - return new_block + return DualBlockNode(primary=primary_new, secondary=secondary_new) def add_child_directive(self, name, parameters=None, position=None): """ Creates a new child DirectiveNode, asserts that both implementations @@ -196,8 +195,7 @@ class DualBlockNode(DualNodeBase): primary_new = self.primary.add_child_directive(name, parameters, position) secondary_new = self.secondary.add_child_directive(name, parameters, position) assertions.assertEqual(primary_new, secondary_new) - new_dir = DualDirectiveNode(primary=primary_new, secondary=secondary_new) - return new_dir + return DualDirectiveNode(primary=primary_new, secondary=secondary_new) def add_child_comment(self, comment="", position=None): """ Creates a new child CommentNode, asserts that both implementations @@ -207,8 +205,7 @@ class DualBlockNode(DualNodeBase): primary_new = self.primary.add_child_comment(comment, position) secondary_new = self.secondary.add_child_comment(comment, position) assertions.assertEqual(primary_new, secondary_new) - new_comment = DualCommentNode(primary=primary_new, secondary=secondary_new) - return new_comment + return DualCommentNode(primary=primary_new, secondary=secondary_new) def _create_matching_list(self, primary_list, secondary_list): """ Matches the list of primary_list to a list of secondary_list and diff --git a/certbot-apache/certbot_apache/_internal/http_01.py b/certbot-apache/certbot_apache/_internal/http_01.py index 120887243..749a57634 100644 --- a/certbot-apache/certbot_apache/_internal/http_01.py +++ b/certbot-apache/certbot_apache/_internal/http_01.py @@ -3,6 +3,7 @@ import errno import logging from typing import List from typing import Set +from typing import TYPE_CHECKING from certbot import errors from certbot.compat import filesystem @@ -11,6 +12,9 @@ from certbot.plugins import common from certbot_apache._internal.obj import VirtualHost # pylint: disable=unused-import from certbot_apache._internal.parser import get_aug_path +if TYPE_CHECKING: + from certbot_apache._internal.configurator import ApacheConfigurator # pragma: no cover + logger = logging.getLogger(__name__) @@ -46,8 +50,9 @@ class ApacheHttp01(common.ChallengePerformer): """ - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) + def __init__(self, configurator: "ApacheConfigurator") -> None: + super().__init__(configurator) + self.configurator: "ApacheConfigurator" self.challenge_conf_pre = os.path.join( self.configurator.conf("challenge-location"), "le_http_01_challenge_pre.conf") diff --git a/certbot-apache/certbot_apache/_internal/obj.py b/certbot-apache/certbot_apache/_internal/obj.py index 9001a860d..4905f971b 100644 --- a/certbot-apache/certbot_apache/_internal/obj.py +++ b/certbot-apache/certbot_apache/_internal/obj.py @@ -176,7 +176,6 @@ class VirtualHost: names=", ".join(self.get_names()), https="Yes" if self.ssl else "No")) - def __eq__(self, other): if isinstance(other, self.__class__): return (self.filep == other.filep and self.path == other.path and diff --git a/certbot-apache/setup.py b/certbot-apache/setup.py index caaa0e2ea..b79912e32 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.22.0.dev0' +version = '1.23.0.dev0' install_requires = [ # We specify the minimum acme and certbot version as the current plugin diff --git a/certbot-ci/certbot_integration_tests/assets/hook.py b/certbot-ci/certbot_integration_tests/assets/hook.py index c11704f47..1b04b2998 100755 --- a/certbot-ci/certbot_integration_tests/assets/hook.py +++ b/certbot-ci/certbot_integration_tests/assets/hook.py @@ -1,9 +1,11 @@ #!/usr/bin/env python +"""A Certbot hook for probing.""" import os import sys hook_script_type = os.path.basename(os.path.dirname(sys.argv[1])) -if hook_script_type == 'deploy' and ('RENEWED_DOMAINS' not in os.environ or 'RENEWED_LINEAGE' not in os.environ): +if hook_script_type == 'deploy' and ('RENEWED_DOMAINS' not in os.environ + or 'RENEWED_LINEAGE' not in os.environ): sys.stderr.write('Environment variables not properly set!\n') sys.exit(1) diff --git a/certbot-ci/certbot_integration_tests/certbot_tests/assertions.py b/certbot-ci/certbot_integration_tests/certbot_tests/assertions.py index c223d524c..92ce8fac8 100644 --- a/certbot-ci/certbot_integration_tests/certbot_tests/assertions.py +++ b/certbot-ci/certbot_integration_tests/certbot_tests/assertions.py @@ -1,9 +1,10 @@ """This module contains advanced assertions for the certbot integration tests.""" import io import os +from typing import Type from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePrivateKey +from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePrivateKey, EllipticCurve from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey from cryptography.hazmat.primitives.serialization import load_pem_private_key @@ -11,7 +12,6 @@ try: import grp POSIX_MODE = True except ImportError: - import win32api import win32security import ntsecuritycon POSIX_MODE = False @@ -21,11 +21,11 @@ SYSTEM_SID = 'S-1-5-18' ADMINS_SID = 'S-1-5-32-544' -def assert_elliptic_key(key, curve): +def assert_elliptic_key(key: str, curve: Type[EllipticCurve]) -> None: """ Asserts that the key at the given path is an EC key using the given curve. :param key: path to key - :param curve: name of the expected elliptic curve + :param EllipticCurve curve: name of the expected elliptic curve """ with open(key, 'rb') as file: privkey1 = file.read() @@ -36,10 +36,10 @@ def assert_elliptic_key(key, curve): assert isinstance(key.curve, curve) -def assert_rsa_key(key): +def assert_rsa_key(key: str) -> None: """ Asserts that the key at the given path is an RSA key. - :param key: path to key + :param str key: path to key """ with open(key, 'rb') as file: privkey1 = file.read() @@ -48,11 +48,11 @@ def assert_rsa_key(key): assert isinstance(key, RSAPrivateKey) -def assert_hook_execution(probe_path, probe_content): +def assert_hook_execution(probe_path: str, probe_content: str) -> None: """ Assert that a certbot hook has been executed - :param probe_path: path to the file that received the hook output - :param probe_content: content expected when the hook is executed + :param str probe_path: path to the file that received the hook output + :param str probe_content: content expected when the hook is executed """ encoding = 'utf-8' if POSIX_MODE else 'utf-16' with io.open(probe_path, 'rt', encoding=encoding) as file: @@ -62,22 +62,22 @@ def assert_hook_execution(probe_path, probe_content): assert probe_content in lines -def assert_saved_renew_hook(config_dir, lineage): +def assert_saved_renew_hook(config_dir: str, lineage: str) -> None: """ Assert that the renew hook configuration of a lineage has been saved. - :param config_dir: location of the certbot configuration - :param lineage: lineage domain name + :param str config_dir: location of the certbot configuration + :param str lineage: lineage domain name """ with open(os.path.join(config_dir, 'renewal', '{0}.conf'.format(lineage))) as file_h: assert 'renew_hook' in file_h.read() -def assert_cert_count_for_lineage(config_dir, lineage, count): +def assert_cert_count_for_lineage(config_dir: str, lineage: str, count: int) -> None: """ Assert the number of certificates generated for a lineage. - :param config_dir: location of the certbot configuration - :param lineage: lineage domain name - :param count: number of expected certificates + :param str config_dir: location of the certbot configuration + :param str lineage: lineage domain name + :param int count: number of expected certificates """ archive_dir = os.path.join(config_dir, 'archive') lineage_dir = os.path.join(archive_dir, lineage) @@ -85,11 +85,11 @@ def assert_cert_count_for_lineage(config_dir, lineage, count): assert len(certs) == count -def assert_equals_group_permissions(file1, file2): +def assert_equals_group_permissions(file1: str, file2: str) -> None: """ Assert that two files have the same permissions for group owner. - :param file1: first file path to compare - :param file2: second file path to compare + :param str file1: first file path to compare + :param str file2: second file path to compare """ # On Windows there is no group, so this assertion does nothing on this platform if POSIX_MODE: @@ -99,11 +99,11 @@ def assert_equals_group_permissions(file1, file2): assert mode_file1 == mode_file2 -def assert_equals_world_read_permissions(file1, file2): +def assert_equals_world_read_permissions(file1: str, file2: str) -> None: """ Assert that two files have the same read permissions for everyone. - :param file1: first file path to compare - :param file2: second file path to compare + :param str file1: first file path to compare + :param str file2: second file path to compare """ if POSIX_MODE: mode_file1 = os.stat(file1).st_mode & 0o004 @@ -134,11 +134,11 @@ def assert_equals_world_read_permissions(file1, file2): assert mode_file1 == mode_file2 -def assert_equals_group_owner(file1, file2): +def assert_equals_group_owner(file1: str, file2: str) -> None: """ Assert that two files have the same group owner. - :param file1: first file path to compare - :param file2: second file path to compare + :param str file1: first file path to compare + :param str file2: second file path to compare """ # On Windows there is no group, so this assertion does nothing on this platform if POSIX_MODE: @@ -148,10 +148,10 @@ def assert_equals_group_owner(file1, file2): assert group_owner_file1 == group_owner_file2 -def assert_world_no_permissions(file): +def assert_world_no_permissions(file: str) -> None: """ Assert that the given file is not world-readable. - :param file: path of the file to check + :param str file: path of the file to check """ if POSIX_MODE: mode_file_all = os.stat(file).st_mode & 0o007 @@ -168,10 +168,10 @@ def assert_world_no_permissions(file): assert not mode -def assert_world_read_permissions(file): +def assert_world_read_permissions(file: str) -> None: """ Assert that the given file is world-readable, but not world-writable or world-executable. - :param file: path of the file to check + :param str file: path of the file to check """ if POSIX_MODE: mode_file_all = os.stat(file).st_mode & 0o007 @@ -188,8 +188,3 @@ def assert_world_read_permissions(file): assert not mode & ntsecuritycon.FILE_GENERIC_WRITE assert not mode & ntsecuritycon.FILE_GENERIC_EXECUTE assert mode & ntsecuritycon.FILE_GENERIC_READ == ntsecuritycon.FILE_GENERIC_READ - - -def _get_current_user(): - account_name = win32api.GetUserNameEx(win32api.NameSamCompatible) - return win32security.LookupAccountName(None, account_name)[0] diff --git a/certbot-ci/certbot_integration_tests/certbot_tests/context.py b/certbot-ci/certbot_integration_tests/certbot_tests/context.py index c86a06754..fdef82252 100644 --- a/certbot-ci/certbot_integration_tests/certbot_tests/context.py +++ b/certbot-ci/certbot_integration_tests/certbot_tests/context.py @@ -3,21 +3,25 @@ import os import shutil import sys import tempfile +from typing import Iterable +from typing import Tuple + +import pytest from certbot_integration_tests.utils import certbot_call class IntegrationTestsContext: """General fixture describing a certbot integration tests context""" - def __init__(self, request): + def __init__(self, request: pytest.FixtureRequest) -> None: self.request = request if hasattr(request.config, 'workerinput'): # Worker node - self.worker_id = request.config.workerinput['workerid'] - acme_xdist = request.config.workerinput['acme_xdist'] + self.worker_id = request.config.workerinput['workerid'] # type: ignore[attr-defined] + acme_xdist = request.config.workerinput['acme_xdist'] # type: ignore[attr-defined] else: # Primary node self.worker_id = 'primary' - acme_xdist = request.config.acme_xdist + acme_xdist = request.config.acme_xdist # type: ignore[attr-defined] self.acme_server = acme_xdist['acme_server'] self.directory_url = acme_xdist['directory_url'] @@ -52,16 +56,17 @@ class IntegrationTestsContext: '"' ).format(sys.executable, self.challtestsrv_port) - def cleanup(self): + def cleanup(self) -> None: """Cleanup the integration test context.""" shutil.rmtree(self.workspace) - def certbot(self, args, force_renew=True): + def certbot(self, args: Iterable[str], force_renew: bool = True) -> Tuple[str, str]: """ Execute certbot with given args, not renewing certificates by default. :param args: args to pass to certbot - :param force_renew: set to False to not renew by default + :param bool force_renew: set to False to not renew by default :return: stdout and stderr from certbot execution + :rtype: Tuple of `str` """ command = ['--authenticator', 'standalone', '--installer', 'null'] command.extend(args) @@ -69,14 +74,15 @@ class IntegrationTestsContext: command, self.directory_url, self.http_01_port, self.tls_alpn_01_port, self.config_dir, self.workspace, force_renew=force_renew) - def get_domain(self, subdomain='le'): + def get_domain(self, subdomain: str = 'le') -> str: """ Generate a certificate domain name suitable for distributed certbot integration tests. This is a requirement to let the distribution know how to redirect the challenge check from the ACME server to the relevant pytest-xdist worker. This resolution is done by appending the pytest worker id to the subdomain, using this pattern: {subdomain}.{worker_id}.wtf - :param subdomain: the subdomain to use in the generated domain (default 'le') + :param str subdomain: the subdomain to use in the generated domain (default 'le') :return: the well-formed domain suitable for redirection on + :rtype: str """ return '{0}.{1}.wtf'.format(subdomain, self.worker_id) 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 12c45088f..0e3a08c8c 100644 --- a/certbot-ci/certbot_integration_tests/certbot_tests/test_main.py +++ b/certbot-ci/certbot_integration_tests/certbot_tests/test_main.py @@ -1,5 +1,4 @@ """Module executing integration tests against certbot core.""" - import os from os.path import exists from os.path import join @@ -7,14 +6,18 @@ import re import shutil import subprocess import time +from typing import Iterable +from typing import Generator +from typing import Type +from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurve from cryptography.hazmat.primitives.asymmetric.ec import SECP256R1 from cryptography.hazmat.primitives.asymmetric.ec import SECP384R1 from cryptography.hazmat.primitives.asymmetric.ec import SECP521R1 from cryptography.x509 import NameOID import pytest -from certbot_integration_tests.certbot_tests import context as certbot_context +from certbot_integration_tests.certbot_tests.context import IntegrationTestsContext from certbot_integration_tests.certbot_tests.assertions import assert_cert_count_for_lineage from certbot_integration_tests.certbot_tests.assertions import assert_elliptic_key from certbot_integration_tests.certbot_tests.assertions import assert_equals_group_owner @@ -30,17 +33,17 @@ from certbot_integration_tests.utils import misc @pytest.fixture(name='context') -def test_context(request): - # pylint: disable=missing-function-docstring +def test_context(request: pytest.FixtureRequest) -> Generator[IntegrationTestsContext, None, None]: + """Fixture providing the integration test context.""" # Fixture request is a built-in pytest fixture describing current test request. - integration_test_context = certbot_context.IntegrationTestsContext(request) + integration_test_context = IntegrationTestsContext(request) try: yield integration_test_context finally: integration_test_context.cleanup() -def test_basic_commands(context): +def test_basic_commands(context: IntegrationTestsContext) -> None: """Test simple commands on Certbot CLI.""" # TMPDIR env variable is set to workspace for the certbot subprocess. # So tempdir module will create any temporary files/dirs in workspace, @@ -58,7 +61,7 @@ def test_basic_commands(context): assert initial_count_tmpfiles == new_count_tmpfiles -def test_hook_dirs_creation(context): +def test_hook_dirs_creation(context: IntegrationTestsContext) -> None: """Test all hooks directory are created during Certbot startup.""" context.certbot(['register']) @@ -66,7 +69,7 @@ def test_hook_dirs_creation(context): assert os.path.isdir(hook_dir) -def test_registration_override(context): +def test_registration_override(context: IntegrationTestsContext) -> None: """Test correct register/unregister, and registration override.""" context.certbot(['register']) context.certbot(['unregister']) @@ -76,14 +79,14 @@ def test_registration_override(context): context.certbot(['update_account', '--email', 'ex1@domain.org,ex2@domain.org']) -def test_prepare_plugins(context): +def test_prepare_plugins(context: IntegrationTestsContext) -> None: """Test that plugins are correctly instantiated and displayed.""" stdout, _ = context.certbot(['plugins', '--init', '--prepare']) assert 'webroot' in stdout -def test_http_01(context): +def test_http_01(context: IntegrationTestsContext) -> None: """Test the HTTP-01 challenge using standalone plugin.""" # We start a server listening on the port for the # TLS-SNI challenge to prevent regressions in #3601. @@ -101,7 +104,7 @@ def test_http_01(context): assert_saved_renew_hook(context.config_dir, certname) -def test_manual_http_auth(context): +def test_manual_http_auth(context: IntegrationTestsContext) -> None: """Test the HTTP-01 challenge using manual plugin.""" with misc.create_http_server(context.http_01_port) as webroot,\ misc.manual_http_hooks(webroot, context.http_01_port) as scripts: @@ -122,7 +125,7 @@ def test_manual_http_auth(context): assert_saved_renew_hook(context.config_dir, certname) -def test_manual_dns_auth(context): +def test_manual_dns_auth(context: IntegrationTestsContext) -> None: """Test the DNS-01 challenge using manual plugin.""" certname = context.get_domain('dns') context.certbot([ @@ -144,14 +147,14 @@ def test_manual_dns_auth(context): assert_cert_count_for_lineage(context.config_dir, certname, 2) -def test_certonly(context): +def test_certonly(context: IntegrationTestsContext) -> None: """Test the certonly verb on certbot.""" context.certbot(['certonly', '--cert-name', 'newname', '-d', context.get_domain('newname')]) assert_cert_count_for_lineage(context.config_dir, 'newname', 1) -def test_certonly_webroot(context): +def test_certonly_webroot(context: IntegrationTestsContext) -> None: """Test the certonly verb with webroot plugin""" with misc.create_http_server(context.http_01_port) as webroot: certname = context.get_domain('webroot') @@ -160,7 +163,7 @@ def test_certonly_webroot(context): assert_cert_count_for_lineage(context.config_dir, certname, 1) -def test_auth_and_install_with_csr(context): +def test_auth_and_install_with_csr(context: IntegrationTestsContext) -> None: """Test certificate issuance and install using an existing CSR.""" certname = context.get_domain('le3') key_path = join(context.workspace, 'key.pem') @@ -187,7 +190,7 @@ def test_auth_and_install_with_csr(context): ]) -def test_renew_files_permissions(context): +def test_renew_files_permissions(context: IntegrationTestsContext) -> None: """Test proper certificate file permissions upon renewal""" certname = context.get_domain('renew') context.certbot(['-d', certname]) @@ -207,7 +210,7 @@ def test_renew_files_permissions(context): assert_equals_group_permissions(privkey1, privkey2) -def test_renew_with_hook_scripts(context): +def test_renew_with_hook_scripts(context: IntegrationTestsContext) -> None: """Test certificate renewal with script hooks.""" certname = context.get_domain('renew') context.certbot(['-d', certname]) @@ -221,7 +224,7 @@ def test_renew_with_hook_scripts(context): assert_hook_execution(context.hook_probe, 'deploy') -def test_renew_files_propagate_permissions(context): +def test_renew_files_propagate_permissions(context: IntegrationTestsContext) -> None: """Test proper certificate renewal with custom permissions propagated on private key.""" certname = context.get_domain('renew') context.certbot(['-d', certname]) @@ -263,7 +266,7 @@ def test_renew_files_propagate_permissions(context): assert_world_no_permissions(privkey2) -def test_graceful_renew_it_is_not_time(context): +def test_graceful_renew_it_is_not_time(context: IntegrationTestsContext) -> None: """Test graceful renew is not done when it is not due time.""" certname = context.get_domain('renew') context.certbot(['-d', certname]) @@ -278,7 +281,7 @@ def test_graceful_renew_it_is_not_time(context): assert_hook_execution(context.hook_probe, 'deploy') -def test_graceful_renew_it_is_time(context): +def test_graceful_renew_it_is_time(context: IntegrationTestsContext) -> None: """Test graceful renew is done when it is due time.""" certname = context.get_domain('renew') context.certbot(['-d', certname]) @@ -298,7 +301,7 @@ def test_graceful_renew_it_is_time(context): assert_hook_execution(context.hook_probe, 'deploy') -def test_renew_with_changed_private_key_complexity(context): +def test_renew_with_changed_private_key_complexity(context: IntegrationTestsContext) -> None: """Test proper renew with updated private key complexity.""" certname = context.get_domain('renew') context.certbot(['-d', certname, '--rsa-key-size', '4096']) @@ -320,7 +323,7 @@ def test_renew_with_changed_private_key_complexity(context): assert os.stat(key3).st_size < 1800 # 2048 bits keys takes less than 1800 bytes -def test_renew_ignoring_directory_hooks(context): +def test_renew_ignoring_directory_hooks(context: IntegrationTestsContext) -> None: """Test hooks are ignored during renewal with relevant CLI flag.""" certname = context.get_domain('renew') context.certbot(['-d', certname]) @@ -335,7 +338,7 @@ def test_renew_ignoring_directory_hooks(context): assert_hook_execution(context.hook_probe, 'deploy') -def test_renew_empty_hook_scripts(context): +def test_renew_empty_hook_scripts(context: IntegrationTestsContext) -> None: """Test proper renew with empty hook scripts.""" certname = context.get_domain('renew') context.certbot(['-d', certname]) @@ -353,7 +356,7 @@ def test_renew_empty_hook_scripts(context): assert_cert_count_for_lineage(context.config_dir, certname, 2) -def test_renew_hook_override(context): +def test_renew_hook_override(context: IntegrationTestsContext) -> None: """Test correct hook override on renew.""" certname = context.get_domain('override') context.certbot([ @@ -398,7 +401,7 @@ def test_renew_hook_override(context): assert_hook_execution(context.hook_probe, 'deploy_override') -def test_invalid_domain_with_dns_challenge(context): +def test_invalid_domain_with_dns_challenge(context: IntegrationTestsContext) -> None: """Test certificate issuance failure with DNS-01 challenge.""" # Manual dns auth hooks from misc are designed to fail if the domain contains 'fail-*'. domains = ','.join([context.get_domain('dns1'), context.get_domain('fail-dns1')]) @@ -415,7 +418,7 @@ def test_invalid_domain_with_dns_challenge(context): assert context.get_domain('fail-dns1') not in stdout -def test_reuse_key(context): +def test_reuse_key(context: IntegrationTestsContext) -> None: """Test various scenarios where a key is reused.""" certname = context.get_domain('reusekey') context.certbot(['--domains', certname, '--reuse-key']) @@ -458,12 +461,12 @@ def test_reuse_key(context): assert len({cert1, cert2, cert3}) == 3 -def test_incorrect_key_type(context): +def test_incorrect_key_type(context: IntegrationTestsContext) -> None: with pytest.raises(subprocess.CalledProcessError): context.certbot(['--key-type="failwhale"']) -def test_ecdsa(context): +def test_ecdsa(context: IntegrationTestsContext) -> None: """Test issuance for ECDSA CSR based request (legacy supported mode).""" key_path = join(context.workspace, 'privkey-p384.pem') csr_path = join(context.workspace, 'csr-p384.der') @@ -484,7 +487,7 @@ def test_ecdsa(context): assert 'ASN1 OID: secp384r1' in certificate -def test_default_key_type(context): +def test_default_key_type(context: IntegrationTestsContext) -> None: """Test default key type is RSA""" certname = context.get_domain('renew') context.certbot([ @@ -495,7 +498,7 @@ def test_default_key_type(context): assert_rsa_key(filename) -def test_default_curve_type(context): +def test_default_curve_type(context: IntegrationTestsContext) -> None: """test that the curve used when not specifying any is secp256r1""" certname = context.get_domain('renew') context.certbot([ @@ -511,7 +514,8 @@ def test_default_curve_type(context): ('secp384r1', SECP384R1, []), ('secp521r1', SECP521R1, ['boulder-v1', 'boulder-v2'])] ) -def test_ecdsa_curves(context, curve, curve_cls, skip_servers): +def test_ecdsa_curves(context: IntegrationTestsContext, curve: str, curve_cls: Type[EllipticCurve], + skip_servers: Iterable[str]) -> None: """Test issuance for each supported ECDSA curve""" if context.acme_server in skip_servers: pytest.skip('ACME server {} does not support ECDSA curve {}' @@ -527,7 +531,7 @@ def test_ecdsa_curves(context, curve, curve_cls, skip_servers): assert_elliptic_key(key, curve_cls) -def test_renew_with_ec_keys(context): +def test_renew_with_ec_keys(context: IntegrationTestsContext) -> None: """Test proper renew with updated private key complexity.""" certname = context.get_domain('renew') context.certbot([ @@ -567,7 +571,7 @@ def test_renew_with_ec_keys(context): assert_rsa_key(key3) -def test_ocsp_must_staple(context): +def test_ocsp_must_staple(context: IntegrationTestsContext) -> None: """Test that OCSP Must-Staple is correctly set in the generated certificate.""" if context.acme_server == 'pebble': pytest.skip('Pebble does not support OCSP Must-Staple.') @@ -580,7 +584,7 @@ def test_ocsp_must_staple(context): assert 'status_request' in certificate or '1.3.6.1.5.5.7.1.24' in certificate -def test_revoke_simple(context): +def test_revoke_simple(context: IntegrationTestsContext) -> None: """Test various scenarios that revokes a certificate.""" # Default action after revoke is to delete the certificate. certname = context.get_domain() @@ -611,7 +615,7 @@ def test_revoke_simple(context): context.certbot(['revoke', '--cert-path', cert_path, '--key-path', key_path]) -def test_revoke_and_unregister(context): +def test_revoke_and_unregister(context: IntegrationTestsContext) -> None: """Test revoke with a reason then unregister.""" cert1 = context.get_domain('le1') cert2 = context.get_domain('le2') @@ -639,7 +643,7 @@ def test_revoke_and_unregister(context): assert cert3 in stdout -def test_revoke_mutual_exclusive_flags(context): +def test_revoke_mutual_exclusive_flags(context: IntegrationTestsContext) -> None: """Test --cert-path and --cert-name cannot be used during revoke.""" cert = context.get_domain('le1') context.certbot(['-d', cert]) @@ -651,7 +655,7 @@ def test_revoke_mutual_exclusive_flags(context): assert 'Exactly one of --cert-path or --cert-name must be specified' in error.value.stderr -def test_revoke_multiple_lineages(context): +def test_revoke_multiple_lineages(context: IntegrationTestsContext) -> None: """Test revoke does not delete certs if multiple lineages share the same dir.""" cert1 = context.get_domain('le1') context.certbot(['-d', cert1]) @@ -683,7 +687,7 @@ def test_revoke_multiple_lineages(context): assert 'Not deleting revoked certificates due to overlapping archive dirs' in f.read() -def test_wildcard_certificates(context): +def test_wildcard_certificates(context: IntegrationTestsContext) -> None: """Test wildcard certificate issuance.""" if context.acme_server == 'boulder-v1': pytest.skip('Wildcard certificates are not supported on ACME v1') @@ -700,7 +704,7 @@ def test_wildcard_certificates(context): assert exists(join(context.config_dir, 'live', certname, 'fullchain.pem')) -def test_ocsp_status_stale(context): +def test_ocsp_status_stale(context: IntegrationTestsContext) -> None: """Test retrieval of OCSP statuses for staled config""" sample_data_path = misc.load_sample_data_path(context.workspace) stdout, _ = context.certbot(['certificates', '--config-dir', sample_data_path]) @@ -711,7 +715,7 @@ def test_ocsp_status_stale(context): .format(stdout.count('EXPIRED'))) -def test_ocsp_status_live(context): +def test_ocsp_status_live(context: IntegrationTestsContext) -> None: """Test retrieval of OCSP statuses for live config""" cert = context.get_domain('ocsp-check') @@ -733,7 +737,7 @@ def test_ocsp_status_live(context): assert stdout.count('REVOKED') == 1, 'Expected {0} to be REVOKED'.format(cert) -def test_ocsp_renew(context): +def test_ocsp_renew(context: IntegrationTestsContext) -> None: """Test that revoked certificates are renewed.""" # Obtain a certificate certname = context.get_domain('ocsp-renew') @@ -750,7 +754,7 @@ def test_ocsp_renew(context): assert_cert_count_for_lineage(context.config_dir, certname, 2) -def test_dry_run_deactivate_authzs(context): +def test_dry_run_deactivate_authzs(context: IntegrationTestsContext) -> None: """Test that Certbot deactivates authorizations when performing a dry run""" name = context.get_domain('dry-run-authz-deactivation') @@ -768,7 +772,7 @@ def test_dry_run_deactivate_authzs(context): assert log_line in f.read(), 'Second order should have been recreated due to authz reuse' -def test_preferred_chain(context): +def test_preferred_chain(context: IntegrationTestsContext) -> None: """Test that --preferred-chain results in the correct chain.pem being produced""" try: issuers = misc.get_acme_issuers(context) diff --git a/certbot-ci/certbot_integration_tests/conftest.py b/certbot-ci/certbot_integration_tests/conftest.py index 5e6ed5562..deb9a5228 100644 --- a/certbot-ci/certbot_integration_tests/conftest.py +++ b/certbot-ci/certbot_integration_tests/conftest.py @@ -1,3 +1,4 @@ +# type: ignore """ General conftest for pytest execution of all integration tests lying in the certbot_integration tests package. diff --git a/certbot-ci/certbot_integration_tests/nginx_tests/context.py b/certbot-ci/certbot_integration_tests/nginx_tests/context.py index f273ed9ff..2a8881aa9 100644 --- a/certbot-ci/certbot_integration_tests/nginx_tests/context.py +++ b/certbot-ci/certbot_integration_tests/nginx_tests/context.py @@ -1,6 +1,10 @@ """Module to handle the context of nginx integration tests.""" import os import subprocess +from typing import Iterable +from typing import Tuple + +import pytest from certbot_integration_tests.certbot_tests import context as certbot_context from certbot_integration_tests.nginx_tests import nginx_config as config @@ -10,7 +14,7 @@ from certbot_integration_tests.utils import misc class IntegrationTestsContext(certbot_context.IntegrationTestsContext): """General fixture describing a certbot-nginx integration tests context""" - def __init__(self, request): + def __init__(self, request: pytest.FixtureRequest) -> None: super().__init__(request) self.nginx_root = os.path.join(self.workspace, 'nginx') @@ -22,16 +26,16 @@ class IntegrationTestsContext(certbot_context.IntegrationTestsContext): file_handler.write('Hello World!') self.nginx_config_path = os.path.join(self.nginx_root, 'nginx.conf') - self.nginx_config = None + self.nginx_config: str - default_server = request.param['default_server'] + default_server = request.param['default_server'] # type: ignore[attr-defined] self.process = self._start_nginx(default_server) - def cleanup(self): + def cleanup(self) -> None: self._stop_nginx() super().cleanup() - def certbot_test_nginx(self, args): + def certbot_test_nginx(self, args: Iterable[str]) -> Tuple[str, str]: """ Main command to execute certbot using the nginx plugin. :param list args: list of arguments to pass to nginx @@ -44,7 +48,7 @@ class IntegrationTestsContext(certbot_context.IntegrationTestsContext): command, self.directory_url, self.http_01_port, self.tls_alpn_01_port, self.config_dir, self.workspace, force_renew=True) - def _start_nginx(self, default_server): + def _start_nginx(self, default_server: bool) -> subprocess.Popen: self.nginx_config = config.construct_nginx_config( self.nginx_root, self.webroot, self.http_01_port, self.tls_alpn_01_port, self.other_port, default_server, wtf_prefix=self.worker_id) @@ -58,7 +62,7 @@ class IntegrationTestsContext(certbot_context.IntegrationTestsContext): misc.check_until_timeout('http://localhost:{0}'.format(self.http_01_port)) return process - def _stop_nginx(self): + def _stop_nginx(self) -> None: assert self.process.poll() is None self.process.terminate() self.process.wait() diff --git a/certbot-ci/certbot_integration_tests/nginx_tests/nginx_config.py b/certbot-ci/certbot_integration_tests/nginx_tests/nginx_config.py index bbbe8ea06..60f1696a8 100644 --- a/certbot-ci/certbot_integration_tests/nginx_tests/nginx_config.py +++ b/certbot-ci/certbot_integration_tests/nginx_tests/nginx_config.py @@ -1,12 +1,14 @@ # -*- coding: utf-8 -*- """General purpose nginx test configuration generator.""" import getpass +from typing import Optional import pkg_resources -def construct_nginx_config(nginx_root, nginx_webroot, http_port, https_port, other_port, - default_server, key_path=None, cert_path=None, wtf_prefix='le'): +def construct_nginx_config(nginx_root: str, nginx_webroot: str, http_port: int, https_port: int, + other_port: int, default_server: bool, key_path: Optional[str] = None, + cert_path: Optional[str] = None, wtf_prefix: str = 'le') -> str: """ This method returns a full nginx configuration suitable for integration tests. :param str nginx_root: nginx root configuration path diff --git a/certbot-ci/certbot_integration_tests/nginx_tests/test_main.py b/certbot-ci/certbot_integration_tests/nginx_tests/test_main.py index 2c52c8523..f9e6554ca 100644 --- a/certbot-ci/certbot_integration_tests/nginx_tests/test_main.py +++ b/certbot-ci/certbot_integration_tests/nginx_tests/test_main.py @@ -1,17 +1,18 @@ """Module executing integration tests against certbot with nginx plugin.""" import os import ssl +from typing import Generator from typing import List import pytest -from certbot_integration_tests.nginx_tests import context as nginx_context +from certbot_integration_tests.nginx_tests.context import IntegrationTestsContext @pytest.fixture(name='context') -def test_context(request): +def test_context(request: pytest.FixtureRequest) -> Generator[IntegrationTestsContext, None, None]: # Fixture request is a built-in pytest fixture describing current test request. - integration_test_context = nginx_context.IntegrationTestsContext(request) + integration_test_context = IntegrationTestsContext(request) try: yield integration_test_context finally: @@ -33,7 +34,7 @@ def test_context(request): ], {'default_server': False}), ], indirect=['context']) def test_certificate_deployment(certname_pattern: str, params: List[str], - context: nginx_context.IntegrationTestsContext) -> None: + context: IntegrationTestsContext) -> None: """ Test various scenarios to deploy a certificate to nginx using certbot. """ diff --git a/certbot-ci/certbot_integration_tests/py.typed b/certbot-ci/certbot_integration_tests/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/certbot-ci/certbot_integration_tests/rfc2136_tests/context.py b/certbot-ci/certbot_integration_tests/rfc2136_tests/context.py index 3d4147313..dde41f367 100644 --- a/certbot-ci/certbot_integration_tests/rfc2136_tests/context.py +++ b/certbot-ci/certbot_integration_tests/rfc2136_tests/context.py @@ -2,9 +2,12 @@ from contextlib import contextmanager import tempfile +from typing import Generator +from typing import Iterable +from typing import Tuple from pkg_resources import resource_filename -from pytest import skip +import pytest from certbot_integration_tests.certbot_tests import context as certbot_context from certbot_integration_tests.utils import certbot_call @@ -12,17 +15,17 @@ from certbot_integration_tests.utils import certbot_call class IntegrationTestsContext(certbot_context.IntegrationTestsContext): """Integration test context for certbot-dns-rfc2136""" - def __init__(self, request): + def __init__(self, request: pytest.FixtureRequest) -> None: super().__init__(request) self.request = request if hasattr(request.config, 'workerinput'): # Worker node - self._dns_xdist = request.config.workerinput['dns_xdist'] + self._dns_xdist = request.config.workerinput['dns_xdist'] # type: ignore[attr-defined] else: # Primary node - self._dns_xdist = request.config.dns_xdist + self._dns_xdist = request.config.dns_xdist # type: ignore[attr-defined] - def certbot_test_rfc2136(self, args): + def certbot_test_rfc2136(self, args: Iterable[str]) -> Tuple[str, str]: """ Main command to execute certbot using the RFC2136 DNS authenticator. :param list args: list of arguments to pass to Certbot @@ -34,7 +37,7 @@ class IntegrationTestsContext(certbot_context.IntegrationTestsContext): self.config_dir, self.workspace, force_renew=True) @contextmanager - def rfc2136_credentials(self, label='default'): + def rfc2136_credentials(self, label: str = 'default') -> Generator[str, None, None]: """ Produces the contents of a certbot-dns-rfc2136 credentials file. :param str label: which RFC2136 credential to use @@ -57,8 +60,8 @@ class IntegrationTestsContext(certbot_context.IntegrationTestsContext): fp.flush() yield fp.name - def skip_if_no_bind9_server(self): + def skip_if_no_bind9_server(self) -> None: """Skips the test if there was no RFC2136-capable DNS server configured in the test environment""" if not self._dns_xdist: - skip('No RFC2136-capable DNS server is configured') + pytest.skip('No RFC2136-capable DNS server is configured') diff --git a/certbot-ci/certbot_integration_tests/rfc2136_tests/test_main.py b/certbot-ci/certbot_integration_tests/rfc2136_tests/test_main.py index ae6c0018e..9466934a8 100644 --- a/certbot-ci/certbot_integration_tests/rfc2136_tests/test_main.py +++ b/certbot-ci/certbot_integration_tests/rfc2136_tests/test_main.py @@ -1,14 +1,16 @@ """Module executing integration tests against Certbot with the RFC2136 DNS authenticator.""" +from typing import Generator + import pytest -from certbot_integration_tests.rfc2136_tests import context as rfc2136_context +from certbot_integration_tests.rfc2136_tests.context import IntegrationTestsContext @pytest.fixture(name="context") -def pytest_context(request): +def test_context(request: pytest.FixtureRequest) -> Generator[IntegrationTestsContext, None, None]: # pylint: disable=missing-function-docstring # Fixture request is a built-in pytest fixture describing current test request. - integration_test_context = rfc2136_context.IntegrationTestsContext(request) + integration_test_context = IntegrationTestsContext(request) try: yield integration_test_context finally: @@ -16,7 +18,7 @@ def pytest_context(request): @pytest.mark.parametrize('domain', [('example.com'), ('sub.example.com')]) -def test_get_certificate(domain, context): +def test_get_certificate(domain: str, context: IntegrationTestsContext) -> None: context.skip_if_no_bind9_server() with context.rfc2136_credentials() as creds: diff --git a/certbot-ci/certbot_integration_tests/utils/acme_server.py b/certbot-ci/certbot_integration_tests/utils/acme_server.py index 65b78faad..611b33d0c 100755 --- a/certbot-ci/certbot_integration_tests/utils/acme_server.py +++ b/certbot-ci/certbot_integration_tests/utils/acme_server.py @@ -11,7 +11,15 @@ import subprocess import sys import tempfile import time +from types import TracebackType +from typing import Any +from typing import cast +from typing import Dict from typing import List +from typing import Mapping +from typing import Optional +from typing import Sequence +from typing import Type import requests @@ -34,8 +42,9 @@ class ACMEServer: ACMEServer is also a context manager, and so can be used to ensure ACME server is started/stopped upon context enter/exit. """ - def __init__(self, acme_server, nodes, http_proxy=True, stdout=False, - dns_server=None, http_01_port=DEFAULT_HTTP_01_PORT): + def __init__(self, acme_server: str, nodes: Sequence[str], http_proxy: bool = True, + stdout: bool = False, dns_server: Optional[str] = None, + http_01_port: int = DEFAULT_HTTP_01_PORT) -> None: """ Create an ACMEServer instance. :param str acme_server: the type of acme server used (boulder-v1, boulder-v2 or pebble) @@ -60,7 +69,7 @@ class ACMEServer: raise ValueError('setting http_01_port is not currently supported ' 'with boulder or the HTTP proxy') - def start(self): + def start(self) -> None: """Start the test stack""" try: if self._proxy: @@ -73,7 +82,7 @@ class ACMEServer: self.stop() raise e - def stop(self): + def stop(self) -> None: """Stop the test stack, and clean its resources""" print('=> Tear down the test infrastructure...') try: @@ -104,14 +113,15 @@ class ACMEServer: self._stdout.close() print('=> Test infrastructure stopped and cleaned up.') - def __enter__(self): + def __enter__(self) -> Dict[str, Any]: self.start() return self.acme_xdist - def __exit__(self, exc_type, exc_val, exc_tb): + def __exit__(self, exc_type: Optional[Type[BaseException]], exc: Optional[BaseException], + traceback: Optional[TracebackType]) -> None: self.stop() - def _construct_acme_xdist(self, acme_server, nodes): + def _construct_acme_xdist(self, acme_server: str, nodes: Sequence[str]) -> None: """Generate and return the acme_xdist dict""" acme_xdist = {'acme_server': acme_server, 'challtestsrv_port': CHALLTESTSRV_PORT} @@ -138,7 +148,7 @@ class ACMEServer: self.acme_xdist = acme_xdist - def _prepare_pebble_server(self): + def _prepare_pebble_server(self) -> None: """Configure and launch the Pebble server""" print('=> Starting pebble instance deployment...') pebble_artifacts_rv = pebble_artifacts.fetch(self._workspace, self._http_01_port) @@ -174,11 +184,11 @@ class ACMEServer: # Wait for the ACME CA server to be up. print('=> Waiting for pebble instance to respond...') - misc.check_until_timeout(self.acme_xdist['directory_url']) + misc.check_until_timeout(self.acme_xdist['directory_url']) # type: ignore[arg-type] print('=> Finished pebble instance deployment.') - def _prepare_boulder_server(self): + def _prepare_boulder_server(self) -> None: """Configure and launch the Boulder server""" print('=> Starting boulder instance deployment...') instance_path = join(self._workspace, 'boulder') @@ -207,7 +217,8 @@ class ACMEServer: # Wait for the ACME CA server to be up. print('=> Waiting for boulder instance to respond...') - misc.check_until_timeout(self.acme_xdist['directory_url'], attempts=300) + misc.check_until_timeout( + self.acme_xdist['directory_url'], attempts=300) # type: ignore[arg-type] if not self._dns_server: # Configure challtestsrv to answer any A record request with ip of the docker host. @@ -226,16 +237,19 @@ class ACMEServer: print('=> Finished boulder instance deployment.') - def _prepare_http_proxy(self): + def _prepare_http_proxy(self) -> None: """Configure and launch an HTTP proxy""" print('=> Configuring the HTTP proxy...') + http_port_map = cast(Dict[str, int], self.acme_xdist['http_port']) mapping = {r'.+\.{0}\.wtf'.format(node): 'http://127.0.0.1:{0}'.format(port) - for node, port in self.acme_xdist['http_port'].items()} + for node, port in http_port_map.items()} command = [sys.executable, proxy.__file__, str(DEFAULT_HTTP_01_PORT), json.dumps(mapping)] self._launch_process(command) print('=> Finished configuring the HTTP proxy.') - def _launch_process(self, command, cwd=os.getcwd(), env=None, force_stderr=False): + def _launch_process(self, command: Sequence[str], cwd: str = os.getcwd(), + env: Optional[Mapping[str, str]] = None, + force_stderr: bool = False) -> subprocess.Popen: """Launch silently a subprocess OS command""" if not env: env = os.environ @@ -248,7 +262,7 @@ class ACMEServer: return process -def main(): +def main() -> None: # pylint: disable=missing-function-docstring parser = argparse.ArgumentParser( description='CLI tool to start a local instance of Pebble or Boulder CA server.') diff --git a/certbot-ci/certbot_integration_tests/utils/certbot_call.py b/certbot-ci/certbot_integration_tests/utils/certbot_call.py index ffd14ac4b..b79b35912 100755 --- a/certbot-ci/certbot_integration_tests/utils/certbot_call.py +++ b/certbot-ci/certbot_integration_tests/utils/certbot_call.py @@ -5,14 +5,20 @@ import os import pkg_resources import subprocess import sys +from typing import Dict +from typing import List +from typing import Mapping +from typing import Sequence +from typing import Tuple import certbot_integration_tests # pylint: disable=wildcard-import,unused-wildcard-import from certbot_integration_tests.utils.constants import * -def certbot_test(certbot_args, directory_url, http_01_port, tls_alpn_01_port, - config_dir, workspace, force_renew=True): +def certbot_test(certbot_args: Sequence[str], directory_url: str, http_01_port: int, + tls_alpn_01_port: int, config_dir: str, workspace: str, + force_renew: bool = True) -> Tuple[str, str]: """ Invoke the certbot executable available in PATH in a test context for the given args. The test context consists in running certbot in debug mode, with various flags suitable @@ -40,7 +46,7 @@ def certbot_test(certbot_args, directory_url, http_01_port, tls_alpn_01_port, return proc.stdout, proc.stderr -def _prepare_environ(workspace): +def _prepare_environ(workspace: str) -> Dict[str, str]: # pylint: disable=missing-function-docstring new_environ = os.environ.copy() @@ -78,7 +84,8 @@ def _prepare_environ(workspace): return new_environ -def _compute_additional_args(workspace, environ, force_renew): +def _compute_additional_args(workspace: str, environ: Mapping[str, str], + force_renew: bool) -> List[str]: additional_args = [] output = subprocess.check_output(['certbot', '--version'], universal_newlines=True, stderr=subprocess.STDOUT, @@ -94,8 +101,9 @@ def _compute_additional_args(workspace, environ, force_renew): return additional_args -def _prepare_args_env(certbot_args, directory_url, http_01_port, tls_alpn_01_port, - config_dir, workspace, force_renew): +def _prepare_args_env(certbot_args: Sequence[str], directory_url: str, http_01_port: int, + tls_alpn_01_port: int, config_dir: str, workspace: str, + force_renew: bool) -> Tuple[List[str], Dict[str, str]]: new_environ = _prepare_environ(workspace) additional_args = _compute_additional_args(workspace, new_environ, force_renew) @@ -126,7 +134,7 @@ def _prepare_args_env(certbot_args, directory_url, http_01_port, tls_alpn_01_por return command, new_environ -def main(): +def main() -> None: # pylint: disable=missing-function-docstring args = sys.argv[1:] diff --git a/certbot-ci/certbot_integration_tests/utils/dns_server.py b/certbot-ci/certbot_integration_tests/utils/dns_server.py index c4bbcaea1..f74a5da40 100644 --- a/certbot-ci/certbot_integration_tests/utils/dns_server.py +++ b/certbot-ci/certbot_integration_tests/utils/dns_server.py @@ -8,7 +8,11 @@ import subprocess import sys import tempfile import time +from types import TracebackType +from typing import Any, Sequence +from typing import Dict from typing import Optional +from typing import Type from pkg_resources import resource_filename @@ -30,7 +34,7 @@ class DNSServer: future to support parallelization (https://github.com/certbot/certbot/issues/8455). """ - def __init__(self, unused_nodes, show_output=False): + def __init__(self, unused_nodes: Sequence[str], show_output: bool = False) -> None: """ Create an DNSServer instance. :param list nodes: list of node names that will be setup by pytest xdist @@ -48,7 +52,7 @@ class DNSServer: # pylint: disable=consider-using-with self._output = sys.stderr if show_output else open(os.devnull, "w") - def start(self): + def start(self) -> None: """Start the DNS server""" try: self._configure_bind() @@ -57,7 +61,7 @@ class DNSServer: self.stop() raise - def stop(self): + def stop(self) -> None: """Stop the DNS server, and clean its resources""" if self.process: try: @@ -71,7 +75,7 @@ class DNSServer: if self._output != sys.stderr: self._output.close() - def _configure_bind(self): + def _configure_bind(self) -> None: """Configure the BIND9 server based on the prebaked configuration""" bind_conf_src = resource_filename( "certbot_integration_tests", "assets/bind-config" @@ -81,7 +85,7 @@ class DNSServer: os.path.join(bind_conf_src, directory), os.path.join(self.bind_root, directory) ) - def _start_bind(self): + def _start_bind(self) -> None: """Launch the BIND9 server as a Docker container""" addr_str = "{}:{}".format(BIND_BIND_ADDRESS[0], BIND_BIND_ADDRESS[1]) # pylint: disable=consider-using-with @@ -150,9 +154,10 @@ class DNSServer: "Gave up waiting for DNS server {} to respond".format(BIND_BIND_ADDRESS) ) - def __enter__(self): + def __start__(self) -> Dict[str, Any]: self.start() return self.dns_xdist - def __exit__(self, exc_type, exc_val, exc_tb): + def __exit__(self, exc_type: Optional[Type[BaseException]], exc: Optional[BaseException], + traceback: Optional[TracebackType]) -> None: self.stop() diff --git a/certbot-ci/certbot_integration_tests/utils/misc.py b/certbot-ci/certbot_integration_tests/utils/misc.py index c92e08a74..9143804f9 100644 --- a/certbot-ci/certbot_integration_tests/utils/misc.py +++ b/certbot-ci/certbot_integration_tests/utils/misc.py @@ -15,6 +15,11 @@ import sys import tempfile import time import warnings +from typing import Generator +from typing import Iterable +from typing import List +from typing import Optional +from typing import Tuple from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.asymmetric import ec @@ -22,10 +27,12 @@ from cryptography.hazmat.primitives.serialization import Encoding from cryptography.hazmat.primitives.serialization import NoEncryption from cryptography.hazmat.primitives.serialization import PrivateFormat from cryptography.x509 import load_pem_x509_certificate +from cryptography.x509 import Certificate from OpenSSL import crypto import pkg_resources import requests +from certbot_integration_tests.certbot_tests.context import IntegrationTestsContext from certbot_integration_tests.utils.constants import PEBBLE_ALTERNATE_ROOTS from certbot_integration_tests.utils.constants import PEBBLE_MANAGEMENT_URL @@ -33,7 +40,7 @@ RSA_KEY_TYPE = 'rsa' ECDSA_KEY_TYPE = 'ecdsa' -def _suppress_x509_verification_warnings(): +def _suppress_x509_verification_warnings() -> None: try: import urllib3 urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) @@ -44,7 +51,7 @@ def _suppress_x509_verification_warnings(): requests.packages.urllib3.disable_warnings(InsecureRequestWarning) -def check_until_timeout(url, attempts=30): +def check_until_timeout(url: str, attempts: int = 30) -> None: """ Wait and block until given url responds with status 200, or raise an exception after the specified number of attempts. @@ -72,12 +79,12 @@ class GracefulTCPServer(socketserver.TCPServer): allow_reuse_address = True -def _run_server(port): +def _run_server(port: int) -> None: GracefulTCPServer(('', port), SimpleHTTPServer.SimpleHTTPRequestHandler).serve_forever() @contextlib.contextmanager -def create_http_server(port): +def create_http_server(port: int) -> Generator[str, None, None]: """ Setup and start an HTTP server for the given TCP port. This server stays active for the lifetime of the context, and is automatically @@ -111,7 +118,7 @@ def create_http_server(port): shutil.rmtree(webroot) -def list_renewal_hooks_dirs(config_dir): +def list_renewal_hooks_dirs(config_dir: str) -> List[str]: """ Find and return paths of all hook directories for the given certbot config directory :param str config_dir: path to the certbot config directory @@ -121,14 +128,14 @@ def list_renewal_hooks_dirs(config_dir): return [os.path.join(renewal_hooks_root, item) for item in ['pre', 'deploy', 'post']] -def generate_test_file_hooks(config_dir, hook_probe): +def generate_test_file_hooks(config_dir: str, hook_probe: str) -> None: """ Create a suite of certbot hook scripts and put them in the relevant hook directory for the given certbot configuration directory. These scripts, when executed, will write specific verbs in the given hook_probe file to allow asserting they have effectively been executed. The deploy hook also checks that the renewal environment variables are set. :param str config_dir: current certbot config directory - :param hook_probe: path to the hook probe to test hook scripts execution + :param str hook_probe: path to the hook probe to test hook scripts execution """ hook_path = pkg_resources.resource_filename('certbot_integration_tests', 'assets/hook.py') @@ -163,7 +170,8 @@ set -e @contextlib.contextmanager -def manual_http_hooks(http_server_root, http_port): +def manual_http_hooks(http_server_root: str, + http_port: int) -> Generator[Tuple[str, str], None, None]: """ Generate suitable http-01 hooks command for test purpose in the given HTTP server webroot directory. These hooks command use temporary python scripts @@ -216,7 +224,8 @@ shutil.rmtree(well_known) shutil.rmtree(tempdir) -def generate_csr(domains, key_path, csr_path, key_type=RSA_KEY_TYPE): +def generate_csr(domains: Iterable[str], key_path: str, csr_path: str, + key_type: str = RSA_KEY_TYPE) -> None: """ Generate a private key, and a CSR for the given domains using this key. :param domains: the domain names to include in the CSR @@ -260,7 +269,7 @@ def generate_csr(domains, key_path, csr_path, key_type=RSA_KEY_TYPE): file_h.write(crypto.dump_certificate_request(crypto.FILETYPE_ASN1, req)) -def read_certificate(cert_path): +def read_certificate(cert_path: str) -> str: """ Load the certificate from the provided path, and return a human readable version of it (TEXT mode). @@ -274,7 +283,7 @@ def read_certificate(cert_path): return crypto.dump_certificate(crypto.FILETYPE_TEXT, cert).decode('utf-8') -def load_sample_data_path(workspace): +def load_sample_data_path(workspace: str) -> str: """ Load the certbot configuration example designed to make OCSP tests, and return its path :param str workspace: current test workspace directory path @@ -305,7 +314,7 @@ def load_sample_data_path(workspace): return copied -def echo(keyword, path=None): +def echo(keyword: str, path: Optional[str] = None) -> str: """ Generate a platform independent executable command that echoes the given keyword into the given file. @@ -320,7 +329,7 @@ def echo(keyword, path=None): os.path.basename(sys.executable), keyword, ' >> "{0}"'.format(path) if path else '') -def get_acme_issuers(context): +def get_acme_issuers(context: IntegrationTestsContext) -> List[Certificate]: """Gets the list of one or more issuer certificates from the ACME server used by the context. :param context: the testing context. diff --git a/certbot-ci/certbot_integration_tests/utils/pebble_artifacts.py b/certbot-ci/certbot_integration_tests/utils/pebble_artifacts.py index 918a5fd04..b929a14fe 100644 --- a/certbot-ci/certbot_integration_tests/utils/pebble_artifacts.py +++ b/certbot-ci/certbot_integration_tests/utils/pebble_artifacts.py @@ -3,6 +3,7 @@ import json import os import stat +from typing import Tuple import pkg_resources import requests @@ -14,7 +15,7 @@ PEBBLE_VERSION = 'v2.3.0' ASSETS_PATH = pkg_resources.resource_filename('certbot_integration_tests', 'assets') -def fetch(workspace, http_01_port=DEFAULT_HTTP_01_PORT): +def fetch(workspace: str, http_01_port: int = DEFAULT_HTTP_01_PORT) -> Tuple[str, str, str]: # pylint: disable=missing-function-docstring suffix = 'linux-amd64' if os.name != 'nt' else 'windows-amd64.exe' @@ -25,7 +26,7 @@ def fetch(workspace, http_01_port=DEFAULT_HTTP_01_PORT): return pebble_path, challtestsrv_path, pebble_config_path -def _fetch_asset(asset, suffix): +def _fetch_asset(asset: str, suffix: str) -> str: asset_path = os.path.join(ASSETS_PATH, '{0}_{1}_{2}'.format(asset, PEBBLE_VERSION, suffix)) if not os.path.exists(asset_path): asset_url = ('https://github.com/letsencrypt/pebble/releases/download/{0}/{1}_{2}' @@ -39,7 +40,7 @@ def _fetch_asset(asset, suffix): return asset_path -def _build_pebble_config(workspace, http_01_port): +def _build_pebble_config(workspace: str, http_01_port: int) -> str: config_path = os.path.join(workspace, 'pebble-config.json') with open(config_path, 'w') as file_h: file_h.write(json.dumps({ diff --git a/certbot-ci/certbot_integration_tests/utils/pebble_ocsp_server.py b/certbot-ci/certbot_integration_tests/utils/pebble_ocsp_server.py index 3458929ad..6fe966ac6 100755 --- a/certbot-ci/certbot_integration_tests/utils/pebble_ocsp_server.py +++ b/certbot-ci/certbot_integration_tests/utils/pebble_ocsp_server.py @@ -22,7 +22,7 @@ from certbot_integration_tests.utils.misc import GracefulTCPServer class _ProxyHandler(BaseHTTPServer.BaseHTTPRequestHandler): # pylint: disable=missing-function-docstring - def do_POST(self): + def do_POST(self) -> None: request = requests.get(PEBBLE_MANAGEMENT_URL + '/intermediate-keys/0', verify=False) issuer_key = serialization.load_pem_private_key(request.content, None, default_backend()) diff --git a/certbot-ci/certbot_integration_tests/utils/proxy.py b/certbot-ci/certbot_integration_tests/utils/proxy.py index 7c46e640d..3f8e099cf 100644 --- a/certbot-ci/certbot_integration_tests/utils/proxy.py +++ b/certbot-ci/certbot_integration_tests/utils/proxy.py @@ -5,17 +5,19 @@ import http.server as BaseHTTPServer import json import re import sys +from typing import Mapping +from typing import Type import requests from certbot_integration_tests.utils.misc import GracefulTCPServer -def _create_proxy(mapping): +def _create_proxy(mapping: Mapping[str, str]) -> Type[BaseHTTPServer.BaseHTTPRequestHandler]: # pylint: disable=missing-function-docstring class ProxyHandler(BaseHTTPServer.BaseHTTPRequestHandler): # pylint: disable=missing-class-docstring - def do_GET(self): + def do_GET(self) -> None: headers = {key.lower(): value for key, value in self.headers.items()} backend = [backend for pattern, backend in mapping.items() if re.match(pattern, headers['host'])][0] diff --git a/certbot-ci/snap_integration_tests/conftest.py b/certbot-ci/snap_integration_tests/conftest.py index ca2712d82..26314ab0e 100644 --- a/certbot-ci/snap_integration_tests/conftest.py +++ b/certbot-ci/snap_integration_tests/conftest.py @@ -1,3 +1,4 @@ +# type: ignore """ General conftest for pytest execution of all integration tests lying in the snap_installer_integration tests package. @@ -18,9 +19,10 @@ def pytest_addoption(parser): parser.addoption('--snap-folder', required=True, help='set the folder path where snaps to test are located') parser.addoption('--snap-arch', default='amd64', - help='set the architecture do test (default: amd64)') + help='set the architecture do test (default: amd64)') parser.addoption('--allow-persistent-changes', action='store_true', - help='needs to be set, and confirm that the test will make persistent changes on this machine') + help='needs to be set, and confirm that the test will make persistent ' + 'changes on this machine') def pytest_configure(config): @@ -30,7 +32,8 @@ def pytest_configure(config): """ if not config.option.allow_persistent_changes: raise RuntimeError('This integration test would install the Certbot snap on your machine. ' - 'Please run it again with the `--allow-persistent-changes` flag set to acknowledge.') + 'Please run it again with the `--allow-persistent-changes` flag set ' + 'to acknowledge.') def pytest_generate_tests(metafunc): @@ -40,6 +43,6 @@ def pytest_generate_tests(metafunc): if "dns_snap_path" in metafunc.fixturenames: snap_arch = metafunc.config.getoption('snap_arch') snap_folder = metafunc.config.getoption('snap_folder') - snap_dns_path_list = glob.glob(os.path.join(snap_folder, + snap_dns_path_list = glob.glob(os.path.join(snap_folder, 'certbot-dns-*_{0}.snap'.format(snap_arch))) metafunc.parametrize("dns_snap_path", snap_dns_path_list) diff --git a/certbot-ci/snap_integration_tests/dns_tests/test_main.py b/certbot-ci/snap_integration_tests/dns_tests/test_main.py index d008efc67..8dc6c4b80 100644 --- a/certbot-ci/snap_integration_tests/dns_tests/test_main.py +++ b/certbot-ci/snap_integration_tests/dns_tests/test_main.py @@ -1,14 +1,17 @@ #!/usr/bin/env python3 +"""Module executing integration tests against certbot snap.""" import glob import os import re import subprocess +from typing import Generator import pytest @pytest.fixture(autouse=True, scope="module") -def install_certbot_snap(request): +def install_certbot_snap(request: pytest.FixtureRequest) -> Generator[None, None, None]: + """Fixture ensuring the certbot snap is installed before each test.""" with pytest.raises(Exception): subprocess.check_call(['certbot', '--version']) try: @@ -22,13 +25,14 @@ def install_certbot_snap(request): subprocess.call(['snap', 'remove', 'certbot']) -def test_dns_plugin_install(dns_snap_path): +def test_dns_plugin_install(dns_snap_path: str) -> None: """ Test that each DNS plugin Certbot snap can be installed and is usable with the Certbot snap. """ - plugin_name = re.match(r'^certbot-(dns-\w+)_.*\.snap$', - os.path.basename(dns_snap_path)).group(1) + match = re.match(r'^certbot-(dns-\w+)_.*\.snap$', os.path.basename(dns_snap_path)) + assert match + plugin_name = match.group(1) snap_name = 'certbot-{0}'.format(plugin_name) assert plugin_name not in subprocess.check_output(['certbot', 'plugins', '--prepare'], universal_newlines=True) diff --git a/certbot-ci/snap_integration_tests/py.typed b/certbot-ci/snap_integration_tests/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/certbot-ci/windows_installer_integration_tests/conftest.py b/certbot-ci/windows_installer_integration_tests/conftest.py index 8a9de057f..3332fc923 100644 --- a/certbot-ci/windows_installer_integration_tests/conftest.py +++ b/certbot-ci/windows_installer_integration_tests/conftest.py @@ -1,3 +1,4 @@ +# type: ignore """ General conftest for pytest execution of all integration tests lying in the window_installer_integration tests package. @@ -21,9 +22,9 @@ def pytest_addoption(parser): default=os.path.join(ROOT_PATH, 'windows-installer', 'build', 'nsis', 'certbot-beta-installer-win32.exe'), help='set the path of the windows installer to use, default to ' - 'CERTBOT_ROOT_PATH\\windows-installer\\build\\nsis\\certbot-beta-installer-win32.exe') + 'CERTBOT_ROOT_PATH\\windows-installer\\build\\nsis\\certbot-beta-installer-win32.exe') # pylint: disable=line-too-long parser.addoption('--allow-persistent-changes', action='store_true', - help='needs to be set, and confirm that the test will make persistent changes on this machine') + help='needs to be set, and confirm that the test will make persistent changes on this machine') # pylint: disable=line-too-long def pytest_configure(config): @@ -33,4 +34,5 @@ def pytest_configure(config): """ if not config.option.allow_persistent_changes: raise RuntimeError('This integration test would install Certbot on your machine. ' - 'Please run it again with the `--allow-persistent-changes` flag set to acknowledge.') + 'Please run it again with the `--allow-persistent-changes` ' + 'flag set to acknowledge.') diff --git a/certbot-ci/windows_installer_integration_tests/py.typed b/certbot-ci/windows_installer_integration_tests/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/certbot-ci/windows_installer_integration_tests/test_main.py b/certbot-ci/windows_installer_integration_tests/test_main.py index f699b736a..ad1622bde 100644 --- a/certbot-ci/windows_installer_integration_tests/test_main.py +++ b/certbot-ci/windows_installer_integration_tests/test_main.py @@ -1,12 +1,16 @@ +"""Module executing integration tests for the windows installer.""" import os import re import subprocess import time import unittest +from typing import Any + +import pytest @unittest.skipIf(os.name != 'nt', reason='Windows installer tests must be run on Windows.') -def test_it(request): +def test_it(request: pytest.FixtureRequest) -> None: try: subprocess.check_call(['certbot', '--version']) except (subprocess.CalledProcessError, OSError): @@ -20,10 +24,12 @@ def test_it(request): # Assert certbot is installed and runnable output = subprocess.check_output(['certbot', '--version'], universal_newlines=True) - assert re.match(r'^certbot \d+\.\d+\.\d+.*$', output), 'Flag --version does not output a version.' + assert re.match(r'^certbot \d+\.\d+\.\d+.*$', + output), 'Flag --version does not output a version.' # Assert renew task is installed and ready - output = _ps('(Get-ScheduledTask -TaskName "Certbot Renew Task").State', capture_stdout=True) + output = _ps('(Get-ScheduledTask -TaskName "Certbot Renew Task").State', + capture_stdout=True) assert output.strip() == 'Ready' # Assert renew task is working @@ -32,7 +38,8 @@ def test_it(request): status = 'Running' while status != 'Ready': - status = _ps('(Get-ScheduledTask -TaskName "Certbot Renew Task").State', capture_stdout=True).strip() + status = _ps('(Get-ScheduledTask -TaskName "Certbot Renew Task").State', + capture_stdout=True).strip() time.sleep(1) log_path = os.path.join('C:\\', 'Certbot', 'log', 'letsencrypt.log') @@ -45,9 +52,10 @@ def test_it(request): assert 'no renewal failures' in data, 'Renew task did not execute properly.' finally: - # Sadly this command cannot work in non interactive mode: uninstaller will ask explicitly permission in an UAC prompt + # Sadly this command cannot work in non interactive mode: uninstaller will + # ask explicitly permission in an UAC prompt # print('Uninstalling Certbot ...') - # uninstall_path = _ps('(gci "HKLM:\\SOFTWARE\\Wow6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall"' + # uninstall_path = _ps('(gci "HKLM:\\SOFTWARE\\Wow6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall"' # pylint: disable=line-too-long # ' | foreach { gp $_.PSPath }' # ' | ? { $_ -match "Certbot" }' # ' | select UninstallString)' @@ -56,6 +64,7 @@ def test_it(request): pass -def _ps(powershell_str, capture_stdout=False): +def _ps(powershell_str: str, capture_stdout: bool = False) -> Any: fn = subprocess.check_output if capture_stdout else subprocess.check_call - return fn(['powershell.exe', '-c', powershell_str], universal_newlines=True) + return fn(['powershell.exe', '-c', powershell_str], # type: ignore[operator] + universal_newlines=True) diff --git a/certbot-compatibility-test/setup.py b/certbot-compatibility-test/setup.py index e66ed738b..32d297328 100644 --- a/certbot-compatibility-test/setup.py +++ b/certbot-compatibility-test/setup.py @@ -1,7 +1,7 @@ from setuptools import find_packages from setuptools import setup -version = '1.22.0.dev0' +version = '1.23.0.dev0' install_requires = [ 'certbot', diff --git a/certbot-dns-cloudflare/setup.py b/certbot-dns-cloudflare/setup.py index 96f0ddad0..cb0a9f777 100644 --- a/certbot-dns-cloudflare/setup.py +++ b/certbot-dns-cloudflare/setup.py @@ -4,7 +4,7 @@ import sys from setuptools import find_packages from setuptools import setup -version = '1.22.0.dev0' +version = '1.23.0.dev0' install_requires = [ 'cloudflare>=1.5.1', diff --git a/certbot-dns-cloudxns/setup.py b/certbot-dns-cloudxns/setup.py index 300246b0f..1739cccd3 100644 --- a/certbot-dns-cloudxns/setup.py +++ b/certbot-dns-cloudxns/setup.py @@ -4,7 +4,7 @@ import sys from setuptools import find_packages from setuptools import setup -version = '1.22.0.dev0' +version = '1.23.0.dev0' install_requires = [ 'dns-lexicon>=3.2.1', diff --git a/certbot-dns-digitalocean/setup.py b/certbot-dns-digitalocean/setup.py index e8472a9b9..ac7f73fad 100644 --- a/certbot-dns-digitalocean/setup.py +++ b/certbot-dns-digitalocean/setup.py @@ -4,7 +4,7 @@ import sys from setuptools import find_packages from setuptools import setup -version = '1.22.0.dev0' +version = '1.23.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 0a287ac2e..1eeed3294 100644 --- a/certbot-dns-dnsimple/setup.py +++ b/certbot-dns-dnsimple/setup.py @@ -4,7 +4,7 @@ import sys from setuptools import find_packages from setuptools import setup -version = '1.22.0.dev0' +version = '1.23.0.dev0' install_requires = [ # This version of lexicon is required to address the problem described in diff --git a/certbot-dns-dnsmadeeasy/setup.py b/certbot-dns-dnsmadeeasy/setup.py index 37b7f7942..39582f4cf 100644 --- a/certbot-dns-dnsmadeeasy/setup.py +++ b/certbot-dns-dnsmadeeasy/setup.py @@ -4,7 +4,7 @@ import sys from setuptools import find_packages from setuptools import setup -version = '1.22.0.dev0' +version = '1.23.0.dev0' install_requires = [ 'dns-lexicon>=3.2.1', diff --git a/certbot-dns-gehirn/setup.py b/certbot-dns-gehirn/setup.py index c1f322329..cbbdc9390 100644 --- a/certbot-dns-gehirn/setup.py +++ b/certbot-dns-gehirn/setup.py @@ -4,7 +4,7 @@ import sys from setuptools import find_packages from setuptools import setup -version = '1.22.0.dev0' +version = '1.23.0.dev0' install_requires = [ 'dns-lexicon>=3.2.1', diff --git a/certbot-dns-google/setup.py b/certbot-dns-google/setup.py index 1838c2737..2cd4db824 100644 --- a/certbot-dns-google/setup.py +++ b/certbot-dns-google/setup.py @@ -4,7 +4,7 @@ import sys from setuptools import find_packages from setuptools import setup -version = '1.22.0.dev0' +version = '1.23.0.dev0' install_requires = [ 'google-api-python-client>=1.5.5', 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 003871a75..751ab6086 100644 --- a/certbot-dns-linode/certbot_dns_linode/_internal/dns_linode.py +++ b/certbot-dns-linode/certbot_dns_linode/_internal/dns_linode.py @@ -2,6 +2,7 @@ import logging import re from typing import Optional +from typing import Union from lexicon.providers import linode from lexicon.providers import linode4 @@ -58,7 +59,7 @@ class Authenticator(dns_common.DNSAuthenticator): if not self.credentials: # pragma: no cover raise errors.Error("Plugin has not been prepared.") api_key = self.credentials.conf('key') - api_version = self.credentials.conf('version') + api_version: Optional[Union[str, int]] = self.credentials.conf('version') if api_version == '': api_version = None diff --git a/certbot-dns-linode/setup.py b/certbot-dns-linode/setup.py index a41d7fe20..2a17edf2a 100644 --- a/certbot-dns-linode/setup.py +++ b/certbot-dns-linode/setup.py @@ -4,7 +4,7 @@ import sys from setuptools import find_packages from setuptools import setup -version = '1.22.0.dev0' +version = '1.23.0.dev0' install_requires = [ 'dns-lexicon>=3.2.1', diff --git a/certbot-dns-luadns/setup.py b/certbot-dns-luadns/setup.py index a4d246fd7..d3ab15100 100644 --- a/certbot-dns-luadns/setup.py +++ b/certbot-dns-luadns/setup.py @@ -4,7 +4,7 @@ import sys from setuptools import find_packages from setuptools import setup -version = '1.22.0.dev0' +version = '1.23.0.dev0' install_requires = [ 'dns-lexicon>=3.2.1', diff --git a/certbot-dns-nsone/setup.py b/certbot-dns-nsone/setup.py index 410f26ee0..e3d0a599f 100644 --- a/certbot-dns-nsone/setup.py +++ b/certbot-dns-nsone/setup.py @@ -4,7 +4,7 @@ import sys from setuptools import find_packages from setuptools import setup -version = '1.22.0.dev0' +version = '1.23.0.dev0' install_requires = [ 'dns-lexicon>=3.2.1', diff --git a/certbot-dns-ovh/setup.py b/certbot-dns-ovh/setup.py index 1ceec6646..9febe8610 100644 --- a/certbot-dns-ovh/setup.py +++ b/certbot-dns-ovh/setup.py @@ -4,7 +4,7 @@ import sys from setuptools import find_packages from setuptools import setup -version = '1.22.0.dev0' +version = '1.23.0.dev0' install_requires = [ 'dns-lexicon>=3.2.1', diff --git a/certbot-dns-rfc2136/setup.py b/certbot-dns-rfc2136/setup.py index d39199829..cd3189023 100644 --- a/certbot-dns-rfc2136/setup.py +++ b/certbot-dns-rfc2136/setup.py @@ -4,7 +4,7 @@ import sys from setuptools import find_packages from setuptools import setup -version = '1.22.0.dev0' +version = '1.23.0.dev0' install_requires = [ 'dnspython>=1.15.0', diff --git a/certbot-dns-route53/setup.py b/certbot-dns-route53/setup.py index d8ec6439d..6c12d0104 100644 --- a/certbot-dns-route53/setup.py +++ b/certbot-dns-route53/setup.py @@ -4,7 +4,7 @@ import sys from setuptools import find_packages from setuptools import setup -version = '1.22.0.dev0' +version = '1.23.0.dev0' install_requires = [ 'boto3', diff --git a/certbot-dns-sakuracloud/setup.py b/certbot-dns-sakuracloud/setup.py index e7bae6f51..3a7879b29 100644 --- a/certbot-dns-sakuracloud/setup.py +++ b/certbot-dns-sakuracloud/setup.py @@ -4,7 +4,7 @@ import sys from setuptools import find_packages from setuptools import setup -version = '1.22.0.dev0' +version = '1.23.0.dev0' install_requires = [ 'dns-lexicon>=3.2.1', diff --git a/certbot-nginx/certbot_nginx/_internal/configurator.py b/certbot-nginx/certbot_nginx/_internal/configurator.py index 596890cd1..ede184159 100644 --- a/certbot-nginx/certbot_nginx/_internal/configurator.py +++ b/certbot-nginx/certbot_nginx/_internal/configurator.py @@ -20,7 +20,6 @@ from acme import challenges from acme import crypto_util as acme_crypto_util from certbot import crypto_util from certbot import errors -from certbot import interfaces from certbot import util from certbot.display import util as display_util from certbot.compat import os @@ -42,7 +41,7 @@ NO_SSL_MODIFIER = 4 logger = logging.getLogger(__name__) -class NginxConfigurator(common.Installer, interfaces.Authenticator): +class NginxConfigurator(common.Configurator): """Nginx configurator. .. todo:: Add proper support for comments in the config. Currently, diff --git a/certbot-nginx/certbot_nginx/_internal/http_01.py b/certbot-nginx/certbot_nginx/_internal/http_01.py index eeca6c855..95592fea0 100644 --- a/certbot-nginx/certbot_nginx/_internal/http_01.py +++ b/certbot-nginx/certbot_nginx/_internal/http_01.py @@ -4,6 +4,7 @@ import io import logging from typing import List from typing import Optional +from typing import TYPE_CHECKING from acme import challenges from certbot import achallenges @@ -13,6 +14,9 @@ from certbot.plugins import common from certbot_nginx._internal import nginxparser from certbot_nginx._internal import obj +if TYPE_CHECKING: + from certbot_nginx._internal.configurator import NginxConfigurator + logger = logging.getLogger(__name__) @@ -36,8 +40,9 @@ class NginxHttp01(common.ChallengePerformer): """ - def __init__(self, configurator): + def __init__(self, configurator: "NginxConfigurator") -> None: super().__init__(configurator) + self.configurator: "NginxConfigurator" self.challenge_conf = os.path.join( configurator.config.config_dir, "le_http_01_cert_challenge.conf") diff --git a/certbot-nginx/setup.py b/certbot-nginx/setup.py index 59e2ea5f2..8169e21a5 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.22.0.dev0' +version = '1.23.0.dev0' install_requires = [ # We specify the minimum acme and certbot version as the current plugin diff --git a/certbot/CHANGELOG.md b/certbot/CHANGELOG.md index 0a5849d82..95c6e6f29 100644 --- a/certbot/CHANGELOG.md +++ b/certbot/CHANGELOG.md @@ -2,7 +2,23 @@ Certbot adheres to [Semantic Versioning](https://semver.org/). -## 1.22.0 - master +## 1.23.0 - master + +### Added + +* + +### Changed + +* + +### Fixed + +* + +More details about these changes can be found on our GitHub repo. + +## 1.22.0 - 2021-12-07 ### Added @@ -10,6 +26,8 @@ Certbot adheres to [Semantic Versioning](https://semver.org/). * The function certbot.util.parse_loose_version was added to parse version strings in the same way as the now deprecated distutils.version.LooseVersion class from the Python standard library. +* Added `--issuance-timeout`. This option specifies how long (in seconds) Certbot will wait + for the server to issue a certificate. ### Changed diff --git a/certbot/certbot/__init__.py b/certbot/certbot/__init__.py index 0b1b00db9..3f9e235bd 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.22.0.dev0' +__version__ = '1.23.0.dev0' diff --git a/certbot/certbot/_internal/account.py b/certbot/certbot/_internal/account.py index bea5314a8..982a97412 100644 --- a/certbot/certbot/_internal/account.py +++ b/certbot/certbot/_internal/account.py @@ -5,9 +5,13 @@ import hashlib import logging import shutil import socket -from typing import cast from typing import Any +from typing import Callable +from typing import cast +from typing import Dict +from typing import List from typing import Mapping +from typing import Optional from cryptography.hazmat.primitives import serialization import josepy as jose @@ -17,6 +21,7 @@ import pytz from acme import fields as acme_fields from acme import messages from acme.client import ClientBase # pylint: disable=unused-import +from certbot import configuration from certbot import errors from certbot import interfaces from certbot import util @@ -53,7 +58,8 @@ class Account: creation_host: str = jose.field("creation_host") register_to_eff: str = jose.field("register_to_eff", omitempty=True) - def __init__(self, regr, key, meta=None): + def __init__(self, regr: messages.RegistrationResource, key: jose.JWK, + meta: Optional['Meta'] = None) -> None: self.key = key self.regr = regr self.meta = self.Meta( @@ -84,16 +90,16 @@ class Account: # account key (and thus its fingerprint) to be updated... @property - def slug(self): + def slug(self) -> str: """Short account identification string, useful for UI.""" return "{1}@{0} ({2})".format(pyrfc3339.generate( self.meta.creation_dt), self.meta.creation_host, self.id[:4]) - def __repr__(self): + def __repr__(self) -> str: return "<{0}({1}, {2}, {3})>".format( self.__class__.__name__, self.regr, self.id, self.meta) - def __eq__(self, other): + def __eq__(self, other: Any) -> bool: return (isinstance(other, self.__class__) and self.key == other.key and self.regr == other.regr and self.meta == other.meta) @@ -102,18 +108,18 @@ class Account: class AccountMemoryStorage(interfaces.AccountStorage): """In-memory account storage.""" - def __init__(self, initial_accounts=None): + def __init__(self, initial_accounts: Dict[str, Account] = None) -> None: self.accounts = initial_accounts if initial_accounts is not None else {} - def find_all(self): + def find_all(self) -> List[Account]: return list(self.accounts.values()) - def save(self, account, client): + def save(self, account: Account, client: ClientBase) -> None: if account.id in self.accounts: logger.debug("Overwriting account: %s", account.id) self.accounts[account.id] = account - def load(self, account_id): + def load(self, account_id: str) -> Account: try: return self.accounts[account_id] except KeyError: @@ -136,30 +142,30 @@ class AccountFileStorage(interfaces.AccountStorage): :ivar certbot.configuration.NamespaceConfig config: Client configuration """ - def __init__(self, config): + def __init__(self, config: configuration.NamespaceConfig) -> None: self.config = config util.make_or_verify_dir(config.accounts_dir, 0o700, self.config.strict_permissions) - def _account_dir_path(self, account_id): + def _account_dir_path(self, account_id: str) -> str: return self._account_dir_path_for_server_path(account_id, self.config.server_path) - def _account_dir_path_for_server_path(self, account_id, server_path): + def _account_dir_path_for_server_path(self, account_id: str, server_path: str) -> str: accounts_dir = self.config.accounts_dir_for_server_path(server_path) return os.path.join(accounts_dir, account_id) @classmethod - def _regr_path(cls, account_dir_path): + def _regr_path(cls, account_dir_path: str) -> str: return os.path.join(account_dir_path, "regr.json") @classmethod - def _key_path(cls, account_dir_path): + def _key_path(cls, account_dir_path: str) -> str: return os.path.join(account_dir_path, "private_key.json") @classmethod - def _metadata_path(cls, account_dir_path): + def _metadata_path(cls, account_dir_path: str) -> str: return os.path.join(account_dir_path, "meta.json") - def _find_all_for_server_path(self, server_path): + def _find_all_for_server_path(self, server_path: str) -> List[Account]: accounts_dir = self.config.accounts_dir_for_server_path(server_path) try: candidates = os.listdir(accounts_dir) @@ -186,15 +192,16 @@ class AccountFileStorage(interfaces.AccountStorage): accounts = prev_accounts return accounts - def find_all(self): + def find_all(self) -> List[Account]: return self._find_all_for_server_path(self.config.server_path) - def _symlink_to_account_dir(self, prev_server_path, server_path, account_id): + def _symlink_to_account_dir(self, prev_server_path: str, server_path: str, + account_id: str) -> None: prev_account_dir = self._account_dir_path_for_server_path(account_id, prev_server_path) new_account_dir = self._account_dir_path_for_server_path(account_id, server_path) os.symlink(prev_account_dir, new_account_dir) - def _symlink_to_accounts_dir(self, prev_server_path, server_path): + def _symlink_to_accounts_dir(self, prev_server_path: str, server_path: str) -> None: accounts_dir = self.config.accounts_dir_for_server_path(server_path) if os.path.islink(accounts_dir): os.unlink(accounts_dir) @@ -203,7 +210,7 @@ class AccountFileStorage(interfaces.AccountStorage): prev_account_dir = self.config.accounts_dir_for_server_path(prev_server_path) os.symlink(prev_account_dir, accounts_dir) - def _load_for_server_path(self, account_id, server_path): + def _load_for_server_path(self, account_id: str, server_path: str) -> Account: account_dir_path = self._account_dir_path_for_server_path(account_id, server_path) if not os.path.isdir(account_dir_path): # isdir is also true for symlinks if server_path in constants.LE_REUSE_SERVERS: @@ -222,17 +229,21 @@ class AccountFileStorage(interfaces.AccountStorage): try: with open(self._regr_path(account_dir_path)) as regr_file: - regr = messages.RegistrationResource.json_loads(regr_file.read()) + # TODO: Remove cast when https://github.com/certbot/certbot/pull/9073 is merged. + regr = cast(messages.RegistrationResource, + messages.RegistrationResource.json_loads(regr_file.read())) with open(self._key_path(account_dir_path)) as key_file: - key = jose.JWK.json_loads(key_file.read()) + # TODO: Remove cast when https://github.com/certbot/certbot/pull/9073 is merged. + key = cast(jose.JWK, jose.JWK.json_loads(key_file.read())) with open(self._metadata_path(account_dir_path)) as metadata_file: - meta = Account.Meta.json_loads(metadata_file.read()) + # TODO: Remove cast when https://github.com/certbot/certbot/pull/9073 is merged. + meta = cast(Account.Meta, Account.Meta.json_loads(metadata_file.read())) except IOError as error: raise errors.AccountStorageError(error) return Account(regr, key, meta) - def load(self, account_id): + def load(self, account_id: str) -> Account: return self._load_for_server_path(account_id, self.config.server_path) def save(self, account: Account, client: ClientBase) -> None: @@ -275,7 +286,7 @@ class AccountFileStorage(interfaces.AccountStorage): except IOError as error: raise errors.AccountStorageError(error) - def delete(self, account_id): + def delete(self, account_id: str) -> None: """Delete registration info from disk :param account_id: id of account which should be deleted @@ -292,17 +303,18 @@ class AccountFileStorage(interfaces.AccountStorage): if not os.listdir(self.config.accounts_dir): self._delete_accounts_dir_for_server_path(self.config.server_path) - def _delete_account_dir_for_server_path(self, account_id, server_path): + def _delete_account_dir_for_server_path(self, account_id: str, server_path: str) -> None: link_func = functools.partial(self._account_dir_path_for_server_path, account_id) nonsymlinked_dir = self._delete_links_and_find_target_dir(server_path, link_func) shutil.rmtree(nonsymlinked_dir) - def _delete_accounts_dir_for_server_path(self, server_path): + def _delete_accounts_dir_for_server_path(self, server_path: str) -> None: link_func = self.config.accounts_dir_for_server_path nonsymlinked_dir = self._delete_links_and_find_target_dir(server_path, link_func) os.rmdir(nonsymlinked_dir) - def _delete_links_and_find_target_dir(self, server_path, link_func): + def _delete_links_and_find_target_dir(self, server_path: str, + link_func: Callable[[str], str]) -> str: """Delete symlinks and return the nonsymlinked directory path. :param str server_path: file path based on server @@ -368,6 +380,6 @@ class AccountFileStorage(interfaces.AccountStorage): uri=regr.uri) regr_file.write(regr.json_dumps()) - def _update_meta(self, account, dir_path): + def _update_meta(self, account: Account, dir_path: str) -> None: with open(self._metadata_path(dir_path), "w") as metadata_file: metadata_file.write(account.meta.json_dumps()) diff --git a/certbot/certbot/_internal/auth_handler.py b/certbot/certbot/_internal/auth_handler.py index f9f6b96c9..0fa1daf8e 100644 --- a/certbot/certbot/_internal/auth_handler.py +++ b/certbot/certbot/_internal/auth_handler.py @@ -3,15 +3,25 @@ import datetime import logging import time from typing import Dict +from typing import Iterable from typing import List +from typing import Optional from typing import Tuple +from typing import Type + +import josepy +from requests.models import Response from acme import challenges +from acme import client from acme import errors as acme_errors from acme import messages from certbot import achallenges +from certbot import configuration from certbot import errors +from certbot import interfaces from certbot._internal import error_handler +from certbot._internal.account import Account from certbot.display import util as display_util from certbot.plugins import common as plugin_common @@ -34,14 +44,17 @@ class AuthHandler: type strings with the most preferred challenge listed first """ - def __init__(self, auth, acme_client, account, pref_challs): + def __init__(self, auth: interfaces.Authenticator, acme_client: Optional[client.ClientV2], + account: Optional[Account], pref_challs: List[str]) -> None: self.auth = auth self.acme = acme_client self.account = account self.pref_challs = pref_challs - def handle_authorizations(self, orderr, config, best_effort=False, max_retries=30): + def handle_authorizations(self, orderr: messages.OrderResource, + config: configuration.NamespaceConfig, best_effort: bool = False, + max_retries: int = 30) -> List[messages.AuthorizationResource]: """ Retrieve all authorizations, perform all challenges required to validate these authorizations, then poll and wait for the authorization to be checked. @@ -57,6 +70,8 @@ class AuthHandler: authzrs = orderr.authorizations[:] if not authzrs: raise errors.AuthorizationError('No authorization to handle.') + if not self.acme: + raise errors.Error("No ACME client defined, authorizations cannot be handled.") # Retrieve challenges that need to be performed to validate authorizations. achalls = self._choose_challenges(authzrs) @@ -97,6 +112,8 @@ class AuthHandler: return authzrs_validated + raise errors.Error("An unexpected error occurred while handling the authorizations.") + def deactivate_valid_authorizations(self, orderr: messages.OrderResource) -> Tuple[List, List]: """ Deactivate all `valid` authorizations in the order, so that they cannot be re-used @@ -106,6 +123,9 @@ class AuthHandler: list of unsuccessfully deactivated authorizations. :rtype: tuple """ + if not self.acme: + raise errors.Error("No ACME client defined, cannot deactivate valid authorizations.") + to_deactivate = [authzr for authzr in orderr.authorizations if authzr.body.status == messages.STATUS_VALID] deactivated = [] @@ -121,17 +141,22 @@ class AuthHandler: return (deactivated, failed) - def _poll_authorizations(self, authzrs, max_retries, best_effort): + def _poll_authorizations(self, authzrs: List[messages.AuthorizationResource], max_retries: int, + best_effort: bool) -> None: """ Poll the ACME CA server, to wait for confirmation that authorizations have their challenges all verified. The poll may occur several times, until all authorizations are checked (valid or invalid), or after a maximum of retries. """ - authzrs_to_check = {index: (authzr, None) + if not self.acme: + raise errors.Error("No ACME client defined, cannot poll authorizations.") + + authzrs_to_check: Dict[int, Tuple[messages.AuthorizationResource, + Optional[Response]]] = {index: (authzr, None) for index, authzr in enumerate(authzrs)} authzrs_failed_to_report = [] # Give an initial second to the ACME CA server to check the authorizations - sleep_seconds = 1 + sleep_seconds: float = 1 for _ in range(max_retries): # Wait for appropriate time (from Retry-After, initial wait, or no wait) if sleep_seconds > 0: @@ -166,8 +191,10 @@ class AuthHandler: # and wait this time before polling again in next loop iteration. # From all the pending authorizations, we take the greatest Retry-After value # to avoid polling an authorization before its relevant Retry-After value. + # (by construction resp cannot be None at that time, but mypy do not know it). retry_after = max(self.acme.retry_after(resp, 3) - for _, resp in authzrs_to_check.values()) + for _, resp in authzrs_to_check.values() + if resp is not None) sleep_seconds = (retry_after - datetime.datetime.now()).total_seconds() # In case of failed authzrs, create a report to the user. @@ -181,12 +208,16 @@ class AuthHandler: # Here authzrs_to_check is still not empty, meaning we exceeded the max polling attempt. raise errors.AuthorizationError('All authorizations were not finalized by the CA.') - def _choose_challenges(self, authzrs): + def _choose_challenges(self, authzrs: Iterable[messages.AuthorizationResource] + ) -> List[achallenges.AnnotatedChallenge]: """ Retrieve necessary and pending challenges to satisfy server. NB: Necessary and already validated challenges are not retrieved, as they can be reused for a certificate issuance. """ + if not self.acme: + raise errors.Error("No ACME client defined, cannot choose the challenges.") + pending_authzrs = [authzr for authzr in authzrs if authzr.body.status != messages.STATUS_VALID] achalls: List[achallenges.AnnotatedChallenge] = [] @@ -208,7 +239,7 @@ class AuthHandler: return achalls - def _get_chall_pref(self, domain): + def _get_chall_pref(self, domain: str) -> List[Type[challenges.Challenge]]: """Return list of challenge preferences. :param str domain: domain for which you are requesting preferences @@ -230,7 +261,7 @@ class AuthHandler: chall_prefs.extend(plugin_pref) return chall_prefs - def _cleanup_challenges(self, achalls): + def _cleanup_challenges(self, achalls: List[achallenges.AnnotatedChallenge]) -> None: """Cleanup challenges. :param achalls: annotated challenges to cleanup @@ -240,7 +271,8 @@ class AuthHandler: logger.info("Cleaning up challenges") self.auth.cleanup(achalls) - def _challenge_factory(self, authzr, path): + def _challenge_factory(self, authzr: messages.AuthorizationResource, + path: List[int]) -> List[achallenges.AnnotatedChallenge]: """Construct Namedtuple Challenges :param messages.AuthorizationResource authzr: authorization @@ -248,12 +280,14 @@ class AuthHandler: :param list path: List of indices from `challenges`. :returns: achalls, list of challenge type - :class:`certbot.achallenges.Indexed` + :class:`certbot.achallenges.AnnotatedChallenge` :rtype: list :raises .errors.Error: if challenge type is not recognized """ + if not self.account: + raise errors.Error("Account is not set.") achalls = [] for index in path: @@ -265,6 +299,8 @@ class AuthHandler: def _report_failed_authzrs(self, failed_authzrs: List[messages.AuthorizationResource]) -> None: """Notifies the user about failed authorizations.""" + if not self.account: + raise errors.Error("Account is not set.") problems: Dict[str, List[achallenges.AnnotatedChallenge]] = {} failed_achalls = [challb_to_achall(challb, self.account.key, authzr.body.identifier.value) for authzr in failed_authzrs for challb in authzr.body.challenges @@ -273,8 +309,9 @@ class AuthHandler: for achall in failed_achalls: problems.setdefault(achall.error.typ, []).append(achall) - msg = [f"\nCertbot failed to authenticate some domains (authenticator: {self.auth.name})." - " The Certificate Authority reported these problems:"] + msg = ["\nCertbot failed to authenticate some domains " + f"(authenticator: {self.auth.name})." + " The Certificate Authority reported these problems:"] for _, achalls in sorted(problems.items(), key=lambda item: item[0]): msg.append(_generate_failed_chall_msg(achalls)) @@ -287,7 +324,8 @@ class AuthHandler: display_util.notify("".join(msg)) -def challb_to_achall(challb, account_key, domain): +def challb_to_achall(challb: messages.ChallengeBody, account_key: josepy.JWK, + domain: str) -> achallenges.AnnotatedChallenge: """Converts a ChallengeBody object to an AnnotatedChallenge. :param .ChallengeBody challb: ChallengeBody @@ -310,7 +348,9 @@ def challb_to_achall(challb, account_key, domain): "Received unsupported challenge of type: {0}".format(chall.typ)) -def gen_challenge_path(challbs, preferences, combinations): +def gen_challenge_path(challbs: List[messages.ChallengeBody], + preferences: List[Type[challenges.Challenge]], + combinations: Tuple[List[int], ...]) -> List[int]: """Generate a plan to get authority over the identity. .. todo:: This can be possibly be rewritten to use resolved_combinations. @@ -328,8 +368,8 @@ def gen_challenge_path(challbs, preferences, combinations): :class:`acme.messages.Challenge`, each of which would be sufficient to prove possession of the identifier. - :returns: tuple of indices from ``challenges``. - :rtype: tuple + :returns: list of indices from ``challenges``. + :rtype: list :raises certbot.errors.AuthorizationError: If a path cannot be created that satisfies the CA given the preferences and @@ -341,7 +381,10 @@ def gen_challenge_path(challbs, preferences, combinations): return _find_dumb_path(challbs, preferences) -def _find_smart_path(challbs, preferences, combinations): +def _find_smart_path(challbs: List[messages.ChallengeBody], + preferences: List[Type[challenges.Challenge]], + combinations: Tuple[List[int], ...] + ) -> List[int]: """Find challenge path with server hints. Can be called if combinations is included. Function uses a simple @@ -356,7 +399,7 @@ def _find_smart_path(challbs, preferences, combinations): # max_cost is now equal to sum(indices) + 1 - best_combo = None + best_combo: Optional[List[int]] = None # Set above completing all of the available challenges best_combo_cost = max_cost @@ -373,12 +416,13 @@ def _find_smart_path(challbs, preferences, combinations): combo_total = 0 if not best_combo: - _report_no_chall_path(challbs) + raise _report_no_chall_path(challbs) return best_combo -def _find_dumb_path(challbs, preferences): +def _find_dumb_path(challbs: List[messages.ChallengeBody], + preferences: List[Type[challenges.Challenge]]) -> List[int]: """Find challenge path without server hints. Should be called if the combinations hint is not included by the @@ -394,16 +438,19 @@ def _find_dumb_path(challbs, preferences): if supported: path.append(i) else: - _report_no_chall_path(challbs) + raise _report_no_chall_path(challbs) return path -def _report_no_chall_path(challbs): - """Logs and raises an error that no satisfiable chall path exists. +def _report_no_chall_path(challbs: List[messages.ChallengeBody]) -> errors.AuthorizationError: + """Logs and return a raisable error reporting that no satisfiable chall path exists. :param challbs: challenges from the authorization that can't be satisfied + :returns: An authorization error + :rtype: certbot.errors.AuthorizationError + """ msg = ("Client with the currently selected authenticator does not support " "any combination of challenges that will satisfy the CA.") @@ -412,7 +459,7 @@ def _report_no_chall_path(challbs): " You may need to use an authenticator " "plugin that can do challenges over DNS.") logger.critical(msg) - raise errors.AuthorizationError(msg) + return errors.AuthorizationError(msg) def _generate_failed_chall_msg(failed_achalls: List[achallenges.AnnotatedChallenge]) -> str: diff --git a/certbot/certbot/_internal/cert_manager.py b/certbot/certbot/_internal/cert_manager.py index ab66e57c7..7eeddfa1a 100644 --- a/certbot/certbot/_internal/cert_manager.py +++ b/certbot/certbot/_internal/cert_manager.py @@ -3,10 +3,18 @@ import datetime import logging import re import traceback +from typing import Any +from typing import Callable +from typing import Iterable from typing import List +from typing import Optional +from typing import Tuple +from typing import TypeVar +from typing import Union import pytz +from certbot import configuration from certbot import crypto_util from certbot import errors from certbot import ocsp @@ -22,7 +30,7 @@ logger = logging.getLogger(__name__) ################### -def update_live_symlinks(config): +def update_live_symlinks(config: configuration.NamespaceConfig) -> None: """Update the certificate file family symlinks to use archive_dir. Use the information in the config file to make symlinks point to @@ -38,7 +46,7 @@ def update_live_symlinks(config): storage.RenewableCert(renewal_file, config, update_symlinks=True) -def rename_lineage(config): +def rename_lineage(config: configuration.NamespaceConfig) -> None: """Rename the specified lineage to the new name. :param config: Configuration. @@ -64,7 +72,7 @@ def rename_lineage(config): .format(certname, new_certname), pause=False) -def certificates(config): +def certificates(config: configuration.NamespaceConfig) -> None: """Display information about certs configured with Certbot :param config: Configuration. @@ -87,7 +95,7 @@ def certificates(config): _describe_certs(config, parsed_certs, parse_failures) -def delete(config): +def delete(config: configuration.NamespaceConfig) -> None: """Delete Certbot files associated with a certificate lineage.""" certnames = get_certnames(config, "delete", allow_multiple=True) msg = ["The following certificate(s) are selected for deletion:\n"] @@ -112,7 +120,9 @@ def delete(config): # Public Helpers ################### -def lineage_for_certname(cli_config, certname): + +def lineage_for_certname(cli_config: configuration.NamespaceConfig, + certname: str) -> Optional[storage.RenewableCert]: """Find a lineage object with name certname.""" configs_dir = cli_config.renewal_configs_dir # Verify the directory is there @@ -129,13 +139,16 @@ def lineage_for_certname(cli_config, certname): return None -def domains_for_certname(config, certname): +def domains_for_certname(config: configuration.NamespaceConfig, + certname: str) -> Optional[List[str]]: """Find the domains in the cert with name certname.""" lineage = lineage_for_certname(config, certname) return lineage.names() if lineage else None -def find_duplicative_certs(config, domains): +def find_duplicative_certs(config: configuration.NamespaceConfig, + domains: List[str]) -> Tuple[Optional[storage.RenewableCert], + Optional[storage.RenewableCert]]: """Find existing certs that match the given domain names. This function searches for certificates whose domains are equal to @@ -158,7 +171,11 @@ def find_duplicative_certs(config, domains): :rtype: `tuple` of `storage.RenewableCert` or `None` """ - def update_certs_for_domain_matches(candidate_lineage, rv): + def update_certs_for_domain_matches(candidate_lineage: storage.RenewableCert, + rv: Tuple[Optional[storage.RenewableCert], + Optional[storage.RenewableCert]] + ) -> Tuple[Optional[storage.RenewableCert], + Optional[storage.RenewableCert]]: """Return cert as identical_names_cert if it matches, or subset_names_cert if it matches as subset """ @@ -177,10 +194,12 @@ def find_duplicative_certs(config, domains): subset_names_cert = candidate_lineage return (identical_names_cert, subset_names_cert) - return _search_lineages(config, update_certs_for_domain_matches, (None, None)) + init: Tuple[Optional[storage.RenewableCert], Optional[storage.RenewableCert]] = (None, None) + + return _search_lineages(config, update_certs_for_domain_matches, init) -def _archive_files(candidate_lineage, filetype): +def _archive_files(candidate_lineage: storage.RenewableCert, filetype: str) -> Optional[List[str]]: """ In order to match things like: /etc/letsencrypt/archive/example.com/chain1.pem. @@ -202,7 +221,8 @@ def _archive_files(candidate_lineage, filetype): return None -def _acceptable_matches(): +def _acceptable_matches() -> List[Union[Callable[[storage.RenewableCert], str], + Callable[[storage.RenewableCert], Optional[List[str]]]]]: """ Generates the list that's passed to match_and_check_overlaps. Is its own function to make unit testing easier. @@ -213,7 +233,7 @@ def _acceptable_matches(): lambda x: _archive_files(x, "cert"), lambda x: _archive_files(x, "fullchain")] -def cert_path_to_lineage(cli_config): +def cert_path_to_lineage(cli_config: configuration.NamespaceConfig) -> str: """ If config.cert_path is defined, try to find an appropriate value for config.certname. :param `configuration.NamespaceConfig` cli_config: parsed command line arguments @@ -226,11 +246,16 @@ def cert_path_to_lineage(cli_config): """ acceptable_matches = _acceptable_matches() match = match_and_check_overlaps(cli_config, acceptable_matches, - lambda x: cli_config.cert_path, lambda x: x.lineagename) + lambda x: cli_config.cert_path, lambda x: x.lineagename) return match[0] -def match_and_check_overlaps(cli_config, acceptable_matches, match_func, rv_func): +def match_and_check_overlaps(cli_config: configuration.NamespaceConfig, + acceptable_matches: Iterable[Union[ + Callable[[storage.RenewableCert], str], + Callable[[storage.RenewableCert], Optional[List[str]]]]], + match_func: Callable[[storage.RenewableCert], str], + rv_func: Callable[[storage.RenewableCert], str]) -> List[str]: """ Searches through all lineages for a match, and checks for duplicates. If a duplicate is found, an error is raised, as performing operations on lineages that have their properties incorrectly duplicated elsewhere is probably a bad idea. @@ -241,21 +266,24 @@ def match_and_check_overlaps(cli_config, acceptable_matches, match_func, rv_func :param function rv_func: specifies what to return """ - def find_matches(candidate_lineage, return_value, acceptable_matches): + def find_matches(candidate_lineage: storage.RenewableCert, return_value: List[str], + acceptable_matches: Iterable[Union[ + Callable[[storage.RenewableCert], str], + Callable[[storage.RenewableCert], Optional[List[str]]]]]) -> List[str]: """Returns a list of matches using _search_lineages.""" - acceptable_matches = [func(candidate_lineage) for func in acceptable_matches] + acceptable_matches_resolved = [func(candidate_lineage) for func in acceptable_matches] acceptable_matches_rv: List[str] = [] - for item in acceptable_matches: + for item in acceptable_matches_resolved: if isinstance(item, list): acceptable_matches_rv += item - else: + elif item: acceptable_matches_rv.append(item) match = match_func(candidate_lineage) if match in acceptable_matches_rv: return_value.append(rv_func(candidate_lineage)) return return_value - matched = _search_lineages(cli_config, find_matches, [], acceptable_matches) + matched: List[str] = _search_lineages(cli_config, find_matches, [], acceptable_matches) if not matched: raise errors.Error("No match found for cert-path {0}!".format(cli_config.cert_path)) elif len(matched) > 1: @@ -263,7 +291,8 @@ def match_and_check_overlaps(cli_config, acceptable_matches, match_func, rv_func return matched -def human_readable_cert_info(config, cert, skip_filter_checks=False): +def human_readable_cert_info(config: configuration.NamespaceConfig, cert: storage.RenewableCert, + skip_filter_checks: bool = False) -> Optional[str]: """ Returns a human readable description of info about a RenewableCert object""" certinfo = [] checker = ocsp.RevocationChecker() @@ -312,7 +341,8 @@ def human_readable_cert_info(config, cert, skip_filter_checks=False): return "".join(certinfo) -def get_certnames(config, verb, allow_multiple=False, custom_prompt=None): +def get_certnames(config: configuration.NamespaceConfig, verb: str, allow_multiple: bool = False, + custom_prompt: Optional[str] = None) -> List[str]: """Get certname from flag, interactively, or error out.""" certname = config.certname if certname: @@ -350,12 +380,13 @@ def get_certnames(config, verb, allow_multiple=False, custom_prompt=None): ################### -def _report_lines(msgs): +def _report_lines(msgs: Iterable[str]) -> str: """Format a results report for a category of single-line renewal outcomes""" return " " + "\n ".join(str(msg) for msg in msgs) -def _report_human_readable(config, parsed_certs): +def _report_human_readable(config: configuration.NamespaceConfig, + parsed_certs: Iterable[storage.RenewableCert]) -> str: """Format a results report for a parsed cert""" certinfo = [] for cert in parsed_certs: @@ -365,7 +396,9 @@ def _report_human_readable(config, parsed_certs): return "\n".join(certinfo) -def _describe_certs(config, parsed_certs, parse_failures): +def _describe_certs(config: configuration.NamespaceConfig, + parsed_certs: Iterable[storage.RenewableCert], + parse_failures: Iterable[str]) -> None: """Print information about the certs we know about""" out: List[str] = [] @@ -386,7 +419,10 @@ def _describe_certs(config, parsed_certs, parse_failures): display_util.notification("\n".join(out), pause=False, wrap=False) -def _search_lineages(cli_config, func, initial_rv, *args): +T = TypeVar('T') + +def _search_lineages(cli_config: configuration.NamespaceConfig, func: Callable[..., T], + initial_rv: T, *args: Any) -> T: """Iterate func over unbroken lineages, allowing custom return conditions. Allows flexible customization of return values, including multiple diff --git a/certbot/certbot/_internal/cli/__init__.py b/certbot/certbot/_internal/cli/__init__.py index e7a1de49b..a69f96666 100644 --- a/certbot/certbot/_internal/cli/__init__.py +++ b/certbot/certbot/_internal/cli/__init__.py @@ -4,7 +4,10 @@ import argparse import logging import logging.handlers import sys +from typing import Any +from typing import List from typing import Optional +from typing import Type import certbot from certbot._internal import constants @@ -40,9 +43,9 @@ from certbot._internal.cli.plugins_parsing import _plugins_parsing from certbot._internal.cli.subparsers import _create_subparsers from certbot._internal.cli.verb_help import VERB_HELP from certbot._internal.cli.verb_help import VERB_HELP_MAP -from certbot.plugins import enhancements from certbot._internal.plugins import disco as plugins_disco import certbot._internal.plugins.selection as plugin_selection +from certbot.plugins import enhancements logger = logging.getLogger(__name__) @@ -51,7 +54,8 @@ logger = logging.getLogger(__name__) helpful_parser: Optional[HelpfulArgumentParser] = None -def prepare_and_parse_args(plugins, args, detect_defaults=False): +def prepare_and_parse_args(plugins: plugins_disco.PluginsRegistry, args: List[str], + detect_defaults: bool = False) -> argparse.Namespace: """Returns parsed command line arguments. :param .PluginsRegistry plugins: available plugins @@ -360,6 +364,11 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): 'ACME Challenges are versioned, but if you pick "http" rather ' 'than "http-01", Certbot will select the latest version ' 'automatically.') + helpful.add( + [None, "certonly", "run"], "--issuance-timeout", type=nonnegative_int, + dest="issuance_timeout", + default=flag_default("issuance_timeout"), + help=config_help("issuance_timeout")) helpful.add( "renew", "--pre-hook", help="Command to be run in a shell before obtaining any certificates." @@ -443,7 +452,7 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): return helpful.parse_args() -def set_by_cli(var): +def set_by_cli(var: str) -> bool: """ Return True if a particular config variable has been set by the user (CLI or config file) including if the user explicitly set it to the @@ -487,7 +496,7 @@ def set_by_cli(var): set_by_cli.detector = None # type: ignore -def has_default_value(option, value): +def has_default_value(option: str, value: Any) -> bool: """Does option have the default value? If the default value of option is not known, False is returned. @@ -505,7 +514,7 @@ def has_default_value(option, value): return False -def option_was_set(option, value): +def option_was_set(option: str, value: Any) -> bool: """Was option set by the user or does it differ from the default? :param str option: configuration variable being considered @@ -521,7 +530,7 @@ def option_was_set(option, value): return set_by_cli(option) or not has_default_value(option, value) -def argparse_type(variable): +def argparse_type(variable: Any) -> Type: """Return our argparse type function for a config variable (default: str)""" # pylint: disable=protected-access if helpful_parser is not None: diff --git a/certbot/certbot/_internal/cli/cli_utils.py b/certbot/certbot/_internal/cli/cli_utils.py index 7c859509c..5f3267eb0 100644 --- a/certbot/certbot/_internal/cli/cli_utils.py +++ b/certbot/certbot/_internal/cli/cli_utils.py @@ -2,6 +2,14 @@ import argparse import copy import inspect +from typing import Any +from typing import Iterable +from typing import List +from typing import Optional +from typing import Sequence +from typing import Tuple +from typing import TYPE_CHECKING +from typing import Union from acme import challenges from certbot import configuration @@ -10,24 +18,27 @@ from certbot import util from certbot._internal import constants from certbot.compat import os +if TYPE_CHECKING: + from certbot._internal.cli import helpful + class _Default: """A class to use as a default to detect if a value is set by a user""" - def __bool__(self): + def __bool__(self) -> bool: return False - def __eq__(self, other): + def __eq__(self, other: Any) -> bool: return isinstance(other, _Default) - def __hash__(self): + def __hash__(self) -> int: return id(_Default) - def __nonzero__(self): + def __nonzero__(self) -> bool: return self.__bool__() -def read_file(filename, mode="rb"): +def read_file(filename: str, mode: str = "rb") -> Tuple[str, Any]: """Returns the given file's contents. :param str filename: path to file @@ -48,7 +59,7 @@ def read_file(filename, mode="rb"): raise argparse.ArgumentTypeError(exc.strerror) -def flag_default(name): +def flag_default(name: str) -> Any: """Default value for CLI flag.""" # XXX: this is an internal housekeeping notion of defaults before # argparse has been set up; it is not accurate for all flags. Call it @@ -57,7 +68,7 @@ def flag_default(name): return copy.deepcopy(constants.CLI_DEFAULTS[name]) -def config_help(name, hidden=False): +def config_help(name: str, hidden: bool = False) -> Optional[str]: """Extract the help message for a `configuration.NamespaceConfig` property docstring.""" if hidden: return argparse.SUPPRESS @@ -73,11 +84,11 @@ class HelpfulArgumentGroup: HelpfulArgumentParser when necessary. """ - def __init__(self, helpful_arg_parser, topic): + def __init__(self, helpful_arg_parser: "helpful.HelpfulArgumentParser", topic: str) -> None: self._parser = helpful_arg_parser self._topic = topic - def add_argument(self, *args, **kwargs): + def add_argument(self, *args: Any, **kwargs: Any) -> None: """Add a new command line argument to the argument group.""" self._parser.add(self._topic, *args, **kwargs) @@ -88,12 +99,12 @@ class CustomHelpFormatter(argparse.HelpFormatter): In particular we fix https://bugs.python.org/issue28742 """ - def _get_help_string(self, action): + def _get_help_string(self, action: argparse.Action) -> Optional[str]: helpstr = action.help - if '%(default)' not in action.help and '(default:' not in action.help: + if action.help and '%(default)' not in action.help and '(default:' not in action.help: if action.default != argparse.SUPPRESS: defaulting_nargs = [argparse.OPTIONAL, argparse.ZERO_OR_MORE] - if action.option_strings or action.nargs in defaulting_nargs: + if helpstr and (action.option_strings or action.nargs in defaulting_nargs): helpstr += ' (default: %(default)s)' return helpstr @@ -101,12 +112,15 @@ class CustomHelpFormatter(argparse.HelpFormatter): class _DomainsAction(argparse.Action): """Action class for parsing domains.""" - def __call__(self, parser, namespace, domain, option_string=None): + def __call__(self, parser: argparse.ArgumentParser, namespace: argparse.Namespace, + domain: Union[str, Sequence[Any], None], + option_string: Optional[str] = None) -> None: """Just wrap add_domains in argparseese.""" - add_domains(namespace, domain) + add_domains(namespace, str(domain) if domain is not None else None) -def add_domains(args_or_config, domains): +def add_domains(args_or_config: Union[argparse.Namespace, configuration.NamespaceConfig], + domains: Optional[str]) -> List[str]: """Registers new domains to be used during the current client run. Domains are not added to the list of requested domains if they have @@ -121,7 +135,10 @@ def add_domains(args_or_config, domains): :rtype: `list` of `str` """ - validated_domains = [] + validated_domains: List[str] = [] + if not domains: + return validated_domains + for domain in domains.split(","): domain = util.enforce_domain_sanity(domain.strip()) validated_domains.append(domain) @@ -137,11 +154,13 @@ class CaseInsensitiveList(list): This class is passed to the `choices` argument of `argparse.add_arguments` through the `helpful` wrapper. It is necessary due to special handling of command line arguments by `set_by_cli` in which the `type_func` is not applied.""" - def __contains__(self, element): + def __contains__(self, element: object) -> bool: + if not isinstance(element, str): + return False return super().__contains__(element.lower()) -def _user_agent_comment_type(value): +def _user_agent_comment_type(value: str) -> str: if "(" in value or ")" in value: raise argparse.ArgumentTypeError("may not contain parentheses") return value @@ -150,13 +169,17 @@ def _user_agent_comment_type(value): class _EncodeReasonAction(argparse.Action): """Action class for parsing revocation reason.""" - def __call__(self, parser, namespace, reason, option_string=None): + def __call__(self, parser: argparse.ArgumentParser, namespace: argparse.Namespace, + reason: Union[str, Sequence[Any], None], + option_string: Optional[str] = None) -> None: """Encodes the reason for certificate revocation.""" - code = constants.REVOCATION_REASONS[reason.lower()] + if reason is None: + raise ValueError("Unexpected null reason.") + code = constants.REVOCATION_REASONS[str(reason).lower()] setattr(namespace, self.dest, code) -def parse_preferred_challenges(pref_challs): +def parse_preferred_challenges(pref_challs: Iterable[str]) -> List[str]: """Translate and validate preferred challenges. :param pref_challs: list of preferred challenge types @@ -183,9 +206,13 @@ def parse_preferred_challenges(pref_challs): class _PrefChallAction(argparse.Action): """Action class for parsing preferred challenges.""" - def __call__(self, parser, namespace, pref_challs, option_string=None): + def __call__(self, parser: argparse.ArgumentParser, namespace: argparse.Namespace, + pref_challs: Union[str, Sequence[Any], None], + option_string: Optional[str] = None) -> None: + if pref_challs is None: + raise ValueError("Unexpected null pref_challs.") try: - challs = parse_preferred_challenges(pref_challs.split(",")) + challs = parse_preferred_challenges(str(pref_challs).split(",")) except errors.Error as error: raise argparse.ArgumentError(self, str(error)) namespace.pref_challs.extend(challs) @@ -194,7 +221,9 @@ class _PrefChallAction(argparse.Action): class _DeployHookAction(argparse.Action): """Action class for parsing deploy hooks.""" - def __call__(self, parser, namespace, values, option_string=None): + def __call__(self, parser: argparse.ArgumentParser, namespace: argparse.Namespace, + values: Union[str, Sequence[Any], None], + option_string: Optional[str] = None) -> None: renew_hook_set = namespace.deploy_hook != namespace.renew_hook if renew_hook_set and namespace.renew_hook != values: raise argparse.ArgumentError( @@ -205,7 +234,9 @@ class _DeployHookAction(argparse.Action): class _RenewHookAction(argparse.Action): """Action class for parsing renew hooks.""" - def __call__(self, parser, namespace, values, option_string=None): + def __call__(self, parser: argparse.ArgumentParser, namespace: argparse.Namespace, + values: Union[str, Sequence[Any], None], + option_string: Optional[str] = None) -> None: deploy_hook_set = namespace.deploy_hook is not None if deploy_hook_set and namespace.deploy_hook != values: raise argparse.ArgumentError( @@ -213,7 +244,7 @@ class _RenewHookAction(argparse.Action): namespace.renew_hook = values -def nonnegative_int(value): +def nonnegative_int(value: str) -> int: """Converts value to an int and checks that it is not negative. This function should used as the type parameter for argparse diff --git a/certbot/certbot/_internal/cli/group_adder.py b/certbot/certbot/_internal/cli/group_adder.py index 0c54c9fe1..96d58824b 100644 --- a/certbot/certbot/_internal/cli/group_adder.py +++ b/certbot/certbot/_internal/cli/group_adder.py @@ -1,9 +1,14 @@ """This module contains a function to add the groups of arguments for the help display""" +from typing import TYPE_CHECKING + from certbot._internal.cli.verb_help import VERB_HELP +if TYPE_CHECKING: + from certbot._internal.cli import helpful -def _add_all_groups(helpful): + +def _add_all_groups(helpful: "helpful.HelpfulArgumentParser") -> None: helpful.add_group("automation", description="Flags for automating execution & other tweaks") helpful.add_group("security", description="Security parameters & server settings") helpful.add_group("testing", diff --git a/certbot/certbot/_internal/cli/helpful.py b/certbot/certbot/_internal/cli/helpful.py index 0848829c4..714c487a4 100644 --- a/certbot/certbot/_internal/cli/helpful.py +++ b/certbot/certbot/_internal/cli/helpful.py @@ -7,6 +7,10 @@ import glob import sys from typing import Any from typing import Dict +from typing import Iterable +from typing import List +from typing import Optional +from typing import Union import configargparse @@ -29,6 +33,7 @@ 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._internal.plugins import disco from certbot.compat import os @@ -40,7 +45,8 @@ class HelpfulArgumentParser: 'certbot --help security' for security options. """ - def __init__(self, args, plugins, detect_defaults=False): + def __init__(self, args: List[str], plugins: Iterable[str], + detect_defaults: bool = False) -> None: from certbot._internal import main self.VERBS = { "auth": main.certonly, @@ -80,6 +86,7 @@ class HelpfulArgumentParser: self.determine_verb() help1 = self.prescan_for_flag("-h", self.help_topics) help2 = self.prescan_for_flag("--help", self.help_topics) + self.help_arg: Union[str, bool] if isinstance(help1, bool) and isinstance(help2, bool): self.help_arg = help1 or help2 else: @@ -111,7 +118,7 @@ class HelpfulArgumentParser: # Help that are synonyms for --help subcommands COMMANDS_TOPICS = ["command", "commands", "subcommand", "subcommands", "verbs"] - def _list_subcommands(self): + def _list_subcommands(self) -> str: longest = max(len(v) for v in VERB_HELP_MAP) text = "The full list of available SUBCOMMANDS is:\n\n" @@ -122,7 +129,7 @@ class HelpfulArgumentParser: text += "\nYou can get more help on a specific subcommand with --help SUBCOMMAND\n" return text - def _usage_string(self, plugins, help_arg): + def _usage_string(self, plugins: Iterable[str], help_arg: Union[str, bool]) -> str: """Make usage strings late so that plugins can be initialised late :param plugins: all discovered plugins @@ -150,13 +157,14 @@ class HelpfulArgumentParser: # if we're doing --help all, the OVERVIEW is part of the SHORT_USAGE at # the top; if we're doing --help someothertopic, it's OT so it's not usage += COMMAND_OVERVIEW % (apache_doc, nginx_doc) - else: + elif isinstance(help_arg, str): custom = VERB_HELP_MAP.get(help_arg, {}).get("usage", None) usage = custom if custom else usage + # Only remaining case is help_arg == False, which gives effectively usage == SHORT_USAGE. return usage - def remove_config_file_domains_for_renewal(self, parsed_args): + def remove_config_file_domains_for_renewal(self, parsed_args: argparse.Namespace) -> None: """Make "certbot renew" safe if domains are set in cli.ini.""" # Works around https://github.com/certbot/certbot/issues/4096 if self.verb == "renew": @@ -164,7 +172,7 @@ class HelpfulArgumentParser: if source.startswith("config_file") and "domains" in flags: parsed_args.domains = _Default() if self.detect_defaults else [] - def parse_args(self): + def parse_args(self) -> argparse.Namespace: """Parses command line arguments and returns the result. :returns: parsed command line arguments @@ -224,7 +232,7 @@ class HelpfulArgumentParser: return parsed_args - def set_test_server(self, parsed_args): + def set_test_server(self, parsed_args: argparse.Namespace) -> None: """We have --staging/--dry-run; perform sanity check and set config.server""" # Flag combinations should produce these results: @@ -253,7 +261,7 @@ class HelpfulArgumentParser: parsed_args.tos = True parsed_args.register_unsafely_without_email = True - def handle_csr(self, parsed_args): + def handle_csr(self, parsed_args: argparse.Namespace) -> None: """Process a --csr flag.""" if parsed_args.verb != "certonly": raise errors.Error("Currently, a CSR file may only be specified " @@ -287,7 +295,7 @@ class HelpfulArgumentParser: .format(", ".join(csr_domains), ", ".join(config_domains))) - def determine_verb(self): + def determine_verb(self) -> None: """Determines the verb/subcommand provided by the user. This function works around some of the limitations of argparse. @@ -311,7 +319,7 @@ class HelpfulArgumentParser: self.verb = "run" - def prescan_for_flag(self, flag, possible_arguments): + def prescan_for_flag(self, flag: str, possible_arguments: Iterable[str]) -> Union[str, bool]: """Checks cli input for flags. Check for a flag, which accepts a fixed set of possible arguments, in @@ -332,7 +340,8 @@ class HelpfulArgumentParser: pass return True - def add(self, topics, *args, **kwargs): + def add(self, topics: Optional[Union[List[Optional[str]], str]], *args: Any, + **kwargs: Any) -> None: """Add a new command line argument. :param topics: str or [str] help topic(s) this should be listed under, @@ -367,7 +376,7 @@ class HelpfulArgumentParser: if self.detect_defaults: kwargs = self.modify_kwargs_for_default_detection(**kwargs) - if self.visible_topics[topic]: + if isinstance(topic, str) and self.visible_topics[topic]: if topic in self.groups: group = self.groups[topic] group.add_argument(*args, **kwargs) @@ -377,7 +386,7 @@ class HelpfulArgumentParser: kwargs["help"] = argparse.SUPPRESS self.parser.add_argument(*args, **kwargs) - def modify_kwargs_for_default_detection(self, **kwargs): + def modify_kwargs_for_default_detection(self, **kwargs: Any) -> Dict[str, Any]: """Modify an arg so we can check if it was set by the user. Changes the parameters given to argparse when adding an argument @@ -399,7 +408,7 @@ class HelpfulArgumentParser: return kwargs - def add_deprecated_argument(self, argument_name, num_args): + def add_deprecated_argument(self, argument_name: str, num_args: int) -> None: """Adds a deprecated argument with the name argument_name. Deprecated arguments are not shown in the help. If they are used @@ -407,7 +416,7 @@ class HelpfulArgumentParser: argument is deprecated and no other action is taken. :param str argument_name: Name of deprecated argument. - :param int nargs: Number of arguments the option takes. + :param int num_args: Number of arguments the option takes. """ # certbot.util.add_deprecated_argument expects the normal add_argument @@ -427,7 +436,8 @@ class HelpfulArgumentParser: add_func = functools.partial(self.add, None) util.add_deprecated_argument(add_func, argument_name, num_args) - def add_group(self, topic, verbs=(), **kwargs): + def add_group(self, topic: str, verbs: Iterable[str] = (), + **kwargs: Any) -> HelpfulArgumentGroup: """Create a new argument group. This method must be called once for every topic, however, calls @@ -449,7 +459,7 @@ class HelpfulArgumentParser: self.groups[topic].add_argument(v, help=VERB_HELP_MAP[v]["short"]) return HelpfulArgumentGroup(self, topic) - def add_plugin_args(self, plugins): + def add_plugin_args(self, plugins: disco.PluginsRegistry) -> None: """ Let each of the plugins add its own command line arguments, which @@ -461,7 +471,7 @@ class HelpfulArgumentParser: description=plugin_ep.long_description) plugin_ep.plugin_cls.inject_parser_options(parser_or_group, name) - def determine_help_topics(self, chosen_topic): + def determine_help_topics(self, chosen_topic: Union[str, bool]) -> Dict[str, bool]: """ The user may have requested help on a topic, return a dict of which diff --git a/certbot/certbot/_internal/cli/paths_parser.py b/certbot/certbot/_internal/cli/paths_parser.py index 6197a4bf9..69a112cfa 100644 --- a/certbot/certbot/_internal/cli/paths_parser.py +++ b/certbot/certbot/_internal/cli/paths_parser.py @@ -1,13 +1,19 @@ """This is a module that adds configuration to the argument parser regarding paths for certificates""" +from typing import TYPE_CHECKING +from typing import Union + from certbot._internal.cli.cli_utils import config_help from certbot._internal.cli.cli_utils import flag_default from certbot.compat import os +if TYPE_CHECKING: + from certbot._internal.cli import helpful -def _paths_parser(helpful): + +def _paths_parser(helpful: "helpful.HelpfulArgumentParser") -> None: add = helpful.add - verb = helpful.verb + verb: Union[str, bool] = helpful.verb if verb == "help": verb = helpful.help_arg @@ -21,7 +27,7 @@ def _paths_parser(helpful): add(["paths", "install", "revoke", "certonly", "manage"], "--cert-path", **cpkwargs) section = "paths" - if verb in ("install", "revoke"): + if isinstance(verb, str) and verb in ("install", "revoke"): section = verb add(section, "--key-path", type=os.path.abspath, help="Path to private key for certificate installation " diff --git a/certbot/certbot/_internal/cli/plugins_parsing.py b/certbot/certbot/_internal/cli/plugins_parsing.py index bbfdf22da..f0a976bf4 100644 --- a/certbot/certbot/_internal/cli/plugins_parsing.py +++ b/certbot/certbot/_internal/cli/plugins_parsing.py @@ -1,8 +1,15 @@ """This is a module that handles parsing of plugins for the argument parser""" +from typing import TYPE_CHECKING + from certbot._internal.cli.cli_utils import flag_default +from certbot._internal.plugins import disco + +if TYPE_CHECKING: + from certbot._internal.cli import helpful -def _plugins_parsing(helpful, plugins): +def _plugins_parsing(helpful: "helpful.HelpfulArgumentParser", + plugins: disco.PluginsRegistry) -> None: # It's nuts, but there are two "plugins" topics. Somehow this works helpful.add_group( "plugins", description="Plugin Selection: Certbot client supports an " diff --git a/certbot/certbot/_internal/cli/subparsers.py b/certbot/certbot/_internal/cli/subparsers.py index d872cf71a..5e3304ba1 100644 --- a/certbot/certbot/_internal/cli/subparsers.py +++ b/certbot/certbot/_internal/cli/subparsers.py @@ -1,4 +1,6 @@ """This module creates subparsers for the argument parser""" +from typing import TYPE_CHECKING + from certbot import interfaces from certbot._internal import constants from certbot._internal.cli.cli_utils import _EncodeReasonAction @@ -7,8 +9,11 @@ from certbot._internal.cli.cli_utils import CaseInsensitiveList from certbot._internal.cli.cli_utils import flag_default from certbot._internal.cli.cli_utils import read_file +if TYPE_CHECKING: + from certbot._internal.cli import helpful -def _create_subparsers(helpful): + +def _create_subparsers(helpful: "helpful.HelpfulArgumentParser") -> None: from certbot._internal.client import sample_user_agent # avoid import loops helpful.add( None, "--user-agent", default=flag_default("user_agent"), diff --git a/certbot/certbot/_internal/client.py b/certbot/certbot/_internal/client.py index 4718098d3..ae1cfd838 100644 --- a/certbot/certbot/_internal/client.py +++ b/certbot/certbot/_internal/client.py @@ -4,9 +4,12 @@ import logging import platform from typing import cast from typing import Any +from typing import Callable from typing import Dict +from typing import IO from typing import List from typing import Optional +from typing import Tuple from typing import Union import warnings @@ -20,8 +23,10 @@ from acme import crypto_util as acme_crypto_util from acme import errors as acme_errors from acme import messages import certbot +from certbot import configuration from certbot import crypto_util from certbot import errors +from certbot import interfaces from certbot import util from certbot._internal import account from certbot._internal import auth_handler @@ -30,15 +35,20 @@ from certbot._internal import constants from certbot._internal import eff from certbot._internal import error_handler from certbot._internal import storage +from certbot._internal.plugins import disco as plugin_disco from certbot._internal.plugins import selection as plugin_selection from certbot.compat import os from certbot.display import ops as display_ops from certbot.display import util as display_util +from certbot.interfaces import AccountStorage logger = logging.getLogger(__name__) -def acme_from_config_key(config, key, regr=None): - "Wrangle ACME client construction" + +def acme_from_config_key(config: configuration.NamespaceConfig, key: jose.JWK, + regr: Optional[messages.RegistrationResource] = None + ) -> acme_client.ClientV2: + """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)) @@ -52,10 +62,10 @@ def acme_from_config_key(config, key, regr=None): "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 + return cast(acme_client.ClientV2, client) -def determine_user_agent(config): +def determine_user_agent(config: configuration.NamespaceConfig) -> str: """ Set a user_agent string in the config based on the choice of plugins. (this wasn't knowable at construction time) @@ -86,8 +96,9 @@ def determine_user_agent(config): ua = config.user_agent return ua -def ua_flags(config): - "Turn some very important CLI flags into clues in the user agent." + +def ua_flags(config: configuration.NamespaceConfig) -> str: + """Turn some very important CLI flags into clues in the user agent.""" if isinstance(config, DummyConfig): return "FLAGS" flags = [] @@ -105,25 +116,30 @@ def ua_flags(config): flags.append("hook") return " ".join(flags) + class DummyConfig: - "Shim for computing a sample user agent." - def __init__(self): + """Shim for computing a sample user agent.""" + def __init__(self) -> None: self.authenticator = "XXX" self.installer = "YYY" self.user_agent = None self.verb = "SUBCOMMAND" - def __getattr__(self, name): - "Any config properties we might have are None." + def __getattr__(self, name: str) -> Any: + """Any config properties we might have are None.""" return None -def sample_user_agent(): - "Document what this Certbot's user agent string will be like." - return determine_user_agent(DummyConfig()) +def sample_user_agent() -> str: + """Document what this Certbot's user agent string will be like.""" + # DummyConfig is designed to mock certbot.configuration.NamespaceConfig. + # Let mypy accept that. + return determine_user_agent(cast(configuration.NamespaceConfig, DummyConfig())) -def register(config, account_storage, tos_cb=None): +def register(config: configuration.NamespaceConfig, account_storage: AccountStorage, + tos_cb: Optional[Callable[[str], None]] = None + ) -> Tuple[account.Account, acme_client.ClientV2]: """Register new account with an ACME CA. This function takes care of generating fresh private key, @@ -143,13 +159,11 @@ def register(config, account_storage, tos_cb=None): Service before registering account, client action is necessary. For example, a CLI tool would prompt the user acceptance. `tos_cb` must be a callable that should accept - `.RegistrationResource` and return a `bool`: ``True`` iff the - Terms of Service present in the contained - `.Registration.terms_of_service` is accepted by the client, and - ``False`` otherwise. ``tos_cb`` will be called only if the - client action is necessary, i.e. when ``terms_of_service is not - None``. This argument is optional, if not supplied it will - default to automatic acceptance! + a Term of Service URL as a string, and raise an exception + if the TOS is not accepted by the client. ``tos_cb`` will be + called only if the client action is necessary, i.e. when + ``terms_of_service is not None``. This argument is optional, + if not supplied it will default to automatic acceptance! :raises certbot.errors.Error: In case of any client problems, in particular registration failure, or unaccepted Terms of Service. @@ -194,12 +208,13 @@ def register(config, account_storage, tos_cb=None): return acc, acme -def perform_registration(acme, config, tos_cb): +def perform_registration(acme: acme_client.ClientV2, config: configuration.NamespaceConfig, + tos_cb: Optional[Callable[[str], None]]) -> messages.RegistrationResource: """ Actually register new account, trying repeatedly if there are email problems - :param acme.client.Client client: ACME client object. + :param acme.client.Client acme: ACME client object. :param certbot.configuration.NamespaceConfig config: Client configuration. :param Callable tos_cb: a callback to handle Term of Service agreement. @@ -210,11 +225,11 @@ def perform_registration(acme, config, tos_cb): eab_credentials_supplied = config.eab_kid and config.eab_hmac_key eab: Optional[Dict[str, Any]] if eab_credentials_supplied: - account_public_key = acme.client.net.key.public_key() + account_public_key = acme.net.key.public_key() eab = messages.ExternalAccountBinding.from_data(account_public_key=account_public_key, kid=config.eab_kid, hmac_key=config.eab_hmac_key, - directory=acme.client.directory) + directory=acme.directory) else: eab = None @@ -225,11 +240,17 @@ def perform_registration(acme, config, tos_cb): raise errors.Error(msg) try: - # TODO: Remove the cast once certbot package is fully typed newreg = messages.NewRegistration.from_data( - email=config.email, - external_account_binding=cast(Optional[messages.ExternalAccountBinding], eab)) - return acme.new_account_and_tos(newreg, tos_cb) + email=config.email, external_account_binding=eab) + # Until ACME v1 support is removed from Certbot, we actually need the provided + # ACME client to be a wrapper of type BackwardsCompatibleClientV2. + # TODO: Remove this cast and rewrite the logic when the client is actually a ClientV2 + try: + return cast(acme_client.BackwardsCompatibleClientV2, + acme).new_account_and_tos(newreg, tos_cb) + except AttributeError: + raise errors.Error("The ACME client must be an instance of " + "acme.client.BackwardsCompatibleClientV2") except messages.Error as e: if e.code in ("invalidEmail", "invalidContact"): if config.noninteractive_mode: @@ -258,7 +279,10 @@ class Client: """ - def __init__(self, config, account_, auth, installer, acme=None): + def __init__(self, config: configuration.NamespaceConfig, account_: Optional[account.Account], + auth: Optional[interfaces.Authenticator], + installer: Optional[interfaces.Installer], + acme: Optional[acme_client.ClientV2] = None) -> None: """Initialize a client.""" self.config = config self.account = account_ @@ -277,7 +301,9 @@ class Client: else: self.auth_handler = None - def obtain_certificate_from_csr(self, csr, orderr=None): + def obtain_certificate_from_csr(self, csr: util.CSR, + orderr: Optional[messages.OrderResource] = None + ) -> Tuple[bytes, bytes]: """Obtain certificate. :param .util.CSR csr: PEM-encoded Certificate Signing @@ -294,28 +320,34 @@ class Client: "not set.") logger.error(msg) raise errors.Error(msg) - if self.account.regr is None: + if self.account is None or self.account.regr is None: raise errors.Error("Please register with the ACME server first.") + if self.acme is None: + raise errors.Error("ACME client is not set.") logger.debug("CSR: %s", csr) if orderr is None: orderr = self._get_order_and_authorizations(csr.data, best_effort=False) - deadline = datetime.datetime.now() + datetime.timedelta(seconds=90) - get_alt_chains = self.config.preferred_chain is not None - orderr = self.acme.finalize_order(orderr, deadline, - fetch_alternative_chains=get_alt_chains) + deadline = datetime.datetime.now() + datetime.timedelta( + seconds=self.config.issuance_timeout) + + logger.debug("Will poll for certificate issuance until %s", deadline) + + orderr = self.acme.finalize_order( + orderr, deadline, fetch_alternative_chains=self.config.preferred_chain is not None) + fullchain = orderr.fullchain_pem - if get_alt_chains and orderr.alternative_fullchains_pem: - fullchain = crypto_util.find_chain_with_issuer([fullchain] + \ - orderr.alternative_fullchains_pem, - self.config.preferred_chain, - not self.config.dry_run) + if self.config.preferred_chain and orderr.alternative_fullchains_pem: + fullchain = crypto_util.find_chain_with_issuer( + [fullchain] + orderr.alternative_fullchains_pem, + self.config.preferred_chain, not self.config.dry_run) cert, chain = crypto_util.cert_and_chain_from_fullchain(fullchain) return cert.encode(), chain.encode() - def obtain_certificate(self, domains, old_keypath=None): + def obtain_certificate(self, domains: List[str], old_keypath: Optional[str] = None + ) -> Tuple[bytes, bytes, util.Key, util.CSR]: """Obtains a certificate from the ACME server. `.register` must be called before `.obtain_certificate` @@ -408,11 +440,11 @@ class Client: cert, chain = self.obtain_certificate_from_csr(csr, orderr) return cert, chain, key, csr - def _get_order_and_authorizations(self, csr_pem: str, + def _get_order_and_authorizations(self, csr_pem: bytes, best_effort: bool) -> messages.OrderResource: """Request a new order and complete its authorizations. - :param str csr_pem: A CSR in PEM format. + :param bytes csr_pem: A CSR in PEM format. :param bool best_effort: True if failing to complete all authorizations should not raise an exception @@ -420,6 +452,8 @@ class Client: :rtype: acme.messages.OrderResource """ + if not self.acme: + raise errors.Error("ACME client is not set.") try: orderr = self.acme.new_order(csr_pem) except acme_errors.WildcardUnsupportedError: @@ -442,7 +476,8 @@ class Client: authzr = self.auth_handler.handle_authorizations(orderr, self.config, best_effort) return orderr.update(authorizations=authzr) - def obtain_and_enroll_certificate(self, domains, certname): + def obtain_and_enroll_certificate(self, domains: List[str], certname: Optional[str] + ) -> Optional[storage.RenewableCert]: """Obtain and enroll certificate. Get a new certificate for the specified domains using the specified @@ -455,8 +490,7 @@ class Client: :type certname: `str` or `None` :returns: A new :class:`certbot._internal.storage.RenewableCert` instance - referred to the enrolled cert lineage, False if the cert could not - be obtained, or None if doing a successful dry run. + referred to the enrolled cert lineage, or None if doing a successful dry run. """ cert, chain, key, _ = self.obtain_certificate(domains) @@ -470,15 +504,14 @@ class Client: new_name = self._choose_lineagename(domains, certname) if self.config.dry_run: - logger.debug("Dry run: Skipping creating new lineage for %s", - new_name) + logger.debug("Dry run: Skipping creating new lineage for %s", new_name) return None return storage.RenewableCert.new_lineage( new_name, cert, key.pem, chain, self.config) - def _choose_lineagename(self, domains, certname): + def _choose_lineagename(self, domains: List[str], certname: Optional[str]) -> str: """Chooses a name for the new lineage. :param domains: domains in certificate request @@ -497,12 +530,13 @@ class Client: return domains[0][2:] return domains[0] - def save_certificate(self, cert_pem, chain_pem, - cert_path, chain_path, fullchain_path): + def save_certificate(self, cert_pem: bytes, chain_pem: bytes, + cert_path: str, chain_path: str, fullchain_path: str + ) -> Tuple[str, str, str]: """Saves the certificate received from the ACME server. - :param str cert_pem: - :param str chain_pem: + :param bytes cert_pem: + :param bytes chain_pem: :param str cert_path: Candidate path to a certificate. :param str chain_path: Candidate path to a certificate chain. :param str fullchain_path: Candidate path to a full cert chain. @@ -517,7 +551,6 @@ class Client: for path in cert_path, chain_path, fullchain_path: util.make_or_verify_dir(os.path.dirname(path), 0o755, self.config.strict_permissions) - cert_file, abs_cert_path = _open_pem_file('cert_path', cert_path) try: @@ -525,17 +558,16 @@ class Client: finally: cert_file.close() - chain_file, abs_chain_path =\ - _open_pem_file('chain_path', chain_path) - fullchain_file, abs_fullchain_path =\ - _open_pem_file('fullchain_path', fullchain_path) + chain_file, abs_chain_path = _open_pem_file('chain_path', chain_path) + fullchain_file, abs_fullchain_path = _open_pem_file('fullchain_path', fullchain_path) _save_chain(chain_pem, chain_file) _save_chain(cert_pem + chain_pem, fullchain_file) return abs_cert_path, abs_chain_path, abs_fullchain_path - def deploy_certificate(self, domains, privkey_path, cert_path, chain_path, fullchain_path): + def deploy_certificate(self, domains: List[str], privkey_path: str, cert_path: str, + chain_path: str, fullchain_path: str) -> None: """Install certificate :param list domains: list of domains to install the certificate @@ -572,7 +604,8 @@ class Client: # sites may have been enabled / final cleanup self.installer.restart() - def enhance_config(self, domains, chain_path, redirect_default=True): + def enhance_config(self, domains: List[str], chain_path: str, + redirect_default: bool = True) -> None: """Enhance the configuration. :param list domains: list of domains to configure @@ -630,6 +663,8 @@ class Client: """ + if not self.installer: + raise errors.Error("No installer plugin has been set.") enh_label = options if enhancement == "ensure-http-header" else enhancement with error_handler.ErrorHandler(self._recovery_routine_with_msg, None): for dom in domains: @@ -649,32 +684,34 @@ class Client: :param str success_msg: message to show on successful recovery """ - self.installer.recovery_routine() - if success_msg: - display_util.notify(success_msg) + if self.installer: + self.installer.recovery_routine() + if success_msg: + display_util.notify(success_msg) - def _rollback_and_restart(self, success_msg): + def _rollback_and_restart(self, success_msg: str) -> None: """Rollback the most recent checkpoint and restart the webserver :param str success_msg: message to show on successful rollback """ - logger.info("Rolling back to previous server configuration...") - try: - self.installer.rollback_checkpoints() - self.installer.restart() - except: - logger.error( - "An error occurred and we failed to restore your config and " - "restart your server. Please post to " - "https://community.letsencrypt.org/c/help " - "with details about your configuration and this error you received." - ) - raise - display_util.notify(success_msg) + if self.installer: + logger.info("Rolling back to previous server configuration...") + try: + self.installer.rollback_checkpoints() + self.installer.restart() + except: + logger.error( + "An error occurred and we failed to restore your config and " + "restart your server. Please post to " + "https://community.letsencrypt.org/c/help " + "with details about your configuration and this error you received." + ) + raise + display_util.notify(success_msg) -def validate_key_csr(privkey, csr=None): +def validate_key_csr(privkey: util.Key, csr: Optional[util.CSR] = None) -> None: """Validate Key and CSR files. Verifies that the client key and csr arguments are valid and correspond to @@ -720,13 +757,16 @@ def validate_key_csr(privkey, csr=None): raise errors.Error("The key and CSR do not match") -def rollback(default_installer, checkpoints, config, plugins): +def rollback(default_installer: str, checkpoints: int, + config: configuration.NamespaceConfig, plugins: plugin_disco.PluginsRegistry) -> None: """Revert configuration the specified number of checkpoints. + :param str default_installer: Default installer name to use for the rollback :param int checkpoints: Number of checkpoints to revert. - :param config: Configuration. :type config: :class:`certbot.configuration.NamespaceConfiguration` + :param plugins: Plugins available + :type plugins: :class:`certbot._internal.plugins.disco.PluginsRegistry` """ # Misconfigurations are only a slight problems... allow the user to rollback @@ -741,7 +781,8 @@ def rollback(default_installer, checkpoints, config, plugins): installer.rollback_checkpoints(checkpoints) installer.restart() -def _open_pem_file(cli_arg_path, pem_path): + +def _open_pem_file(cli_arg_path: str, pem_path: str) -> Tuple[IO, str]: """Open a pem file. If cli_arg_path was set by the client, open that. @@ -759,10 +800,11 @@ def _open_pem_file(cli_arg_path, pem_path): uniq = util.unique_file(pem_path, 0o644, "wb") return uniq[0], os.path.abspath(uniq[1]) -def _save_chain(chain_pem, chain_file): + +def _save_chain(chain_pem: bytes, chain_file: IO) -> None: """Saves chain_pem at a unique path based on chain_path. - :param str chain_pem: certificate chain in PEM format + :param bytes chain_pem: certificate chain in PEM format :param str chain_file: chain file object """ diff --git a/certbot/certbot/_internal/constants.py b/certbot/certbot/_internal/constants.py index f7695670d..18160d47e 100644 --- a/certbot/certbot/_internal/constants.py +++ b/certbot/certbot/_internal/constants.py @@ -1,5 +1,7 @@ """Certbot constants.""" import logging +from typing import Any +from typing import Dict import pkg_resources @@ -13,7 +15,7 @@ SETUPTOOLS_PLUGINS_ENTRY_POINT = "certbot.plugins" OLD_SETUPTOOLS_PLUGINS_ENTRY_POINT = "letsencrypt.plugins" """Plugins Setuptools entry point before rename.""" -CLI_DEFAULTS = dict( +CLI_DEFAULTS: Dict[str, Any] = dict( config_files=[ os.path.join(misc.get_default_folder('config'), 'cli.ini'), # https://freedesktop.org/wiki/Software/xdg-user-dirs/ @@ -76,6 +78,7 @@ CLI_DEFAULTS = dict( random_sleep_on_renew=True, eab_hmac_key=None, eab_kid=None, + issuance_timeout=90, # Subparsers num=None, diff --git a/certbot/certbot/_internal/display/completer.py b/certbot/certbot/_internal/display/completer.py index b43859b19..821aba780 100644 --- a/certbot/certbot/_internal/display/completer.py +++ b/certbot/certbot/_internal/display/completer.py @@ -1,8 +1,14 @@ """Provides Tab completion when prompting users for a path.""" import glob +from types import TracebackType from typing import Callable from typing import Iterator from typing import Optional +from typing import Type +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing_extensions import Literal # readline module is not available on all systems try: @@ -28,12 +34,12 @@ class Completer: """ - def __init__(self): + def __init__(self) -> None: self._iter: Iterator[str] self._original_completer: Optional[Callable] self._original_delims: str - def complete(self, text, state): + def complete(self, text: str, state: int) -> Optional[str]: """Provides path completion for use with readline. :param str text: text to offer completions for @@ -48,7 +54,7 @@ class Completer: self._iter = glob.iglob(text + '*') return next(self._iter, None) - def __enter__(self): + def __enter__(self) -> None: self._original_completer = readline.get_completer() self._original_delims = readline.get_completer_delims() @@ -62,6 +68,9 @@ class Completer: else: readline.parse_and_bind('tab: complete') - def __exit__(self, unused_type, unused_value, unused_traceback): + def __exit__(self, unused_type: Optional[Type[BaseException]], + unused_value: Optional[BaseException], + unused_traceback: Optional[TracebackType]) -> 'Literal[False]': readline.set_completer_delims(self._original_delims) readline.set_completer(self._original_completer) + return False diff --git a/certbot/certbot/_internal/display/dummy_readline.py b/certbot/certbot/_internal/display/dummy_readline.py index fb3d807bb..2b6e4c310 100644 --- a/certbot/certbot/_internal/display/dummy_readline.py +++ b/certbot/certbot/_internal/display/dummy_readline.py @@ -1,21 +1,25 @@ """A dummy module with no effect for use on systems without readline.""" +from typing import Callable +from typing import Iterable +from typing import List +from typing import Optional -def get_completer(): +def get_completer() -> Optional[Callable[[], str]]: """An empty implementation of readline.get_completer.""" -def get_completer_delims(): +def get_completer_delims() -> List[str]: """An empty implementation of readline.get_completer_delims.""" -def parse_and_bind(unused_command): +def parse_and_bind(unused_command: str) -> None: """An empty implementation of readline.parse_and_bind.""" -def set_completer(unused_function=None): +def set_completer(unused_function: Optional[Callable[[], str]] = None) -> None: """An empty implementation of readline.set_completer.""" -def set_completer_delims(unused_delims): +def set_completer_delims(unused_delims: Iterable[str]) -> None: """An empty implementation of readline.set_completer_delims.""" diff --git a/certbot/certbot/_internal/display/obj.py b/certbot/certbot/_internal/display/obj.py index b30587b4e..39b737e80 100644 --- a/certbot/certbot/_internal/display/obj.py +++ b/certbot/certbot/_internal/display/obj.py @@ -1,7 +1,13 @@ """This modules define the actual display implementations used in Certbot""" import logging import sys +from typing import Any +from typing import Iterable +from typing import List from typing import Optional +from typing import TextIO +from typing import Tuple +from typing import TypeVar from typing import Union import zope.component @@ -35,12 +41,14 @@ it as a heading)""" # 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): + def __init__(self) -> None: self.display: Optional[Union[FileDisplay, NoninteractiveDisplay]] = None _SERVICE = _DisplayService() +T = TypeVar("T") + # This use of IDisplay can be removed when this class is no longer accessible # through the public API in certbot.display.util. @@ -49,15 +57,14 @@ class FileDisplay: """File-based display.""" # see https://github.com/certbot/certbot/issues/3915 - def __init__(self, outfile, force_interactive): + def __init__(self, outfile: TextIO, force_interactive: bool) -> None: 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): + def notification(self, 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 @@ -89,9 +96,11 @@ class FileDisplay: 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: str, choices: Union[List[Tuple[str, str]], List[str]], + ok_label: Optional[str] = None, cancel_label: Optional[str] = None, # pylint: disable=unused-argument + help_label: Optional[str] = None, default: Optional[int] = None, # pylint: disable=unused-argument + cli_flag: Optional[str] = None, force_interactive: bool = False, + **unused_kwargs: Any) -> Tuple[str, int]: """Display a menu. .. todo:: This doesn't enable the help label/button (I wasn't sold on @@ -113,8 +122,9 @@ class FileDisplay: :rtype: tuple """ - if self._return_default(message, default, cli_flag, force_interactive): - return OK, default + return_default = self._return_default(message, default, cli_flag, force_interactive) + if return_default is not None: + return OK, return_default self._print_menu(message, choices) @@ -122,8 +132,8 @@ class FileDisplay: return code, selection - 1 - def input(self, message, default=None, - cli_flag=None, force_interactive=False, **unused_kwargs): + def input(self, message: str, default: Optional[str] = None, cli_flag: Optional[str] = None, + force_interactive: bool = False, **unused_kwargs: Any) -> Tuple[str, str]: """Accept input from the user. :param str message: message to display to the user @@ -138,8 +148,9 @@ class FileDisplay: :rtype: tuple """ - if self._return_default(message, default, cli_flag, force_interactive): - return OK, default + return_default = self._return_default(message, default, cli_flag, force_interactive) + if return_default is not None: + return OK, return_default # Trailing space must be added outside of util.wrap_lines to # be preserved @@ -150,8 +161,9 @@ class FileDisplay: 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): + def yesno(self, message: str, yes_label: str = "Yes", no_label: str = "No", + default: Optional[bool] = None, cli_flag: Optional[str] = None, + force_interactive: bool = False, **unused_kwargs: Any) -> bool: """Query the user with a yes/no question. Yes and No label must begin with different letters, and must contain at @@ -169,8 +181,9 @@ class FileDisplay: :rtype: bool """ - if self._return_default(message, default, cli_flag, force_interactive): - return default + return_default = self._return_default(message, default, cli_flag, force_interactive) + if return_default is not None: + return return_default message = util.wrap_lines(message) @@ -192,8 +205,9 @@ class FileDisplay: ans.startswith(no_label[0].upper())): return False - def checklist(self, message, tags, default=None, - cli_flag=None, force_interactive=False, **unused_kwargs): + def checklist(self, message: str, tags: List[str], default: Optional[List[str]] = None, + cli_flag: Optional[str] = None, force_interactive: bool = False, + **unused_kwargs: Any) -> Tuple[str, List[str]]: """Display a checklist. :param str message: Message to display to user @@ -209,8 +223,9 @@ class FileDisplay: :rtype: tuple """ - if self._return_default(message, default, cli_flag, force_interactive): - return OK, default + return_default = self._return_default(message, default, cli_flag, force_interactive) + if return_default is not None: + return OK, return_default while True: self._print_menu(message, tags) @@ -233,22 +248,23 @@ class FileDisplay: else: return code, [] - def _return_default(self, prompt, default, cli_flag, force_interactive): + def _return_default(self, prompt: str, default: Optional[T], + cli_flag: Optional[str], force_interactive: bool) -> Optional[T]: """Should we return the default instead of prompting the user? :param str prompt: prompt for the user - :param default: default answer to prompt + :param T 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 - :returns: True if we should return the default without prompting - :rtype: bool + :returns: The default value if we should return it else `None` + :rtype: T or `None` """ # assert_valid_call(prompt, default, cli_flag, force_interactive) if self._can_interact(force_interactive): - return False + return None if default is None: msg = "Unable to get an answer for the question:\n{0}".format(prompt) if cli_flag: @@ -259,9 +275,9 @@ class FileDisplay: logger.debug( "Falling back to default %s for the prompt:\n%s", default, prompt) - return True + return default - def _can_interact(self, force_interactive): + def _can_interact(self, force_interactive: bool) -> bool: """Can we safely interact with the user? :param bool force_interactive: if interactivity is forced @@ -282,8 +298,9 @@ class FileDisplay: self.skipped_interaction = True return False - def directory_select(self, message, default=None, cli_flag=None, - force_interactive=False, **unused_kwargs): + def directory_select(self, message: str, default: Optional[str] = None, + cli_flag: Optional[str] = None, force_interactive: bool = False, + **unused_kwargs: Any) -> Tuple[str, str]: """Display a directory selection screen. :param str message: prompt to give the user @@ -300,7 +317,8 @@ class FileDisplay: with completer.Completer(): return self.input(message, default, cli_flag, force_interactive) - def _scrub_checklist_input(self, indices, tags): + def _scrub_checklist_input(self, indices: Iterable[Union[str, int]], + tags: List[str]) -> List[str]: """Validate input and transform indices to appropriate tags. :param list indices: input @@ -312,21 +330,22 @@ class FileDisplay: """ # They should all be of type int try: - indices = [int(index) for index in indices] + indices_int = [int(index) for index in indices] except ValueError: return [] # Remove duplicates - indices = list(set(indices)) + indices_int = list(set(indices_int)) # Check all input is within range - for index in indices: + for index in indices_int: if index < 1 or index > len(tags): return [] - # Transform indices to appropriate tags - return [tags[index - 1] for index in indices] + # Transform indices_int to appropriate tags + return [tags[index - 1] for index in indices_int] - def _print_menu(self, message, choices): + def _print_menu(self, message: str, + choices: Union[List[Tuple[str, str]], List[str]]) -> None: """Print a menu on the screen. :param str message: title of menu @@ -355,7 +374,7 @@ class FileDisplay: self.outfile.write(SIDE_FRAME + os.linesep) self.outfile.flush() - def _get_valid_int_ans(self, max_): + def _get_valid_int_ans(self, max_: int) -> Tuple[str, int]: """Get a numerical selection. :param int max: The maximum entry (len of choices), must be positive @@ -398,21 +417,23 @@ class FileDisplay: class NoninteractiveDisplay: """A display utility implementation that never asks for interactive user input""" - def __init__(self, outfile, *unused_args, **unused_kwargs): + def __init__(self, outfile: TextIO, *unused_args: Any, **unused_kwargs: Any) -> None: 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""" + def _interaction_fail(self, message: str, cli_flag: Optional[str], + extra: str = "") -> errors.MissingCommandlineFlag: + """Return error to raise 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) + return errors.MissingCommandlineFlag(msg) - def notification(self, message, pause=False, wrap=True, decorate=True, **unused_kwargs): # pylint: disable=unused-argument + def notification(self, message: str, pause: bool = False, wrap: bool = True, # pylint: disable=unused-argument + decorate: bool = True, **unused_kwargs: Any) -> None: """Displays a notification without waiting for user acceptance. :param str message: Message to display to stdout @@ -434,8 +455,10 @@ class NoninteractiveDisplay: ) self.outfile.flush() - def menu(self, message, choices, ok_label=None, cancel_label=None, - help_label=None, default=None, cli_flag=None, **unused_kwargs): + def menu(self, message: str, choices: Union[List[Tuple[str, str]], List[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, **unused_kwargs: Any) -> Tuple[str, int]: # pylint: disable=unused-argument """Avoid displaying a menu. @@ -454,11 +477,12 @@ class NoninteractiveDisplay: """ if default is None: - self._interaction_fail(message, cli_flag, "Choices: " + repr(choices)) + raise self._interaction_fail(message, cli_flag, "Choices: " + repr(choices)) return OK, default - def input(self, message, default=None, cli_flag=None, **unused_kwargs): + def input(self, message: str, default: Optional[str] = None, cli_flag: Optional[str] = None, + **unused_kwargs: Any) -> Tuple[str, str]: """Accept input from the user. :param str message: message to display to the user @@ -471,11 +495,12 @@ class NoninteractiveDisplay: """ if default is None: - self._interaction_fail(message, cli_flag) + raise 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: str, yes_label: Optional[str] = None, no_label: Optional[str] = None, # pylint: disable=unused-argument + default: Optional[bool] = None, cli_flag: Optional[str] = None, + **unused_kwargs: Any) -> bool: """Decide Yes or No, without asking anybody :param str message: question for the user @@ -487,11 +512,11 @@ class NoninteractiveDisplay: """ if default is None: - self._interaction_fail(message, cli_flag) + raise self._interaction_fail(message, cli_flag) return default - def checklist(self, message, tags, default=None, - cli_flag=None, **unused_kwargs): + def checklist(self, message: str, tags: Iterable[str], default: Optional[List[str]] = None, + cli_flag: Optional[str] = None, **unused_kwargs: Any) -> Tuple[str, List[str]]: """Display a checklist. :param str message: Message to display to user @@ -505,11 +530,11 @@ class NoninteractiveDisplay: """ if default is None: - self._interaction_fail(message, cli_flag, "? ".join(tags) + "?") + raise self._interaction_fail(message, cli_flag, "? ".join(tags) + "?") return OK, default - def directory_select(self, message, default=None, - cli_flag=None, **unused_kwargs): + def directory_select(self, message: str, default: Optional[str] = None, + cli_flag: Optional[str] = None, **unused_kwargs: Any) -> Tuple[str, str]: """Simulate prompting the user for a directory. This function returns default if it is not ``None``, otherwise, diff --git a/certbot/certbot/_internal/display/util.py b/certbot/certbot/_internal/display/util.py index b9aa132b6..d2115289e 100644 --- a/certbot/certbot/_internal/display/util.py +++ b/certbot/certbot/_internal/display/util.py @@ -1,12 +1,13 @@ """Internal Certbot display utilities.""" -from typing import List -import textwrap import sys +import textwrap +from typing import List +from typing import Optional from certbot.compat import misc -def wrap_lines(msg): +def wrap_lines(msg: str) -> str: """Format lines nicely to 80 chars. :param str msg: Original message @@ -28,7 +29,7 @@ def wrap_lines(msg): return '\n'.join(fixed_l) -def parens_around_char(label): +def parens_around_char(label: str) -> str: """Place parens around first character of label. :param str label: Must contain at least one character @@ -37,7 +38,7 @@ def parens_around_char(label): return "({first}){rest}".format(first=label[0], rest=label[1:]) -def input_with_timeout(prompt=None, timeout=36000.0): +def input_with_timeout(prompt: Optional[str] = None, timeout: float = 36000.0) -> str: """Get user input with a timeout. Behaves the same as the builtin input, however, an error is raised if @@ -67,7 +68,7 @@ def input_with_timeout(prompt=None, timeout=36000.0): return line.rstrip('\n') -def separate_list_input(input_): +def separate_list_input(input_: str) -> List[str]: """Separate a comma or space separated list. :param str input_: input from the user @@ -97,10 +98,10 @@ def summarize_domain_list(domains: List[str]) -> str: if not domains: return "" - l = len(domains) - if l == 1: + length = len(domains) + if length == 1: return domains[0] - elif l == 2: + elif length == 2: return " and ".join(domains) else: - return "{0} and {1} more domains".format(domains[0], l-1) + return "{0} and {1} more domains".format(domains[0], length-1) diff --git a/certbot/certbot/_internal/eff.py b/certbot/certbot/_internal/eff.py index cf07a3d44..2f3926895 100644 --- a/certbot/certbot/_internal/eff.py +++ b/certbot/certbot/_internal/eff.py @@ -1,5 +1,6 @@ """Subscribes users to the EFF newsletter.""" import logging +from typing import cast from typing import Optional import requests @@ -32,16 +33,18 @@ def prepare_subscription(config: configuration.NamespaceConfig, acc: Account) -> if config.email is None: _report_failure("you didn't provide an e-mail address") else: - acc.meta = acc.meta.update(register_to_eff=config.email) + # TODO: Remove cast when https://github.com/certbot/certbot/pull/9073 is merged. + acc.meta = cast(Account.Meta, acc.meta.update(register_to_eff=config.email)) elif config.email and _want_subscription(): - acc.meta = acc.meta.update(register_to_eff=config.email) + # TODO: Remove cast when https://github.com/certbot/certbot/pull/9073 is merged. + acc.meta = cast(Account.Meta, acc.meta.update(register_to_eff=config.email)) if acc.meta.register_to_eff: storage = AccountFileStorage(config) storage.update_meta(acc) -def handle_subscription(config: configuration.NamespaceConfig, acc: Account) -> None: +def handle_subscription(config: configuration.NamespaceConfig, acc: Optional[Account]) -> None: """High level function to take care of EFF newsletter subscriptions. Once subscription is handled, it will not be handled again. @@ -50,12 +53,14 @@ def handle_subscription(config: configuration.NamespaceConfig, acc: Account) -> :param Account acc: Current client account. """ - if config.dry_run: + if config.dry_run or not acc: return if acc.meta.register_to_eff: - subscribe(acc.meta.register_to_eff) + # TODO: Remove cast when https://github.com/certbot/certbot/pull/9073 is merged. + subscribe(cast(str, acc.meta.register_to_eff)) - acc.meta = acc.meta.update(register_to_eff=None) + # TODO: Remove cast when https://github.com/certbot/certbot/pull/9073 is merged. + acc.meta = cast(Account.Meta, acc.meta.update(register_to_eff=None)) storage = AccountFileStorage(config) storage.update_meta(acc) diff --git a/certbot/certbot/_internal/error_handler.py b/certbot/certbot/_internal/error_handler.py index 64aad155e..0e63d02de 100644 --- a/certbot/certbot/_internal/error_handler.py +++ b/certbot/certbot/_internal/error_handler.py @@ -3,10 +3,13 @@ import functools import logging import signal import traceback +from types import TracebackType from typing import Any from typing import Callable from typing import Dict from typing import List +from typing import Optional +from typing import Type from typing import Union from certbot import errors @@ -74,7 +77,7 @@ class ErrorHandler: deferred until they finish. """ - def __init__(self, func, *args, **kwargs): + def __init__(self, func: Callable[..., Any], *args: Any, **kwargs: Any) -> None: self.call_on_regular_exit = False self.body_executed = False self.funcs: List[Callable[[], Any]] = [] @@ -83,11 +86,13 @@ class ErrorHandler: if func is not None: self.register(func, *args, **kwargs) - def __enter__(self): + def __enter__(self) -> None: self.body_executed = False self._set_signal_handlers() - def __exit__(self, exec_type, exec_value, trace): + def __exit__(self, exec_type: Optional[Type[BaseException]], + exec_value: Optional[BaseException], + trace: Optional[TracebackType]) -> bool: self.body_executed = True retval = False # SystemExit is ignored to properly handle forks that don't exec @@ -108,7 +113,7 @@ class ErrorHandler: self._call_signals() return retval - def register(self, func: Callable, *args: Any, **kwargs: Any) -> None: + def register(self, func: Callable[..., Any], *args: Any, **kwargs: Any) -> None: """Sets func to be run with the given arguments during cleanup. :param function func: function to be called in case of an error @@ -116,7 +121,7 @@ class ErrorHandler: """ self.funcs.append(functools.partial(func, *args, **kwargs)) - def _call_registered(self): + def _call_registered(self) -> None: """Calls all registered functions""" logger.debug("Calling registered functions") while self.funcs: @@ -128,7 +133,7 @@ class ErrorHandler: ''.join(output).rstrip()) self.funcs.pop() - def _set_signal_handlers(self): + def _set_signal_handlers(self) -> None: """Sets signal handlers for signals in _SIGNALS.""" for signum in _SIGNALS: prev_handler = signal.getsignal(signum) @@ -137,13 +142,13 @@ class ErrorHandler: self.prev_handlers[signum] = prev_handler signal.signal(signum, self._signal_handler) - def _reset_signal_handlers(self): + def _reset_signal_handlers(self) -> None: """Resets signal handlers for signals in _SIGNALS.""" for signum, handler in self.prev_handlers.items(): signal.signal(signum, handler) self.prev_handlers.clear() - def _signal_handler(self, signum, unused_frame): + def _signal_handler(self, signum: int, unused_frame: Any) -> None: """Replacement function for handling received signals. Store the received signal. If we are executing the code block in @@ -156,7 +161,7 @@ class ErrorHandler: if not self.body_executed: raise errors.SignalExit - def _call_signals(self): + def _call_signals(self) -> None: """Finally call the deferred signals.""" for signum in self.received_signals: logger.debug("Calling signal %s", signum) @@ -169,6 +174,6 @@ class ExitHandler(ErrorHandler): In addition to cleaning up on all signals, also cleans up on regular exit. """ - def __init__(self, func, *args, **kwargs): + def __init__(self, func: Callable[..., Any], *args: Any, **kwargs: Any) -> None: ErrorHandler.__init__(self, func, *args, **kwargs) self.call_on_regular_exit = True diff --git a/certbot/certbot/_internal/hooks.py b/certbot/certbot/_internal/hooks.py index e4975f1c8..813f7f6bd 100644 --- a/certbot/certbot/_internal/hooks.py +++ b/certbot/certbot/_internal/hooks.py @@ -2,8 +2,10 @@ import logging from typing import List +from typing import Optional from typing import Set +from certbot import configuration from certbot import errors from certbot import util from certbot.compat import filesystem @@ -15,7 +17,7 @@ from certbot.plugins import util as plug_util logger = logging.getLogger(__name__) -def validate_hooks(config): +def validate_hooks(config: configuration.NamespaceConfig) -> None: """Check hook commands are executable.""" validate_hook(config.pre_hook, "pre") validate_hook(config.post_hook, "post") @@ -23,7 +25,7 @@ def validate_hooks(config): validate_hook(config.renew_hook, "renew") -def _prog(shell_cmd): +def _prog(shell_cmd: str) -> Optional[str]: """Extract the program run by a shell command. :param str shell_cmd: command to be executed @@ -36,10 +38,11 @@ def _prog(shell_cmd): plug_util.path_surgery(shell_cmd) if not util.exe_exists(shell_cmd): return None + return os.path.basename(shell_cmd) -def validate_hook(shell_cmd, hook_name): +def validate_hook(shell_cmd: str, hook_name: str) -> None: """Check that a command provided as a hook is plausibly executable. :raises .errors.HookCommandNotFound: if the command is not found @@ -57,7 +60,7 @@ def validate_hook(shell_cmd, hook_name): raise errors.HookCommandNotFound(msg) -def pre_hook(config): +def pre_hook(config: configuration.NamespaceConfig) -> None: """Run pre-hooks if they exist and haven't already been run. When Certbot is running with the renew subcommand, this function @@ -81,7 +84,7 @@ def pre_hook(config): executed_pre_hooks: Set[str] = set() -def _run_pre_hook_if_necessary(command): +def _run_pre_hook_if_necessary(command: str) -> None: """Run the specified pre-hook if we haven't already. If we've already run this exact command before, a message is logged @@ -97,7 +100,7 @@ def _run_pre_hook_if_necessary(command): executed_pre_hooks.add(command) -def post_hook(config): +def post_hook(config: configuration.NamespaceConfig) -> None: """Run post-hooks if defined. This function also registers any executables found in @@ -131,7 +134,7 @@ def post_hook(config): post_hooks: List[str] = [] -def _run_eventually(command): +def _run_eventually(command: str) -> None: """Registers a post-hook to be run eventually. All commands given to this function will be run exactly once in the @@ -144,13 +147,14 @@ def _run_eventually(command): post_hooks.append(command) -def run_saved_post_hooks(): +def run_saved_post_hooks() -> None: """Run any post hooks that were saved up in the course of the 'renew' verb""" for cmd in post_hooks: _run_hook("post-hook", cmd) -def deploy_hook(config, domains, lineage_path): +def deploy_hook(config: configuration.NamespaceConfig, domains: List[str], + lineage_path: str) -> None: """Run post-issuance hook if defined. :param configuration.NamespaceConfig config: Certbot settings @@ -164,7 +168,8 @@ def deploy_hook(config, domains, lineage_path): lineage_path, config.dry_run) -def renew_hook(config, domains, lineage_path): +def renew_hook(config: configuration.NamespaceConfig, domains: List[str], + lineage_path: str) -> None: """Run post-renewal hooks. This function runs any hooks found in @@ -196,7 +201,7 @@ def renew_hook(config, domains, lineage_path): lineage_path, config.dry_run) -def _run_deploy_hook(command, domains, lineage_path, dry_run): +def _run_deploy_hook(command: str, domains: List[str], lineage_path: str, dry_run: bool) -> None: """Run the specified deploy-hook (if not doing a dry run). If dry_run is True, command is not run and a message is logged @@ -220,7 +225,7 @@ def _run_deploy_hook(command, domains, lineage_path, dry_run): _run_hook("deploy-hook", command) -def _run_hook(cmd_name, shell_cmd): +def _run_hook(cmd_name: str, shell_cmd: str) -> str: """Run a hook command. :param str cmd_name: the user facing name of the hook being run @@ -234,7 +239,7 @@ def _run_hook(cmd_name, shell_cmd): return err -def list_hooks(dir_path): +def list_hooks(dir_path: str) -> List[str]: """List paths to all hooks found in dir_path in sorted order. :param str dir_path: directory to search diff --git a/certbot/certbot/_internal/lock.py b/certbot/certbot/_internal/lock.py index 80b79d993..95c44856f 100644 --- a/certbot/certbot/_internal/lock.py +++ b/certbot/certbot/_internal/lock.py @@ -89,10 +89,10 @@ class _BaseLockMechanism: """ return self._fd is not None - def acquire(self): # pylint: disable=missing-function-docstring + def acquire(self) -> None: # pylint: disable=missing-function-docstring pass # pragma: no cover - def release(self): # pylint: disable=missing-function-docstring + def release(self) -> None: # pylint: disable=missing-function-docstring pass # pragma: no cover @@ -143,7 +143,8 @@ class _UnixLockMechanism(_BaseLockMechanism): # Normally os module should not be imported in certbot codebase except in certbot.compat # for the sake of compatibility over Windows and Linux. # We make an exception here, since _lock_success is private and called only on Linux. - from os import stat, fstat # pylint: disable=os-module-forbidden + from os import fstat # pylint: disable=os-module-forbidden + from os import stat # pylint: disable=os-module-forbidden try: stat1 = stat(self._path) except OSError as err: @@ -196,7 +197,7 @@ class _WindowsLockMechanism(_BaseLockMechanism): Consequently, mscvrt.locking is sufficient to obtain an effective lock, and the race condition encountered on Linux is not possible on Windows, leading to a simpler workflow. """ - def acquire(self): + def acquire(self) -> None: """Acquire the lock""" open_mode = os.O_RDWR | os.O_CREAT | os.O_TRUNC @@ -220,7 +221,7 @@ class _WindowsLockMechanism(_BaseLockMechanism): self._fd = fd - def release(self): + def release(self) -> None: """Release the lock.""" try: if not self._fd: diff --git a/certbot/certbot/_internal/log.py b/certbot/certbot/_internal/log.py index 12b144d54..e168158d9 100644 --- a/certbot/certbot/_internal/log.py +++ b/certbot/certbot/_internal/log.py @@ -29,9 +29,14 @@ import sys import tempfile import traceback from types import TracebackType +from typing import Any from typing import IO +from typing import Optional +from typing import Tuple +from typing import Type from acme import messages +from certbot import configuration from certbot import errors from certbot import util from certbot._internal import constants @@ -45,7 +50,7 @@ FILE_FMT = "%(asctime)s:%(levelname)s:%(name)s:%(message)s" logger = logging.getLogger(__name__) -def pre_arg_parse_setup(): +def pre_arg_parse_setup() -> None: """Setup logging before command line arguments are parsed. Terminal logging is setup using @@ -85,7 +90,7 @@ def pre_arg_parse_setup(): log_path=temp_handler.path) -def post_arg_parse_setup(config): +def post_arg_parse_setup(config: configuration.NamespaceConfig) -> None: """Setup logging after command line arguments are parsed. This function assumes `pre_arg_parse_setup` was called earlier and @@ -137,7 +142,8 @@ def post_arg_parse_setup(config): debug=config.debug, quiet=config.quiet, log_path=file_path) -def setup_log_file_handler(config, logfile, fmt): +def setup_log_file_handler(config: configuration.NamespaceConfig, logfile: str, + fmt: str) -> Tuple[logging.Handler, str]: """Setup file debug logging. :param certbot.configuration.NamespaceConfig config: Configuration object @@ -179,13 +185,13 @@ class ColoredStreamHandler(logging.StreamHandler): :ivar bool red_level: The level at which to output """ - def __init__(self, stream=None): + def __init__(self, stream: Optional[IO] = None) -> None: super().__init__(stream) self.colored = (sys.stderr.isatty() if stream is None else stream.isatty()) self.red_level = logging.WARNING - def format(self, record): + def format(self, record: logging.LogRecord) -> str: """Formats the string representation of record. :param logging.LogRecord record: Record to be formatted @@ -207,11 +213,12 @@ class MemoryHandler(logging.handlers.MemoryHandler): only happens when flush(force=True) is called. """ - def __init__(self, target=None, capacity=10000): + def __init__(self, target: Optional[logging.Handler] = None, + capacity: int = 10000) -> None: # capacity doesn't matter because should_flush() is overridden super().__init__(capacity, target=target) - def close(self): + def close(self) -> None: """Close the memory handler, but don't set the target to None.""" # This allows the logging module which may only have a weak # reference to the target handler to properly flush and close it. @@ -219,7 +226,7 @@ class MemoryHandler(logging.handlers.MemoryHandler): super().close() self.target = target - def flush(self, force=False): # pylint: disable=arguments-differ + def flush(self, force: bool = False) -> None: # pylint: disable=arguments-differ """Flush the buffer if force=True. If force=False, this call is a noop. @@ -232,7 +239,7 @@ class MemoryHandler(logging.handlers.MemoryHandler): if force: super().flush() - def shouldFlush(self, record): + def shouldFlush(self, record: logging.LogRecord) -> bool: """Should the buffer be automatically flushed? :param logging.LogRecord record: log record to be considered @@ -254,7 +261,7 @@ class TempHandler(logging.StreamHandler): :ivar str path: file system path to the temporary log file """ - def __init__(self): + def __init__(self) -> None: self._workdir = tempfile.mkdtemp() self.path = os.path.join(self._workdir, 'log') stream = util.safe_open(self.path, mode='w', chmod=0o600) @@ -264,7 +271,7 @@ class TempHandler(logging.StreamHandler): self.stream: IO[str] self._delete = True - def emit(self, record): + def emit(self, record: logging.LogRecord) -> None: """Log the specified logging record. :param logging.LogRecord record: Record to be formatted @@ -273,7 +280,7 @@ class TempHandler(logging.StreamHandler): self._delete = False super().emit(record) - def close(self): + def close(self) -> None: """Close the handler and the temporary log file. The temporary log file is deleted if it wasn't used. @@ -292,7 +299,8 @@ class TempHandler(logging.StreamHandler): self.release() -def pre_arg_parse_except_hook(memory_handler, *args, **kwargs): +def pre_arg_parse_except_hook(memory_handler: MemoryHandler, + *args: Any, **kwargs: Any) -> None: """A simple wrapper around post_arg_parse_except_hook. The additional functionality provided by this wrapper is the memory @@ -319,8 +327,9 @@ def pre_arg_parse_except_hook(memory_handler, *args, **kwargs): memory_handler.flush(force=True) -def post_arg_parse_except_hook(exc_type: type, exc_value: BaseException, trace: TracebackType, - debug: bool, quiet: bool, log_path: str): +def post_arg_parse_except_hook(exc_type: Type[BaseException], exc_value: BaseException, + trace: TracebackType, debug: bool, quiet: bool, + log_path: str) -> None: """Logs fatal exceptions and reports them to the user. If debug is True, the full exception and traceback is shown to the @@ -369,7 +378,7 @@ def post_arg_parse_except_hook(exc_type: type, exc_value: BaseException, trace: exit_func() -def exit_with_advice(log_path: str): +def exit_with_advice(log_path: str) -> None: """Print a link to the community forums, the debug log path, and exit The message is printed to stderr and the program will exit with a diff --git a/certbot/certbot/_internal/main.py b/certbot/certbot/_internal/main.py index 3301c72c8..9c29b9560 100644 --- a/certbot/certbot/_internal/main.py +++ b/certbot/certbot/_internal/main.py @@ -11,6 +11,7 @@ from typing import Iterable from typing import List from typing import Optional from typing import Tuple +from typing import TypeVar from typing import Union import configobj @@ -18,6 +19,7 @@ import josepy as jose import zope.component import zope.interface +from acme import client as acme_client from acme import errors as acme_errors import certbot from certbot import configuration @@ -56,7 +58,7 @@ USER_CANCELLED = ("User chose to cancel the operation and may " logger = logging.getLogger(__name__) -def _suggest_donation_if_appropriate(config): +def _suggest_donation_if_appropriate(config: configuration.NamespaceConfig) -> None: """Potentially suggest a donation to support Certbot. :param config: Configuration object @@ -82,7 +84,10 @@ def _suggest_donation_if_appropriate(config): ) -def _get_and_save_cert(le_client, config, domains=None, certname=None, lineage=None): +def _get_and_save_cert(le_client: client.Client, config: configuration.NamespaceConfig, + domains: Optional[List[str]] = None, certname: Optional[str] = None, + lineage: Optional[storage.RenewableCert] = None + ) -> Optional[storage.RenewableCert]: """Authenticate and enroll certificate. This method finds the relevant lineage, figures out what to do with it, @@ -115,14 +120,15 @@ def _get_and_save_cert(le_client, config, domains=None, certname=None, lineage=N display_util.notify( "{action} for {domains}".format( action="Simulating renewal of an existing certificate" - if config.dry_run else "Renewing an existing certificate", + if config.dry_run else "Renewing an existing certificate", domains=internal_display_util.summarize_domain_list(domains or lineage.names()) ) ) renewal.renew_cert(config, domains, le_client, lineage) else: # TREAT AS NEW REQUEST - assert domains is not None + if domains is None: + raise errors.Error("Domain list cannot be none if the lineage is not set.") display_util.notify( "{action} for {domains}".format( action="Simulating a certificate request" if config.dry_run else @@ -164,7 +170,7 @@ def _handle_unexpected_key_type_migration(config: configuration.NamespaceConfig, def _handle_subset_cert_request(config: configuration.NamespaceConfig, - domains: List[str], + domains: Iterable[str], cert: storage.RenewableCert ) -> Tuple[str, Optional[storage.RenewableCert]]: """Figure out what to do if a previous cert had a subset of the names now requested @@ -264,7 +270,8 @@ def _handle_identical_cert_request(config: configuration.NamespaceConfig, raise AssertionError('This is impossible') -def _find_lineage_for_domains(config, domains): +def _find_lineage_for_domains(config: configuration.NamespaceConfig, domains: List[str] + ) -> Tuple[Optional[str], Optional[storage.RenewableCert]]: """Determine whether there are duplicated names and how to handle them (renew, reinstall, newcert, or raising an error to stop the client run if the user chooses to cancel the operation when @@ -304,7 +311,8 @@ def _find_lineage_for_domains(config, domains): return None, None -def _find_cert(config, domains, certname): +def _find_cert(config: configuration.NamespaceConfig, domains: List[str], certname: str + ) -> Tuple[bool, Optional[storage.RenewableCert]]: """Finds an existing certificate object given domains and/or a certificate name. :param config: Configuration object @@ -328,10 +336,9 @@ def _find_cert(config, domains, certname): return (action != "reinstall"), lineage -def _find_lineage_for_domains_and_certname(config: configuration.NamespaceConfig, - domains: List[str], - certname: str - ) -> Tuple[str, Optional[storage.RenewableCert]]: +def _find_lineage_for_domains_and_certname( + config: configuration.NamespaceConfig, domains: List[str], + certname: str) -> Tuple[Optional[str], Optional[storage.RenewableCert]]: """Find appropriate lineage based on given domains and/or certname. :param config: Configuration object @@ -357,7 +364,8 @@ def _find_lineage_for_domains_and_certname(config: configuration.NamespaceConfig lineage = cert_manager.lineage_for_certname(config, certname) if lineage: if domains: - if set(cert_manager.domains_for_certname(config, certname)) != set(domains): + computed_domains = cert_manager.domains_for_certname(config, certname) + if computed_domains and set(computed_domains) != set(domains): _handle_unexpected_key_type_migration(config, lineage) _ask_user_to_confirm_new_names(config, domains, certname, lineage.names()) # raises if no @@ -367,10 +375,14 @@ def _find_lineage_for_domains_and_certname(config: configuration.NamespaceConfig elif domains: return "newcert", None raise errors.ConfigurationError("No certificate with name {0} found. " - "Use -d to specify domains, or run certbot certificates to see " - "possible certificate names.".format(certname)) + "Use -d to specify domains, or run certbot certificates to see " + "possible certificate names.".format(certname)) -def _get_added_removed(after, before): + +T = TypeVar("T") + + +def _get_added_removed(after: Iterable[T], before: Iterable[T]) -> Tuple[List[T], List[T]]: """Get lists of items removed from `before` and a lists of items added to `after` """ @@ -380,7 +392,8 @@ def _get_added_removed(after, before): removed.sort() return added, removed -def _format_list(character, strings): + +def _format_list(character: str, strings: Iterable[str]) -> str: """Format list with given character """ if not strings: @@ -392,7 +405,10 @@ def _format_list(character, strings): br=os.linesep ) -def _ask_user_to_confirm_new_names(config, new_domains, certname, old_domains): + +def _ask_user_to_confirm_new_names(config: configuration.NamespaceConfig, + new_domains: Iterable[str], certname: str, + old_domains: Iterable[str]) -> None: """Ask user to confirm update cert certname to contain new_domains. :param config: Configuration object @@ -429,7 +445,9 @@ def _ask_user_to_confirm_new_names(config, new_domains, certname, old_domains): raise errors.ConfigurationError("Specified mismatched certificate name and domains.") -def _find_domains_or_certname(config, installer, question=None): +def _find_domains_or_certname(config: configuration.NamespaceConfig, + installer: Optional[interfaces.Installer], + question: Optional[str] = None) -> Tuple[List[str], str]: """Retrieve domains and certname from config or user input. :param config: Configuration object @@ -596,7 +614,7 @@ def _is_interactive_only_auth(config: configuration.NamespaceConfig) -> bool: def _csr_report_new_cert(config: configuration.NamespaceConfig, cert_path: Optional[str], - chain_path: Optional[str], fullchain_path: Optional[str]): + chain_path: Optional[str], fullchain_path: Optional[str]) -> None: """ --csr variant of _report_new_cert. Until --csr is overhauled (#8332) this is transitional function to report the creation @@ -633,7 +651,9 @@ def _csr_report_new_cert(config: configuration.NamespaceConfig, cert_path: Optio ) -def _determine_account(config): +def _determine_account(config: configuration.NamespaceConfig + ) -> Tuple[account.Account, + Optional[acme_client.ClientV2]]: """Determine which account to use. If ``config.account`` is ``None``, it will be updated based on the @@ -649,9 +669,9 @@ def _determine_account(config): :raises errors.Error: If unable to register an account with ACME server """ - def _tos_cb(terms_of_service): + def _tos_cb(terms_of_service: str) -> None: if config.tos: - return True + return 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)) @@ -660,17 +680,19 @@ def _determine_account(config): raise errors.Error( "Registration cannot proceed without accepting " "Terms of Service.") - return None account_storage = account.AccountFileStorage(config) - acme = None + acme: Optional[acme_client.ClientV2] = None if config.account is not None: acc = account_storage.load(config.account) else: accounts = account_storage.find_all() if len(accounts) > 1: - acc = display_ops.choose_account(accounts) + potential_acc = display_ops.choose_account(accounts) + if not potential_acc: + raise errors.Error("No account has been chosen.") + acc = potential_acc elif len(accounts) == 1: acc = accounts[0] else: # no account registered yet @@ -691,7 +713,7 @@ def _determine_account(config): return acc, acme -def _delete_if_appropriate(config): +def _delete_if_appropriate(config: configuration.NamespaceConfig) -> None: """Does the user want to delete their now-revoked certs? If run in non-interactive mode, deleting happens automatically. @@ -729,21 +751,23 @@ def _delete_if_appropriate(config): config, config.certname) try: cert_manager.match_and_check_overlaps(config, [lambda x: archive_dir], - lambda x: x.archive_dir, lambda x: x) + lambda x: x.archive_dir, lambda x: x.lineagename) except errors.OverlappingMatchFound: logger.warning("Not deleting revoked certificates due to overlapping archive dirs. " "More than one certificate is using %s", archive_dir) return except Exception as e: msg = ('config.default_archive_dir: {0}, config.live_dir: {1}, archive_dir: {2},' - 'original exception: {3}') + 'original exception: {3}') msg = msg.format(config.default_archive_dir, config.live_dir, archive_dir, e) raise errors.Error(msg) cert_manager.delete(config) -def _init_le_client(config, authenticator, installer): +def _init_le_client(config: configuration.NamespaceConfig, + authenticator: Optional[interfaces.Authenticator], + installer: Optional[interfaces.Installer]) -> client.Client: """Initialize Let's Encrypt Client :param config: Configuration object @@ -758,19 +782,19 @@ def _init_le_client(config, authenticator, installer): :rtype: client.Client """ + acc: Optional[account.Account] if authenticator is not None: # if authenticator was given, then we will need account... acc, acme = _determine_account(config) logger.debug("Picked account: %r", acc) - # XXX - #crypto_util.validate_key_csr(acc.key) else: acc, acme = None, None return client.Client(config, acc, authenticator, installer, acme=acme) -def unregister(config, unused_plugins): +def unregister(config: configuration.NamespaceConfig, + unused_plugins: plugins_disco.PluginsRegistry) -> Optional[str]: """Deactivate account on server :param config: Configuration object @@ -779,8 +803,8 @@ def unregister(config, unused_plugins): :param unused_plugins: List of plugins (deprecated) :type unused_plugins: plugins_disco.PluginsRegistry - :returns: `None` - :rtype: None + :returns: `None` or a string indicating an error + :rtype: None or str """ account_storage = account.AccountFileStorage(config) @@ -799,6 +823,9 @@ def unregister(config, unused_plugins): acc, acme = _determine_account(config) cb_client = client.Client(config, acc, None, None, acme=acme) + if not cb_client.acme: + raise errors.Error("ACME client is not set.") + # delete on boulder cb_client.acme.deactivate_registration(acc.regr) account_files = account.AccountFileStorage(config) @@ -809,7 +836,8 @@ def unregister(config, unused_plugins): return None -def register(config, unused_plugins): +def register(config: configuration.NamespaceConfig, + unused_plugins: plugins_disco.PluginsRegistry) -> Optional[str]: """Create accounts on the server. :param config: Configuration object @@ -839,7 +867,8 @@ def register(config, unused_plugins): return None -def update_account(config, unused_plugins): +def update_account(config: configuration.NamespaceConfig, + unused_plugins: plugins_disco.PluginsRegistry) -> Optional[str]: """Modify accounts on the server. :param config: Configuration object @@ -864,8 +893,11 @@ def update_account(config, unused_plugins): acc, acme = _determine_account(config) cb_client = client.Client(config, acc, None, None, acme=acme) - # Empty list of contacts in case the user is removing all emails + if not cb_client.acme: + raise errors.Error("ACME client is not set.") + + # Empty list of contacts in case the user is removing all emails acc_contacts: Iterable[str] = () if config.email: acc_contacts = ['mailto:' + email for email in config.email.split(',')] @@ -904,7 +936,8 @@ def _cert_name_from_config_or_lineage(config: configuration.NamespaceConfig, return None -def _install_cert(config, le_client, domains, lineage=None): +def _install_cert(config: configuration.NamespaceConfig, le_client: client.Client, + domains: List[str], lineage: Optional[storage.RenewableCert] = None) -> None: """Install a cert :param config: Configuration object @@ -923,7 +956,8 @@ def _install_cert(config, le_client, domains, lineage=None): :rtype: None """ - path_provider = lineage if lineage else config + path_provider: Union[storage.RenewableCert, + configuration.NamespaceConfig] = lineage if lineage else config assert path_provider.cert_path is not None le_client.deploy_certificate(domains, path_provider.key_path, path_provider.cert_path, @@ -931,7 +965,8 @@ def _install_cert(config, le_client, domains, lineage=None): le_client.enhance_config(domains, path_provider.chain_path) -def install(config, plugins): +def install(config: configuration.NamespaceConfig, + plugins: plugins_disco.PluginsRegistry) -> Optional[str]: """Install a previously obtained cert in a server. :param config: Configuration object @@ -940,8 +975,8 @@ def install(config, plugins): :param plugins: List of plugins :type plugins: plugins_disco.PluginsRegistry - :returns: `None` - :rtype: None + :returns: `None` or the error message + :rtype: None or str """ # XXX: Update for renewer/RenewableCert @@ -990,7 +1025,8 @@ def install(config, plugins): return None -def _populate_from_certname(config): + +def _populate_from_certname(config: configuration.NamespaceConfig) -> configuration.NamespaceConfig: """Helper function for install to populate missing config values from lineage defined by --cert-name.""" @@ -1007,14 +1043,18 @@ def _populate_from_certname(config): config.namespace.fullchain_path = lineage.fullchain_path return config -def _check_certificate_and_key(config): + +def _check_certificate_and_key(config: configuration.NamespaceConfig) -> None: if not os.path.isfile(filesystem.realpath(config.cert_path)): raise errors.ConfigurationError("Error while reading certificate from path " "{0}".format(config.cert_path)) if not os.path.isfile(filesystem.realpath(config.key_path)): raise errors.ConfigurationError("Error while reading private key from path " "{0}".format(config.key_path)) -def plugins_cmd(config, plugins): + + +def plugins_cmd(config: configuration.NamespaceConfig, + plugins: plugins_disco.PluginsRegistry) -> None: """List server software plugins. :param config: Configuration object @@ -1052,7 +1092,8 @@ def plugins_cmd(config, plugins): notify(str(available)) -def enhance(config, plugins): +def enhance(config: configuration.NamespaceConfig, + plugins: plugins_disco.PluginsRegistry) -> Optional[str]: """Add security enhancements to existing configuration :param config: Configuration object @@ -1061,8 +1102,8 @@ def enhance(config, plugins): :param plugins: List of plugins :type plugins: plugins_disco.PluginsRegistry - :returns: `None` - :rtype: None + :returns: `None` or a string indicating an error + :rtype: None or str """ supported_enhancements = ["hsts", "redirect", "uir", "staple"] @@ -1089,6 +1130,8 @@ def enhance(config, plugins): config, "enhance", allow_multiple=False, custom_prompt=certname_question)[0] cert_domains = cert_manager.domains_for_certname(config, config.certname) + if cert_domains is None: + raise errors.Error("Could not find the list of domains for the given certificate name.") if config.noninteractive_mode: domains = cert_domains else: @@ -1100,6 +1143,8 @@ def enhance(config, plugins): "defined, exiting.") lineage = cert_manager.lineage_for_certname(config, config.certname) + if not lineage: + raise errors.Error("Could not find the lineage for the given certificate name.") if not config.chain_path: config.chain_path = lineage.chain_path if oldstyle_enh: @@ -1111,7 +1156,7 @@ def enhance(config, plugins): return None -def rollback(config, plugins): +def rollback(config: configuration.NamespaceConfig, plugins: plugins_disco.PluginsRegistry) -> None: """Rollback server configuration changes made during install. :param config: Configuration object @@ -1126,7 +1171,9 @@ def rollback(config, plugins): """ client.rollback(config.installer, config.checkpoints, config, plugins) -def update_symlinks(config, unused_plugins): + +def update_symlinks(config: configuration.NamespaceConfig, + unused_plugins: plugins_disco.PluginsRegistry) -> None: """Update the certificate file family symlinks Use the information in the config file to make symlinks point to @@ -1144,7 +1191,9 @@ def update_symlinks(config, unused_plugins): """ cert_manager.update_live_symlinks(config) -def rename(config, unused_plugins): + +def rename(config: configuration.NamespaceConfig, + unused_plugins: plugins_disco.PluginsRegistry) -> None: """Rename a certificate Use the information in the config file to rename an existing @@ -1162,7 +1211,9 @@ def rename(config, unused_plugins): """ cert_manager.rename_lineage(config) -def delete(config, unused_plugins): + +def delete(config: configuration.NamespaceConfig, + unused_plugins: plugins_disco.PluginsRegistry) -> None: """Delete a certificate Use the information in the config file to delete an existing @@ -1181,7 +1232,8 @@ def delete(config, unused_plugins): cert_manager.delete(config) -def certificates(config, unused_plugins): +def certificates(config: configuration.NamespaceConfig, + unused_plugins: plugins_disco.PluginsRegistry) -> None: """Display information about certs configured with Certbot :param config: Configuration object @@ -1197,7 +1249,8 @@ def certificates(config, unused_plugins): cert_manager.certificates(config) -def revoke(config, unused_plugins: plugins_disco.PluginsRegistry) -> Optional[str]: +def revoke(config: configuration.NamespaceConfig, + unused_plugins: plugins_disco.PluginsRegistry) -> Optional[str]: """Revoke a previously obtained certificate. :param config: Configuration object @@ -1251,7 +1304,8 @@ def revoke(config, unused_plugins: plugins_disco.PluginsRegistry) -> Optional[st return None -def run(config, plugins): +def run(config: configuration.NamespaceConfig, + plugins: plugins_disco.PluginsRegistry) -> Optional[str]: """Obtain a certificate and install. :param config: Configuration object @@ -1361,7 +1415,8 @@ def _csr_get_and_save_cert(config: configuration.NamespaceConfig, return cert_path, chain_path, fullchain_path -def renew_cert(config, plugins, lineage): +def renew_cert(config: configuration.NamespaceConfig, plugins: plugins_disco.PluginsRegistry, + lineage: storage.RenewableCert) -> None: """Renew & save an existing cert. Do not install it. :param config: Configuration object @@ -1385,6 +1440,9 @@ def renew_cert(config, plugins, lineage): renewed_lineage = _get_and_save_cert(le_client, config, lineage=lineage) + if not renewed_lineage: + raise errors.Error("An existing certificate for the given name could not be found.") + if installer and not config.dry_run: # In case of a renewal, reload server to pick up new certificate. updater.run_renewal_deployer(config, renewed_lineage, installer) @@ -1392,7 +1450,7 @@ def renew_cert(config, plugins, lineage): installer.restart() -def certonly(config, plugins): +def certonly(config: configuration.NamespaceConfig, plugins: plugins_disco.PluginsRegistry) -> None: """Authenticate & obtain cert, but do not install it. This implements the 'certonly' subcommand. @@ -1412,7 +1470,6 @@ def certonly(config, plugins): # SETUP: Select plugins and construct a client instance # installers are used in auth mode to determine domain names installer, auth = plug_sel.choose_configurator_plugins(config, plugins, "certonly") - le_client = _init_le_client(config, auth, installer) if config.csr: @@ -1443,7 +1500,8 @@ def certonly(config, plugins): eff.handle_subscription(config, le_client.account) -def renew(config, unused_plugins): +def renew(config: configuration.NamespaceConfig, + unused_plugins: plugins_disco.PluginsRegistry) -> None: """Renew previously-obtained certificates. :param config: Configuration object @@ -1462,7 +1520,7 @@ def renew(config, unused_plugins): hooks.run_saved_post_hooks() -def make_or_verify_needed_dirs(config): +def make_or_verify_needed_dirs(config: configuration.NamespaceConfig) -> None: """Create or verify existence of config, work, and hook directories. :param config: Configuration object @@ -1514,7 +1572,7 @@ def make_displayer(config: configuration.NamespaceConfig devnull.close() -def main(cli_args=None): +def main(cli_args: List[str] = None) -> Optional[Union[str, int]]: """Run Certbot. :param cli_args: command line to Certbot, defaults to ``sys.argv[1:]`` diff --git a/certbot/certbot/_internal/plugins/disco.py b/certbot/certbot/_internal/plugins/disco.py index 1a8cf22c7..30409aff0 100644 --- a/certbot/certbot/_internal/plugins/disco.py +++ b/certbot/certbot/_internal/plugins/disco.py @@ -1,9 +1,14 @@ """Utilities for plugins discovery and selection.""" -from collections.abc import Mapping import itertools import logging import sys +from typing import Callable +from typing import cast from typing import Dict +from typing import Iterable +from typing import Iterator +from typing import List +from typing import Mapping from typing import Optional from typing import Type from typing import Union @@ -13,6 +18,7 @@ import pkg_resources import zope.interface import zope.interface.verify +from certbot import configuration from certbot import errors from certbot import interfaces from certbot._internal import constants @@ -49,7 +55,7 @@ class PluginEntryPoint: # this object is mutable, don't allow it to be hashed! __hash__ = None # type: ignore - def __init__(self, entry_point: pkg_resources.EntryPoint, with_prefix=False): + def __init__(self, entry_point: pkg_resources.EntryPoint, with_prefix: bool = False) -> None: self.name = self.entry_point_to_plugin_name(entry_point, with_prefix) self.plugin_cls: Type[interfaces.Plugin] = entry_point.load() self.entry_point = entry_point @@ -59,7 +65,7 @@ class PluginEntryPoint: self._hidden = False self._long_description: Optional[str] = None - def check_name(self, name): + def check_name(self, name: Optional[str]) -> bool: """Check if the name refers to this plugin.""" if name == self.name: if self.warning_message: @@ -68,43 +74,46 @@ class PluginEntryPoint: return False @classmethod - def entry_point_to_plugin_name(cls, entry_point, with_prefix): + def entry_point_to_plugin_name(cls, entry_point: pkg_resources.EntryPoint, + with_prefix: bool) -> str: """Unique plugin name for an ``entry_point``""" if with_prefix: + if not entry_point.dist: + raise errors.Error(f"Entrypoint {entry_point.name} has no distribution!") return entry_point.dist.key + ":" + entry_point.name return entry_point.name @property - def description(self): + def description(self) -> str: """Description of the plugin.""" return self.plugin_cls.description @property - def description_with_name(self): + def description_with_name(self) -> str: """Description with name. Handy for UI.""" return "{0} ({1})".format(self.description, self.name) @property - def long_description(self): + def long_description(self) -> str: """Long description of the plugin.""" if self._long_description: return self._long_description return getattr(self.plugin_cls, "long_description", self.description) @long_description.setter - def long_description(self, description): + def long_description(self, description: str) -> None: self._long_description = description @property - def hidden(self): + def hidden(self) -> bool: """Should this plugin be hidden from UI?""" return self._hidden or getattr(self.plugin_cls, "hidden", False) @hidden.setter - def hidden(self, hide): + def hidden(self, hide: bool) -> None: self._hidden = hide - def ifaces(self, *ifaces_groups): + def ifaces(self, *ifaces_groups: Iterable[Type]) -> bool: """Does plugin implements specified interface groups?""" return not ifaces_groups or any( all(_implements(self.plugin_cls, iface) @@ -112,20 +121,20 @@ class PluginEntryPoint: for ifaces in ifaces_groups) @property - def initialized(self): + def initialized(self) -> bool: """Has the plugin been initialized already?""" return self._initialized is not None - def init(self, config=None): + def init(self, config: Optional[configuration.NamespaceConfig] = None) -> interfaces.Plugin: """Memoized plugin initialization.""" - if not self.initialized: + if not self._initialized: self.entry_point.require() # fetch extras! # 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): + def verify(self, ifaces: Iterable[Type]) -> bool: """Verify that the plugin conforms to the specified interfaces.""" if not self.initialized: raise ValueError("Plugin is not initialized.") @@ -136,13 +145,13 @@ class PluginEntryPoint: return True @property - def prepared(self): + def prepared(self) -> bool: """Has the plugin been prepared already?""" if not self.initialized: logger.debug(".prepared called on uninitialized %r", self) return self._prepared is not None - def prepare(self): + def prepare(self) -> Union[bool, Error]: """Memoized plugin preparation.""" if self._initialized is None: raise ValueError("Plugin is not initialized.") @@ -161,29 +170,30 @@ class PluginEntryPoint: self._prepared = error else: self._prepared = True - return self._prepared + # Mypy seems to fail to understand the actual type here, let's help it. + return cast(Union[bool, Error], self._prepared) @property - def misconfigured(self): + def misconfigured(self) -> bool: """Is plugin misconfigured?""" return isinstance(self._prepared, errors.MisconfigurationError) @property - def problem(self): + def problem(self) -> Optional[Exception]: """Return the Exception raised during plugin setup, or None if all is well""" if isinstance(self._prepared, Exception): return self._prepared return None @property - def available(self): + def available(self) -> bool: """Is plugin available, i.e. prepared or misconfigured?""" return self._prepared is True or self.misconfigured - def __repr__(self): + def __repr__(self) -> str: return "PluginEntryPoint#{0}".format(self.name) - def __str__(self): + def __str__(self) -> str: lines = [ "* {0}".format(self.name), "Description: {0}".format(self.plugin_cls.description), @@ -205,7 +215,7 @@ class PluginEntryPoint: class PluginsRegistry(Mapping): """Plugins registry.""" - def __init__(self, plugins): + def __init__(self, plugins: Mapping[str, PluginEntryPoint]) -> None: # plugins are sorted so the same order is used between runs. # This prevents deadlock caused by plugins acquiring a lock # and ensures at least one concurrent Certbot instance will run @@ -213,7 +223,7 @@ class PluginsRegistry(Mapping): self._plugins = dict(sorted(plugins.items())) @classmethod - def find_all(cls): + def find_all(cls) -> 'PluginsRegistry': """Find plugins using setuptools entry points.""" plugins: Dict[str, PluginEntryPoint] = {} plugin_paths_string = os.getenv('CERTBOT_PLUGIN_PATH') @@ -245,7 +255,9 @@ class PluginsRegistry(Mapping): return cls(plugins) @classmethod - def _load_entry_point(cls, entry_point, plugins, with_prefix): + def _load_entry_point(cls, entry_point: pkg_resources.EntryPoint, + plugins: Dict[str, PluginEntryPoint], + with_prefix: bool) -> PluginEntryPoint: plugin_ep = PluginEntryPoint(entry_point, with_prefix) if plugin_ep.name in plugins: other_ep = plugins[plugin_ep.name] @@ -261,47 +273,47 @@ class PluginsRegistry(Mapping): return plugin_ep - def __getitem__(self, name): + def __getitem__(self, name: str) -> PluginEntryPoint: return self._plugins[name] - def __iter__(self): + def __iter__(self) -> Iterator[str]: return iter(self._plugins) - def __len__(self): + def __len__(self) -> int: return len(self._plugins) - def init(self, config): + def init(self, config: configuration.NamespaceConfig) -> List[interfaces.Plugin]: """Initialize all plugins in the registry.""" return [plugin_ep.init(config) for plugin_ep in self._plugins.values()] - def filter(self, pred): + def filter(self, pred: Callable[[PluginEntryPoint], bool]) -> "PluginsRegistry": """Filter plugins based on predicate.""" return type(self)({name: plugin_ep for name, plugin_ep - in self._plugins.items() if pred(plugin_ep)}) + in self._plugins.items() if pred(plugin_ep)}) - def visible(self): + def visible(self) -> "PluginsRegistry": """Filter plugins based on visibility.""" return self.filter(lambda plugin_ep: not plugin_ep.hidden) - def ifaces(self, *ifaces_groups): + def ifaces(self, *ifaces_groups: Iterable[Type]) -> "PluginsRegistry": """Filter plugins based on interfaces.""" return self.filter(lambda p_ep: p_ep.ifaces(*ifaces_groups)) - def verify(self, ifaces): + def verify(self, ifaces: Iterable[Type]) -> "PluginsRegistry": """Filter plugins based on verification.""" return self.filter(lambda p_ep: p_ep.verify(ifaces)) - def prepare(self): + def prepare(self) -> List[Union[bool, Error]]: """Prepare all plugins in the registry.""" return [plugin_ep.prepare() for plugin_ep in self._plugins.values()] - def available(self): + def available(self) -> "PluginsRegistry": """Filter plugins based on availability.""" return self.filter(lambda p_ep: p_ep.available) # successfully prepared + misconfigured - def find_init(self, plugin): + def find_init(self, plugin: interfaces.Plugin) -> Optional[PluginEntryPoint]: """Find an initialized plugin. This is particularly useful for finding a name for the plugin:: @@ -321,12 +333,12 @@ class PluginsRegistry(Mapping): return candidates[0] return None - def __repr__(self): + def __repr__(self) -> str: return "{0}({1})".format( self.__class__.__name__, ','.join( repr(p_ep) for p_ep in self._plugins.values())) - def __str__(self): + def __str__(self) -> str: if not self._plugins: return "No plugins" return "\n\n".join(str(p_ep) for p_ep in self._plugins.values()) diff --git a/certbot/certbot/_internal/plugins/manual.py b/certbot/certbot/_internal/plugins/manual.py index d2372e7dd..dc45ae271 100644 --- a/certbot/certbot/_internal/plugins/manual.py +++ b/certbot/certbot/_internal/plugins/manual.py @@ -1,6 +1,12 @@ """Manual authenticator plugin""" import logging +from typing import Any +from typing import Callable from typing import Dict +from typing import Iterable +from typing import List +from typing import Tuple +from typing import Type from acme import challenges from certbot import achallenges @@ -88,7 +94,7 @@ asked to create multiple distinct TXT records with the same name. This is permitted by DNS standards.) """ - def __init__(self, *args, **kwargs): + def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) self.reverter = reverter.Reverter(self.config) self.reverter.recovery_routine() @@ -97,14 +103,14 @@ permitted by DNS standards.) self.subsequent_any_challenge = False @classmethod - def add_parser_arguments(cls, add): + def add_parser_arguments(cls, add: Callable[..., None]) -> None: add('auth-hook', help='Path or command to execute for the authentication script') add('cleanup-hook', help='Path or command to execute for the cleanup script') util.add_deprecated_argument(add, 'public-ip-logging-ok', 0) - def prepare(self): # pylint: disable=missing-function-docstring + def prepare(self) -> None: # pylint: disable=missing-function-docstring if self.config.noninteractive_mode and not self.conf('auth-hook'): raise errors.PluginError( 'An authentication script must be provided with --{0} when ' @@ -112,7 +118,7 @@ permitted by DNS standards.) self.option_name('auth-hook'))) self._validate_hooks() - def _validate_hooks(self): + def _validate_hooks(self) -> None: if self.config.validate_hooks: for name in ('auth-hook', 'cleanup-hook'): hook = self.conf(name) @@ -120,13 +126,13 @@ permitted by DNS standards.) hook_prefix = self.option_name(name)[:-len('-hook')] hooks.validate_hook(hook, hook_prefix) - def more_info(self): # pylint: disable=missing-function-docstring + def more_info(self) -> str: # pylint: disable=missing-function-docstring return ( 'This plugin allows the user to customize setup for domain ' 'validation challenges either through shell scripts provided by ' 'the user or by performing the setup manually.') - def auth_hint(self, failed_achalls): + def auth_hint(self, failed_achalls: Iterable[achallenges.AnnotatedChallenge]) -> str: has_chall = lambda cls: any(isinstance(achall.chall, cls) for achall in failed_achalls) has_dns = has_chall(challenges.DNS01) @@ -162,11 +168,12 @@ permitted by DNS standards.) ) ) - def get_chall_pref(self, domain): + def get_chall_pref(self, domain: str) -> Iterable[Type[challenges.Challenge]]: # pylint: disable=unused-argument,missing-function-docstring return [challenges.HTTP01, challenges.DNS01] - def perform(self, achalls): # pylint: disable=missing-function-docstring + def perform(self, achalls: List[achallenges.AnnotatedChallenge] + ) -> List[challenges.ChallengeResponse]: # pylint: disable=missing-function-docstring responses = [] last_dns_achall = 0 for i, achall in enumerate(achalls): @@ -180,7 +187,8 @@ permitted by DNS standards.) responses.append(achall.response(achall.account_key)) return responses - def _perform_achall_with_script(self, achall, achalls): + def _perform_achall_with_script(self, achall: achallenges.AnnotatedChallenge, + achalls: List[achallenges.AnnotatedChallenge]) -> None: env = dict(CERTBOT_DOMAIN=achall.domain, CERTBOT_VALIDATION=achall.validation(achall.account_key), CERTBOT_ALL_DOMAINS=','.join(one_achall.domain for one_achall in achalls), @@ -194,7 +202,8 @@ permitted by DNS standards.) env['CERTBOT_AUTH_OUTPUT'] = out.strip() self.env[achall] = env - def _perform_achall_manually(self, achall, last_dns_achall=False): + def _perform_achall_manually(self, achall: achallenges.AnnotatedChallenge, + last_dns_achall: bool = False) -> None: validation = achall.validation(achall.account_key) if isinstance(achall.chall, challenges.HTTP01): msg = self._HTTP_INSTRUCTIONS.format( @@ -225,7 +234,7 @@ permitted by DNS standards.) display_util.notification(msg, wrap=False, force_interactive=True) self.subsequent_any_challenge = True - def cleanup(self, achalls): # pylint: disable=missing-function-docstring + def cleanup(self, achalls: Iterable[achallenges.AnnotatedChallenge]) -> None: # pylint: disable=missing-function-docstring if self.conf('cleanup-hook'): for achall in achalls: env = self.env.pop(achall) @@ -235,7 +244,7 @@ permitted by DNS standards.) self._execute_hook('cleanup-hook', achall.domain) self.reverter.recovery_routine() - def _execute_hook(self, hook_name, achall_domain): + def _execute_hook(self, hook_name: str, achall_domain: str) -> Tuple[str, str]: returncode, err, out = misc.execute_command_status( self.option_name(hook_name), self.conf(hook_name), env=util.env_no_snap_for_external_calls() diff --git a/certbot/certbot/_internal/plugins/null.py b/certbot/certbot/_internal/plugins/null.py index b800c5c39..e79a88bb9 100644 --- a/certbot/certbot/_internal/plugins/null.py +++ b/certbot/certbot/_internal/plugins/null.py @@ -1,5 +1,9 @@ """Null plugin.""" import logging +from typing import Callable +from typing import List +from typing import Optional +from typing import Union from certbot import interfaces from certbot.plugins import common @@ -14,41 +18,42 @@ class Installer(common.Plugin, interfaces.Installer): hidden = True @classmethod - def add_parser_arguments(cls, add): + def add_parser_arguments(cls, add: Callable[..., None]) -> None: pass # pylint: disable=missing-function-docstring - def prepare(self): + def prepare(self) -> None: pass # pragma: no cover - def more_info(self): + def more_info(self) -> str: return "Installer that doesn't do anything (for testing)." - def get_all_names(self): + def get_all_names(self) -> List[str]: return [] - def deploy_cert(self, domain, cert_path, key_path, - chain_path=None, fullchain_path=None): + def deploy_cert(self, domain: str, cert_path: str, key_path: str, + chain_path: str, fullchain_path: str) -> None: pass # pragma: no cover - def enhance(self, domain, enhancement, options=None): + def enhance(self, domain: str, enhancement: str, + options: Optional[Union[List[str], str]] = None) -> None: pass # pragma: no cover - def supported_enhancements(self): + def supported_enhancements(self) -> List[str]: return [] - def save(self, title=None, temporary=False): + def save(self, title: Optional[str] = None, temporary: bool = False) -> None: pass # pragma: no cover - def rollback_checkpoints(self, rollback=1): + def rollback_checkpoints(self, rollback: int = 1) -> None: pass # pragma: no cover - def recovery_routine(self): + def recovery_routine(self) -> None: pass # pragma: no cover - def config_test(self): + def config_test(self) -> None: pass # pragma: no cover - def restart(self): + def restart(self) -> None: pass # pragma: no cover diff --git a/certbot/certbot/_internal/plugins/selection.py b/certbot/certbot/_internal/plugins/selection.py index 0e88e1324..f62db4089 100644 --- a/certbot/certbot/_internal/plugins/selection.py +++ b/certbot/certbot/_internal/plugins/selection.py @@ -1,8 +1,13 @@ """Decide which plugins to use for authentication & installation""" import logging +from typing import cast +from typing import Iterable +from typing import List from typing import Optional from typing import Tuple +from typing import Type +from typing import TypeVar from certbot import configuration from certbot import errors @@ -14,32 +19,36 @@ from certbot.display import util as display_util logger = logging.getLogger(__name__) -def pick_configurator( - config, default, plugins, - question="How would you like to authenticate and install " - "certificates?"): +def pick_configurator(config: configuration.NamespaceConfig, default: Optional[str], + plugins: disco.PluginsRegistry, + question: str = "How would you like to authenticate and install " + "certificates?") -> Optional[interfaces.Plugin]: """Pick configurator plugin.""" return pick_plugin( config, default, plugins, question, (interfaces.Authenticator, interfaces.Installer)) -def pick_installer(config, default, plugins, - question="How would you like to install certificates?"): +def pick_installer(config: configuration.NamespaceConfig, default: Optional[str], + plugins: disco.PluginsRegistry, + question: str = "How would you like to install certificates?" + ) -> Optional[interfaces.Installer]: """Pick installer plugin.""" - return pick_plugin( - config, default, plugins, question, (interfaces.Installer,)) + return pick_plugin(config, default, plugins, question, (interfaces.Installer,)) -def pick_authenticator( - config, default, plugins, question="How would you " - "like to authenticate with the ACME CA?"): +def pick_authenticator(config: configuration.NamespaceConfig, default: Optional[str], + plugins: disco.PluginsRegistry, + question: str = "How would you " + "like to authenticate with the ACME CA?" + ) -> Optional[interfaces.Authenticator]: """Pick authentication plugin.""" return pick_plugin( config, default, plugins, question, (interfaces.Authenticator,)) -def get_unprepared_installer(config, plugins): +def get_unprepared_installer(config: configuration.NamespaceConfig, + plugins: disco.PluginsRegistry) -> Optional[interfaces.Installer]: """ Get an unprepared interfaces.Installer object. @@ -69,10 +78,15 @@ def get_unprepared_installer(config, plugins): "Could not select or initialize the requested installer %s." % req_inst) -def pick_plugin(config, default, plugins, question, ifaces): +P = TypeVar('P', bound=interfaces.Plugin) + + +def pick_plugin(config: configuration.NamespaceConfig, default: Optional[str], + plugins: disco.PluginsRegistry, question: str, + ifaces: Iterable[Type]) -> Optional[P]: """Pick plugin. - :param certbot.configuration.NamespaceConfig: Configuration + :param certbot.configuration.NamespaceConfig config: Configuration :param str default: Plugin name supplied by user or ``None``. :param certbot._internal.plugins.disco.PluginsRegistry plugins: All plugins registered as entry points. @@ -108,22 +122,23 @@ def pick_plugin(config, default, plugins, question, ifaces): if len(prepared) > 1: logger.debug("Multiple candidate plugins: %s", prepared) - plugin_ep = choose_plugin(list(prepared.values()), question) - if plugin_ep is None: + plugin_ep1 = choose_plugin(list(prepared.values()), question) + if plugin_ep1 is None: return None - return plugin_ep.init() + return cast(P, plugin_ep1.init()) elif len(prepared) == 1: - plugin_ep = list(prepared.values())[0] - logger.debug("Single candidate plugin: %s", plugin_ep) - if plugin_ep.misconfigured: + plugin_ep2 = list(prepared.values())[0] + logger.debug("Single candidate plugin: %s", plugin_ep2) + if plugin_ep2.misconfigured: return None - return plugin_ep.init() + return plugin_ep2.init() else: logger.debug("No candidate plugin") return None -def choose_plugin(prepared, question): +def choose_plugin(prepared: List[disco.PluginEntryPoint], + question: str) -> Optional[disco.PluginEntryPoint]: """Allow the user to choose their plugin. :param list prepared: List of `~.PluginEntryPoint`. @@ -152,17 +167,29 @@ def choose_plugin(prepared, question): else: return None + noninstaller_plugins = ["webroot", "manual", "standalone", "dns-cloudflare", "dns-cloudxns", "dns-digitalocean", "dns-dnsimple", "dns-dnsmadeeasy", "dns-gehirn", "dns-google", "dns-linode", "dns-luadns", "dns-nsone", "dns-ovh", "dns-rfc2136", "dns-route53", "dns-sakuracloud"] -def record_chosen_plugins(config, plugins, auth, inst): - "Update the config entries to reflect the plugins we actually selected." - config.authenticator = plugins.find_init(auth).name if auth else None - config.installer = plugins.find_init(inst).name if inst else None + +def record_chosen_plugins(config: configuration.NamespaceConfig, plugins: disco.PluginsRegistry, + auth: Optional[interfaces.Authenticator], + inst: Optional[interfaces.Installer]) -> None: + """Update the config entries to reflect the plugins we actually selected.""" + config.authenticator = None + if auth: + auth_ep = plugins.find_init(auth) + if auth_ep: + config.authenticator = auth_ep.name + config.installer = None + if inst: + inst_ep = plugins.find_init(inst) + if inst_ep: + config.installer = inst_ep.name logger.info("Plugins selected: Authenticator %s, Installer %s", - config.authenticator, config.installer) + config.authenticator, config.installer) def choose_configurator_plugins(config: configuration.NamespaceConfig, @@ -181,7 +208,7 @@ def choose_configurator_plugins(config: configuration.NamespaceConfig, """ req_auth, req_inst = cli_plugin_requests(config) - installer_question = None + installer_question = "" if verb == "enhance": installer_question = ("Which installer would you like to use to " @@ -209,11 +236,14 @@ def choose_configurator_plugins(config: configuration.NamespaceConfig, logger.warning("Specifying an authenticator doesn't make sense when " "running Certbot with verb \"%s\"", verb) # Try to meet the user's request and/or ask them to pick plugins - authenticator = installer = None + authenticator: Optional[interfaces.Authenticator] = None + installer: Optional[interfaces.Installer] = None if verb == "run" and req_auth == req_inst: # Unless the user has explicitly asked for different auth/install, # only consider offering a single choice - authenticator = installer = pick_configurator(config, req_inst, plugins) + configurator = pick_configurator(config, req_inst, plugins) + authenticator = cast(Optional[interfaces.Authenticator], configurator) + installer = cast(Optional[interfaces.Installer], configurator) else: if need_inst or req_inst: installer = pick_installer(config, req_inst, plugins, installer_question) @@ -231,11 +261,11 @@ def choose_configurator_plugins(config: configuration.NamespaceConfig, return installer, authenticator -def set_configurator(previously, now): +def set_configurator(previously: Optional[str], now: Optional[str]) -> Optional[str]: """ Setting configurators multiple ways is okay, as long as they all agree :param str previously: previously identified request for the installer/authenticator - :param str requested: the request currently being processed + :param str now: the request currently being processed """ if not now: # we're not actually setting anything @@ -247,7 +277,8 @@ def set_configurator(previously, now): return now -def cli_plugin_requests(config): +def cli_plugin_requests(config: configuration.NamespaceConfig + ) -> Tuple[Optional[str], Optional[str]]: """ Figure out which plugins the user requested with CLI and config options @@ -302,7 +333,8 @@ def cli_plugin_requests(config): return req_auth, req_inst -def diagnose_configurator_problem(cfg_type, requested, plugins): +def diagnose_configurator_problem(cfg_type: str, requested: Optional[str], + plugins: disco.PluginsRegistry) -> None: """ Raise the most helpful error message about a plugin being unavailable diff --git a/certbot/certbot/_internal/plugins/standalone.py b/certbot/certbot/_internal/plugins/standalone.py index 45c801256..f70d2ed9e 100644 --- a/certbot/certbot/_internal/plugins/standalone.py +++ b/certbot/certbot/_internal/plugins/standalone.py @@ -3,14 +3,19 @@ import collections import errno import logging import socket +from typing import Any +from typing import Callable from typing import DefaultDict from typing import Dict +from typing import Iterable from typing import List +from typing import Mapping from typing import Set from typing import Tuple +from typing import Type from typing import TYPE_CHECKING -import OpenSSL +from OpenSSL import crypto from acme import challenges from acme import standalone as acme_standalone @@ -42,12 +47,15 @@ class ServerManager: will serve the same URLs! """ - def __init__(self, certs, http_01_resources): - self._instances: Dict[int, acme_standalone.BaseDualNetworkedServers] = {} + def __init__(self, certs: Mapping[bytes, Tuple[crypto.PKey, crypto.X509]], + http_01_resources: Set[acme_standalone.HTTP01RequestHandler.HTTP01Resource] + ) -> None: + self._instances: Dict[int, acme_standalone.HTTP01DualNetworkedServers] = {} self.certs = certs self.http_01_resources = http_01_resources - def run(self, port, challenge_type, listenaddr=""): + def run(self, port: int, challenge_type: Type[challenges.Challenge], + listenaddr: str = "") -> acme_standalone.HTTP01DualNetworkedServers: """Run ACME server on specified ``port``. This method is idempotent, i.e. all calls with the same pair of @@ -81,7 +89,7 @@ class ServerManager: self._instances[real_port] = servers return servers - def stop(self, port): + def stop(self, port: int) -> None: """Stop ACME server running on the specified ``port``. :param int port: @@ -94,7 +102,7 @@ class ServerManager: instance.shutdown_and_server_close() del self._instances[port] - def running(self): + def running(self) -> Dict[int, acme_standalone.HTTP01DualNetworkedServers]: """Return all running instances. Once the server is stopped using `stop`, it will not be @@ -118,7 +126,7 @@ class Authenticator(common.Plugin, interfaces.Authenticator): description = "Spin up a temporary webserver" - def __init__(self, *args, **kwargs): + def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) self.served: ServedType = collections.defaultdict(set) @@ -127,44 +135,49 @@ class Authenticator(common.Plugin, interfaces.Authenticator): # values, main thread writes). Due to the nature of CPython's # GIL, the operations are safe, c.f. # https://docs.python.org/2/faq/library.html#what-kinds-of-global-value-mutation-are-thread-safe - self.certs: Dict[bytes, Tuple[OpenSSL.crypto.PKey, OpenSSL.crypto.X509]] = {} + self.certs: Mapping[bytes, Tuple[crypto.PKey, crypto.X509]] = {} self.http_01_resources: Set[acme_standalone.HTTP01RequestHandler.HTTP01Resource] = set() self.servers = ServerManager(self.certs, self.http_01_resources) @classmethod - def add_parser_arguments(cls, add): + def add_parser_arguments(cls, add: Callable[..., None]) -> None: pass # No additional argument for the standalone plugin parser - def more_info(self): # pylint: disable=missing-function-docstring + def more_info(self) -> str: # pylint: disable=missing-function-docstring return("This authenticator creates its own ephemeral TCP listener " "on the necessary port in order to respond to incoming " "http-01 challenges from the certificate authority. Therefore, " "it does not rely on any existing server program.") - def prepare(self): # pylint: disable=missing-function-docstring + def prepare(self) -> None: # pylint: disable=missing-function-docstring pass - def get_chall_pref(self, domain): + def get_chall_pref(self, domain: str) -> Iterable[Type[challenges.Challenge]]: # pylint: disable=unused-argument,missing-function-docstring return [challenges.HTTP01] - def perform(self, achalls): # pylint: disable=missing-function-docstring + def perform(self, achalls: Iterable[achallenges.AnnotatedChallenge] + ) -> List[challenges.ChallengeResponse]: # pylint: disable=missing-function-docstring return [self._try_perform_single(achall) for achall in achalls] - def _try_perform_single(self, achall): + def _try_perform_single(self, + achall: achallenges.AnnotatedChallenge) -> challenges.ChallengeResponse: while True: try: return self._perform_single(achall) except errors.StandaloneBindError as error: _handle_perform_error(error) - def _perform_single(self, achall): + def _perform_single(self, + achall: achallenges.AnnotatedChallenge) -> challenges.ChallengeResponse: servers, response = self._perform_http_01(achall) self.served[servers].add(achall) return response - def _perform_http_01(self, achall): + def _perform_http_01(self, achall: achallenges.AnnotatedChallenge + ) -> Tuple[acme_standalone.HTTP01DualNetworkedServers, + challenges.ChallengeResponse]: port = self.config.http01_port addr = self.config.http01_address servers = self.servers.run(port, challenges.HTTP01, listenaddr=addr) @@ -174,7 +187,7 @@ class Authenticator(common.Plugin, interfaces.Authenticator): self.http_01_resources.add(resource) return servers, response - def cleanup(self, achalls): # pylint: disable=missing-function-docstring + def cleanup(self, achalls: Iterable[achallenges.AnnotatedChallenge]) -> None: # pylint: disable=missing-function-docstring # reduce self.served and close servers if no challenges are served for unused_servers, server_achalls in self.served.items(): for achall in achalls: @@ -193,7 +206,7 @@ class Authenticator(common.Plugin, interfaces.Authenticator): "accept inbound connections from the internet.") -def _handle_perform_error(error): +def _handle_perform_error(error: errors.StandaloneBindError) -> None: if error.socket_error.errno == errno.EACCES: raise errors.PluginError( "Could not bind TCP port {0} because you don't have " diff --git a/certbot/certbot/_internal/plugins/webroot.py b/certbot/certbot/_internal/plugins/webroot.py index 1a4892ac9..bb61d8220 100644 --- a/certbot/certbot/_internal/plugins/webroot.py +++ b/certbot/certbot/_internal/plugins/webroot.py @@ -3,10 +3,17 @@ import argparse import collections import json import logging +from typing import Any +from typing import Callable from typing import DefaultDict from typing import Dict +from typing import Iterable from typing import List +from typing import Optional +from typing import Sequence from typing import Set +from typing import Type +from typing import Union from acme import challenges from certbot import crypto_util @@ -57,11 +64,11 @@ necessary validation resources to appropriate paths on the file system. It expects that there is some other HTTP server configured to serve all files under specified web root ({0}).""" - def more_info(self): # pylint: disable=missing-function-docstring + def more_info(self) -> str: # pylint: disable=missing-function-docstring return self.MORE_INFO.format(self.conf("path")) @classmethod - def add_parser_arguments(cls, add): + def add_parser_arguments(cls, add: Callable[..., None]) -> None: add("path", "-w", default=[], action=_WebrootPathAction, help="public_html / webroot path. This can be specified multiple " "times to handle different domains; each domain will have " @@ -78,34 +85,34 @@ to serve all files under specified web root ({0}).""" "file, it needs to be on a single line, like: webroot-map = " '{"example.com":"/var/www"}.') - def auth_hint(self, failed_achalls): # pragma: no cover + def auth_hint(self, failed_achalls: Iterable[AnnotatedChallenge]) -> str: # pragma: no cover return ("The Certificate Authority failed to download the temporary challenge files " "created by Certbot. Ensure that the listed domains serve their content from " "the provided --webroot-path/-w and that files created there can be downloaded " "from the internet.") - def get_chall_pref(self, domain): # pragma: no cover + def get_chall_pref(self, domain: str) -> Iterable[Type[challenges.Challenge]]: # pylint: disable=unused-argument,missing-function-docstring return [challenges.HTTP01] - def __init__(self, *args, **kwargs): + def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) self.full_roots: Dict[str, str] = {} self.performed: DefaultDict[str, Set[AnnotatedChallenge]] = collections.defaultdict(set) # stack of dirs successfully created by this authenticator self._created_dirs: List[str] = [] - def prepare(self): # pylint: disable=missing-function-docstring + def prepare(self) -> None: # pylint: disable=missing-function-docstring pass - def perform(self, achalls): # pylint: disable=missing-function-docstring + def perform(self, achalls: Iterable[AnnotatedChallenge]) -> List[challenges.ChallengeResponse]: # pylint: disable=missing-function-docstring self._set_webroots(achalls) self._create_challenge_dirs() return [self._perform_single(achall) for achall in achalls] - def _set_webroots(self, achalls): + def _set_webroots(self, achalls: Iterable[AnnotatedChallenge]) -> None: if self.conf("path"): webroot_path = self.conf("path")[-1] logger.info("Using the webroot path %s for all unmatched domains.", @@ -127,7 +134,7 @@ to serve all files under specified web root ({0}).""" known_webroots.insert(0, new_webroot) self.conf("map")[achall.domain] = new_webroot - def _prompt_for_webroot(self, domain, known_webroots): + def _prompt_for_webroot(self, domain: str, known_webroots: List[str]) -> Optional[str]: webroot = None while webroot is None: @@ -142,7 +149,8 @@ to serve all files under specified web root ({0}).""" return webroot - def _prompt_with_webroot_list(self, domain, known_webroots): + def _prompt_with_webroot_list(self, domain: str, + known_webroots: List[str]) -> Optional[str]: path_flag = "--" + self.option_name("path") while True: @@ -156,7 +164,7 @@ to serve all files under specified web root ({0}).""" "webroot when using the webroot plugin.") return None if index == 0 else known_webroots[index - 1] # code == display_util.OK - def _prompt_for_new_webroot(self, domain, allowraise=False): + def _prompt_for_new_webroot(self, domain: str, allowraise: bool = False) -> Optional[str]: code, webroot = ops.validated_directory( _validate_webroot, "Input the webroot for {0}:".format(domain), @@ -169,7 +177,7 @@ to serve all files under specified web root ({0}).""" "webroot when using the webroot plugin.") return _validate_webroot(webroot) # code == display_util.OK - def _create_challenge_dirs(self): + def _create_challenge_dirs(self) -> None: path_map = self.conf("map") if not path_map: raise errors.PluginError( @@ -227,10 +235,10 @@ to serve all files under specified web root ({0}).""" with safe_open(web_config_path, mode="w", chmod=0o644) as web_config: web_config.write(_WEB_CONFIG_CONTENT) - def _get_validation_path(self, root_path, achall): + def _get_validation_path(self, root_path: str, achall: AnnotatedChallenge) -> str: return os.path.join(root_path, achall.chall.encode("token")) - def _perform_single(self, achall): + def _perform_single(self, achall: AnnotatedChallenge) -> challenges.ChallengeResponse: response, validation = achall.response_and_validation() root_path = self.full_roots[achall.domain] @@ -249,7 +257,7 @@ to serve all files under specified web root ({0}).""" self.performed[root_path].add(achall) return response - def cleanup(self, achalls): # pylint: disable=missing-function-docstring + def cleanup(self, achalls: Iterable[AnnotatedChallenge]) -> None: # pylint: disable=missing-function-docstring for achall in achalls: root_path = self.full_roots.get(achall.domain, None) if root_path is not None: @@ -270,7 +278,6 @@ to serve all files under specified web root ({0}).""" logger.info("Not cleaning up the web.config file in %s " "because it is not generated by Certbot.", root_path) - not_removed: List[str] = [] while self._created_dirs: path = self._created_dirs.pop() @@ -287,8 +294,12 @@ to serve all files under specified web root ({0}).""" class _WebrootMapAction(argparse.Action): """Action class for parsing webroot_map.""" - def __call__(self, parser, namespace, webroot_map, option_string=None): - for domains, webroot_path in json.loads(webroot_map).items(): + def __call__(self, parser: argparse.ArgumentParser, namespace: argparse.Namespace, + webroot_map: Union[str, Sequence[Any], None], + option_string: Optional[str] = None) -> None: + if webroot_map is None: + return + for domains, webroot_path in json.loads(str(webroot_map)).items(): webroot_path = _validate_webroot(webroot_path) namespace.webroot_map.update( (d, webroot_path) for d in cli.add_domains(namespace, domains)) @@ -297,11 +308,15 @@ class _WebrootMapAction(argparse.Action): class _WebrootPathAction(argparse.Action): """Action class for parsing webroot_path.""" - def __init__(self, *args, **kwargs): + def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) self._domain_before_webroot = False - def __call__(self, parser, namespace, webroot_path, option_string=None): + def __call__(self, parser: argparse.ArgumentParser, namespace: argparse.Namespace, + webroot_path: Union[str, Sequence[Any], None], + option_string: Optional[str] = None) -> None: + if webroot_path is None: + return if self._domain_before_webroot: raise errors.PluginError( "If you specify multiple webroot paths, " @@ -316,10 +331,10 @@ class _WebrootPathAction(argparse.Action): elif namespace.domains: self._domain_before_webroot = True - namespace.webroot_path.append(_validate_webroot(webroot_path)) + namespace.webroot_path.append(_validate_webroot(str(webroot_path))) -def _validate_webroot(webroot_path): +def _validate_webroot(webroot_path: str) -> str: """Validates and returns the absolute path of webroot_path. :param str webroot_path: path to the webroot directory diff --git a/certbot/certbot/_internal/renewal.py b/certbot/certbot/_internal/renewal.py index 183e83ec0..91a38b11f 100644 --- a/certbot/certbot/_internal/renewal.py +++ b/certbot/certbot/_internal/renewal.py @@ -7,8 +7,13 @@ import random import sys import time import traceback +from typing import Any +from typing import Dict +from typing import Iterable from typing import List +from typing import Mapping from typing import Optional +from typing import Union from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.asymmetric import ec @@ -50,7 +55,8 @@ CONFIG_ITEMS = set(itertools.chain( BOOL_CONFIG_ITEMS, INT_CONFIG_ITEMS, STR_CONFIG_ITEMS, ('pref_challs',))) -def _reconstitute(config, full_path): +def _reconstitute(config: configuration.NamespaceConfig, + full_path: str) -> Optional[storage.RenewableCert]: """Try to instantiate a RenewableCert, updating config with relevant items. This is specifically for use in renewal and enforces several checks @@ -108,7 +114,8 @@ def _reconstitute(config, full_path): return renewal_candidate -def _restore_webroot_config(config, renewalparams): +def _restore_webroot_config(config: configuration.NamespaceConfig, + renewalparams: Mapping[str, Any]) -> None: """ webroot_map is, uniquely, a dict, and the general-purpose configuration restoring logic is not able to correctly parse it from the serialized @@ -125,7 +132,8 @@ def _restore_webroot_config(config, renewalparams): config.webroot_path = wp -def _restore_plugin_configs(config, renewalparams): +def _restore_plugin_configs(config: configuration.NamespaceConfig, + renewalparams: Mapping[str, Any]) -> None: """Sets plugin specific values in config from renewalparams :param configuration.NamespaceConfig config: configuration for the @@ -168,7 +176,8 @@ def _restore_plugin_configs(config, renewalparams): setattr(config, config_item, cast(config_value)) -def restore_required_config_elements(config, renewalparams): +def restore_required_config_elements(config: configuration.NamespaceConfig, + renewalparams: Mapping[str, Any]) -> None: """Sets non-plugin specific values in config from renewalparams :param configuration.NamespaceConfig config: configuration for the @@ -189,7 +198,7 @@ def restore_required_config_elements(config, renewalparams): setattr(config.namespace, item_name, value) -def _remove_deprecated_config_elements(renewalparams): +def _remove_deprecated_config_elements(renewalparams: Mapping[str, Any]) -> Dict[str, Any]: """Removes deprecated config options from the parsed renewalparams. :param dict renewalparams: list of parsed renewalparams @@ -202,7 +211,7 @@ def _remove_deprecated_config_elements(renewalparams): if option_name not in cli.DEPRECATED_OPTIONS} -def _restore_pref_challs(unused_name, value): +def _restore_pref_challs(unused_name: str, value: Union[List[str], str]) -> List[str]: """Restores preferred challenges from a renewal config file. If value is a `str`, it should be a single challenge type. @@ -224,7 +233,7 @@ def _restore_pref_challs(unused_name, value): return cli.parse_preferred_challenges(value) -def _restore_bool(name, value): +def _restore_bool(name: str, value: str) -> bool: """Restores a boolean key-value pair from a renewal config file. :param str name: option name @@ -243,7 +252,7 @@ def _restore_bool(name, value): return lowercase_value == "true" -def _restore_int(name, value): +def _restore_int(name: str, value: str) -> int: """Restores an integer key-value pair from a renewal config file. :param str name: option name @@ -265,7 +274,7 @@ def _restore_int(name, value): raise errors.Error("Expected a numeric value for {0}".format(name)) -def _restore_str(name, value): +def _restore_str(name: str, value: str) -> Optional[str]: """Restores a string key-value pair from a renewal config file. :param str name: option name @@ -290,7 +299,7 @@ def _restore_str(name, value): return None if value == "None" else value -def should_renew(config, lineage): +def should_renew(config: configuration.NamespaceConfig, lineage: storage.RenewableCert) -> bool: """Return true if any of the circumstances for automatic renewal apply.""" if config.renew_by_default: logger.debug("Auto-renewal forced with --force-renewal...") @@ -305,7 +314,8 @@ def should_renew(config, lineage): return False -def _avoid_invalidating_lineage(config, lineage, original_server): +def _avoid_invalidating_lineage(config: configuration.NamespaceConfig, + lineage: storage.RenewableCert, original_server: str) -> None: """Do not renew a valid cert with one from a staging server!""" if util.is_staging(config.server): if not util.is_staging(original_server): @@ -344,7 +354,7 @@ def renew_cert(config: configuration.NamespaceConfig, domains: Optional[List[str hooks.renew_hook(config, domains, lineage.live_dir) -def report(msgs, category): +def report(msgs: Iterable[str], category: str) -> str: """Format a results report for a category of renewal outcomes""" lines = ("%s (%s)" % (m, category) for m in msgs) return " " + "\n ".join(lines) @@ -398,7 +408,7 @@ def _renew_describe_results(config: configuration.NamespaceConfig, renew_success notify(display_obj.SIDE_FRAME) -def handle_renewal_request(config): +def handle_renewal_request(config: configuration.NamespaceConfig) -> None: """Examine each lineage; renew if due and report results""" # This is trivially False if config.domains is empty @@ -448,7 +458,7 @@ def handle_renewal_request(config): continue try: - if renewal_candidate is None: + if not renewal_candidate: parse_failures.append(renewal_file) else: # This call is done only for retro-compatibility purposes. @@ -490,7 +500,8 @@ def handle_renewal_request(config): lineagename, e ) logger.debug("Traceback was:\n%s", traceback.format_exc()) - renew_failures.append(renewal_candidate.fullchain) + if renewal_candidate: + renew_failures.append(renewal_candidate.fullchain) # Describe all the results _renew_describe_results(config, renew_successes, renew_failures, diff --git a/certbot/certbot/_internal/reporter.py b/certbot/certbot/_internal/reporter.py index 22275ff99..333fcca48 100644 --- a/certbot/certbot/_internal/reporter.py +++ b/certbot/certbot/_internal/reporter.py @@ -5,6 +5,7 @@ import queue import sys import textwrap +from certbot import configuration from certbot import util logger = logging.getLogger(__name__) @@ -27,11 +28,11 @@ class Reporter: _msg_type = collections.namedtuple('_msg_type', 'priority text on_crash') - def __init__(self, config): + def __init__(self, config: configuration.NamespaceConfig) -> None: self.messages: queue.PriorityQueue[Reporter._msg_type] = queue.PriorityQueue() self.config = config - def add_message(self, msg, priority, on_crash=True): + 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. @@ -47,7 +48,7 @@ class Reporter: self.messages.put(self._msg_type(priority, msg, on_crash)) logger.debug("Reporting to user: %s", msg) - def print_messages(self): + def print_messages(self) -> None: """Prints messages to the user and clears the message queue. If there is an unhandled exception, only messages for which diff --git a/certbot/certbot/_internal/snap_config.py b/certbot/certbot/_internal/snap_config.py index f7832cd55..3aad79912 100644 --- a/certbot/certbot/_internal/snap_config.py +++ b/certbot/certbot/_internal/snap_config.py @@ -1,7 +1,9 @@ """Module configuring Certbot in a snap environment""" import logging import socket +from typing import Iterable from typing import List +from typing import Optional from requests import Session from requests.adapters import HTTPAdapter @@ -79,23 +81,24 @@ def prepare_env(cli_args: List[str]) -> List[str]: class _SnapdConnection(HTTPConnection): - def __init__(self): + def __init__(self) -> None: super().__init__("localhost") - self.sock = None + self.sock: Optional[socket.socket] = None - def connect(self): + def connect(self) -> None: self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) self.sock.connect("/run/snapd.socket") class _SnapdConnectionPool(HTTPConnectionPool): - def __init__(self): + def __init__(self) -> None: super().__init__("localhost") - def _new_conn(self): + def _new_conn(self) -> _SnapdConnection: return _SnapdConnection() class _SnapdAdapter(HTTPAdapter): - def get_connection(self, url, proxies=None): + def get_connection(self, url: str, + proxies: Optional[Iterable[str]] = None) -> _SnapdConnectionPool: return _SnapdConnectionPool() diff --git a/certbot/certbot/_internal/storage.py b/certbot/certbot/_internal/storage.py index 954241c0e..2b4f44fd1 100644 --- a/certbot/certbot/_internal/storage.py +++ b/certbot/certbot/_internal/storage.py @@ -5,8 +5,13 @@ import logging import re import shutil import stat -from typing import cast +from typing import Any +from typing import Dict +from typing import Iterable +from typing import List +from typing import Mapping from typing import Optional +from typing import Tuple import configobj from cryptography.hazmat.backends import default_backend @@ -39,7 +44,7 @@ CURRENT_VERSION = pkg_resources.parse_version(certbot.__version__) BASE_PRIVKEY_MODE = 0o600 -def renewal_conf_files(config: configuration.NamespaceConfig): +def renewal_conf_files(config: configuration.NamespaceConfig) -> List[str]: """Build a list of all renewal configuration files. :param configuration.NamespaceConfig config: Configuration object @@ -53,7 +58,7 @@ def renewal_conf_files(config: configuration.NamespaceConfig): return result -def renewal_file_for_certname(config, certname): +def renewal_file_for_certname(config: configuration.NamespaceConfig, certname: str) -> str: """Return /path/to/certname.conf in the renewal conf directory""" path = os.path.join(config.renewal_configs_dir, "{0}.conf".format(certname)) if not os.path.exists(path): @@ -74,7 +79,8 @@ def cert_path_for_cert_name(config: configuration.NamespaceConfig, cert_name: st cert_name_implied_conf, encoding='utf-8', default_encoding='utf-8')["fullchain"] -def config_with_defaults(config=None): +def config_with_defaults(config: Optional[configuration.NamespaceConfig] = None + ) -> configobj.ConfigObj: """Merge supplied config, if provided, on top of builtin defaults.""" defaults_copy = configobj.ConfigObj( constants.RENEWER_DEFAULTS, encoding='utf-8', default_encoding='utf-8') @@ -83,7 +89,9 @@ def config_with_defaults(config=None): return defaults_copy -def add_time_interval(base_time, interval, textparser=parsedatetime.Calendar()): +def add_time_interval(base_time: datetime.datetime, interval: str, + textparser: parsedatetime.Calendar = parsedatetime.Calendar() + ) -> datetime.datetime: """Parse the time specified time interval, and add it to the base_time The interval can be in the English-language format understood by @@ -107,7 +115,9 @@ def add_time_interval(base_time, interval, textparser=parsedatetime.Calendar()): return textparser.parseDT(interval, base_time, tzinfo=tzinfo)[0] -def write_renewal_config(o_filename, n_filename, archive_dir, target, relevant_data): +def write_renewal_config(o_filename: str, n_filename: str, archive_dir: str, + target: Mapping[str, str], + relevant_data: Mapping[str, Any]) -> configobj.ConfigObj: """Writes a renewal config file with the specified name and values. :param str o_filename: Absolute path to the previous version of config file @@ -160,7 +170,8 @@ def write_renewal_config(o_filename, n_filename, archive_dir, target, relevant_d return config -def rename_renewal_config(prev_name, new_name, cli_config): +def rename_renewal_config(prev_name: str, new_name: str, + cli_config: configuration.NamespaceConfig) -> None: """Renames cli_config.certname's config to cli_config.new_certname. :param .NamespaceConfig cli_config: parsed command line @@ -178,7 +189,8 @@ def rename_renewal_config(prev_name, new_name, cli_config): "for the new certificate name.") -def update_configuration(lineagename, archive_dir, target, cli_config): +def update_configuration(lineagename: str, archive_dir: str, target: Mapping[str, str], + cli_config: configuration.NamespaceConfig) -> configobj.ConfigObj: """Modifies lineagename's config to contain the specified values. :param str lineagename: Name of the lineage being modified @@ -206,7 +218,7 @@ def update_configuration(lineagename, archive_dir, target, cli_config): return configobj.ConfigObj(config_filename, encoding='utf-8', default_encoding='utf-8') -def get_link_target(link): +def get_link_target(link: str) -> str: """Get an absolute path to the target of link. :param str link: Path to a symbolic link @@ -228,7 +240,7 @@ def get_link_target(link): return os.path.abspath(target) -def _write_live_readme_to(readme_path, is_base_dir=False): +def _write_live_readme_to(readme_path: str, is_base_dir: bool = False) -> None: prefix = "" if is_base_dir: prefix = "[cert name]/" @@ -249,7 +261,7 @@ def _write_live_readme_to(readme_path, is_base_dir=False): "certificates.\n".format(prefix=prefix)) -def _relevant(namespaces, option): +def _relevant(namespaces: Iterable[str], option: str) -> bool: """ Is this option one that could be restored for future renewal purposes? @@ -265,7 +277,7 @@ def _relevant(namespaces, option): any(option.startswith(namespace) for namespace in namespaces)) -def relevant_values(all_values): +def relevant_values(all_values: Mapping[str, Any]) -> Dict[str, Any]: """Return a new dict containing only items relevant for renewal. :param dict all_values: The original values. @@ -287,7 +299,8 @@ def relevant_values(all_values): rv["server"] = all_values["server"] return rv -def lineagename_for_filename(config_filename): + +def lineagename_for_filename(config_filename: str) -> str: """Returns the lineagename for a configuration filename. """ if not config_filename.endswith(".conf"): @@ -295,16 +308,21 @@ def lineagename_for_filename(config_filename): "renewal config file name must end in .conf") return os.path.basename(config_filename[:-len(".conf")]) -def renewal_filename_for_lineagename(config, lineagename): + +def renewal_filename_for_lineagename(config: configuration.NamespaceConfig, + lineagename: str) -> str: """Returns the lineagename for a configuration filename. """ return os.path.join(config.renewal_configs_dir, lineagename) + ".conf" -def _relpath_from_file(archive_dir, from_file): + +def _relpath_from_file(archive_dir: str, from_file: str) -> str: """Path to a directory from a file""" return os.path.relpath(archive_dir, os.path.dirname(from_file)) -def full_archive_path(config_obj, cli_config, lineagename): + +def full_archive_path(config_obj: configobj.ConfigObj, cli_config: configuration.NamespaceConfig, + lineagename: str) -> str: """Returns the full archive path for a lineagename Uses cli_config to determine archive path if not available from config_obj. @@ -317,11 +335,13 @@ def full_archive_path(config_obj, cli_config, lineagename): return config_obj["archive_dir"] return os.path.join(cli_config.default_archive_dir, lineagename) -def _full_live_path(cli_config, lineagename): + +def _full_live_path(cli_config: configuration.NamespaceConfig, lineagename: str) -> str: """Returns the full default live path for a lineagename""" return os.path.join(cli_config.live_dir, lineagename) -def delete_files(config, certname): + +def delete_files(config: configuration.NamespaceConfig, certname: str) -> None: """Delete all files related to the certificate. If some files are not found, ignore them and continue. @@ -423,7 +443,8 @@ class RenewableCert(interfaces.RenewableCert): renewal configuration file and/or systemwide defaults. """ - def __init__(self, config_filename, cli_config, update_symlinks=False): + def __init__(self, config_filename: str, cli_config: configuration.NamespaceConfig, + update_symlinks: bool = False) -> None: """Instantiate a RenewableCert object from an existing lineage. :param str config_filename: the path to the renewal config file @@ -477,27 +498,27 @@ class RenewableCert(interfaces.RenewableCert): self._check_symlinks() @property - def key_path(self): + def key_path(self) -> str: """Duck type for self.privkey""" return self.privkey @property - def cert_path(self): + def cert_path(self) -> str: """Duck type for self.cert""" return self.cert @property - def chain_path(self): + def chain_path(self) -> str: """Duck type for self.chain""" return self.chain @property - def fullchain_path(self): + def fullchain_path(self) -> str: """Duck type for self.fullchain""" return self.fullchain @property - def lineagename(self): + def lineagename(self) -> str: """Name given to the certificate lineage. :rtype: str @@ -506,21 +527,24 @@ class RenewableCert(interfaces.RenewableCert): return self._lineagename @property - def target_expiry(self): + def target_expiry(self) -> datetime.datetime: """The current target certificate's expiration datetime :returns: Expiration datetime of the current target certificate :rtype: :class:`datetime.datetime` """ - return crypto_util.notAfter(self.current_target("cert")) + cert_path = self.current_target("cert") + if not cert_path: + raise errors.Error("Target certificate does not exist.") + return crypto_util.notAfter(cert_path) @property - def archive_dir(self): + def archive_dir(self) -> str: """Returns the default or specified archive directory""" return full_archive_path(self.configuration, self.cli_config, self.lineagename) - def relative_archive_dir(self, from_file): + def relative_archive_dir(self, from_file: str) -> str: """Returns the default or specified archive directory as a relative path Used for creating symbolic links. @@ -539,7 +563,7 @@ class RenewableCert(interfaces.RenewableCert): return util.is_staging(self.server) return False - def _check_symlinks(self): + def _check_symlinks(self) -> None: """Raises an exception if a symlink doesn't exist""" for kind in ALL_FOUR: link = getattr(self, kind) @@ -551,7 +575,7 @@ class RenewableCert(interfaces.RenewableCert): raise errors.CertStorageError("target {0} of symlink {1} does " "not exist".format(target, link)) - def _update_symlinks(self): + def _update_symlinks(self) -> None: """Updates symlinks to use archive_dir""" for kind in ALL_FOUR: link = getattr(self, kind) @@ -562,7 +586,7 @@ class RenewableCert(interfaces.RenewableCert): os.unlink(link) os.symlink(new_link, link) - def _consistent(self): + def _consistent(self) -> bool: """Are the files associated with this lineage self-consistent? :returns: Whether the files stored in connection with this @@ -630,7 +654,7 @@ class RenewableCert(interfaces.RenewableCert): # for x in ALL_FOUR))) == 1 return True - def _fix(self): + def _fix(self) -> None: """Attempt to fix defects or inconsistencies in this lineage. .. todo:: Currently unimplemented. @@ -648,7 +672,7 @@ class RenewableCert(interfaces.RenewableCert): # happen as a result of random tampering by a sysadmin, or # filesystem errors, or crashes.) - def _previous_symlinks(self): + def _previous_symlinks(self) -> List[Tuple[str, str]]: """Returns the kind and path of all symlinks used in recovery. :returns: list of (kind, symlink) tuples @@ -663,7 +687,7 @@ class RenewableCert(interfaces.RenewableCert): return previous_symlinks - def _fix_symlinks(self): + def _fix_symlinks(self) -> None: """Fixes symlinks in the event of an incomplete version update. If there is no problem with the current symlinks, this function @@ -682,7 +706,7 @@ class RenewableCert(interfaces.RenewableCert): if os.path.exists(link): os.unlink(link) - def current_target(self, kind): + def current_target(self, kind: str) -> Optional[str]: """Returns full path to which the specified item currently points. :param str kind: the lineage member item ("cert", "privkey", @@ -702,7 +726,7 @@ class RenewableCert(interfaces.RenewableCert): return None return get_link_target(link) - def current_version(self, kind): + def current_version(self, kind: str) -> Optional[int]: """Returns numerical version of the specified item. For example, if kind is "chain" and the current chain link @@ -729,7 +753,7 @@ class RenewableCert(interfaces.RenewableCert): logger.debug("No matches for target %s.", kind) return None - def version(self, kind, version): + def version(self, kind: str, version: int) -> str: """The filename that corresponds to the specified version and kind. .. warning:: The specified version may not exist in this @@ -746,10 +770,13 @@ class RenewableCert(interfaces.RenewableCert): """ if kind not in ALL_FOUR: raise errors.CertStorageError("unknown kind of item") - where = os.path.dirname(self.current_target(kind)) + link = self.current_target(kind) + if not link: + raise errors.Error(f"Target {kind} does not exist!") + where = os.path.dirname(link) return os.path.join(where, "{0}{1}.pem".format(kind, version)) - def available_versions(self, kind): + def available_versions(self, kind: str) -> List[int]: """Which alternative versions of the specified kind of item exist? The archive directory where the current version is stored is @@ -764,13 +791,16 @@ class RenewableCert(interfaces.RenewableCert): """ if kind not in ALL_FOUR: raise errors.CertStorageError("unknown kind of item") - where = os.path.dirname(self.current_target(kind)) + link = self.current_target(kind) + if not link: + raise errors.Error(f"Target {kind} does not exist!") + where = os.path.dirname(link) files = os.listdir(where) pattern = re.compile(r"^{0}([0-9]+)\.pem$".format(kind)) matches = [pattern.match(f) for f in files] return sorted([int(m.groups()[0]) for m in matches if m]) - def newest_available_version(self, kind): + def newest_available_version(self, kind: str) -> int: """Newest available version of the specified kind of item? :param str kind: the lineage member item (``cert``, @@ -782,7 +812,7 @@ class RenewableCert(interfaces.RenewableCert): """ return max(self.available_versions(kind)) - def latest_common_version(self): + def latest_common_version(self) -> int: """Newest version for which all items are available? :returns: the newest available version for which all members @@ -797,7 +827,7 @@ class RenewableCert(interfaces.RenewableCert): versions = [self.available_versions(x) for x in ALL_FOUR] return max(n for n in versions[0] if all(n in v for v in versions[1:])) - def next_free_version(self): + def next_free_version(self) -> int: """Smallest version newer than all full or partial versions? :returns: the smallest version number that is larger than any @@ -811,7 +841,7 @@ class RenewableCert(interfaces.RenewableCert): # for the others. return max(self.newest_available_version(x) for x in ALL_FOUR) + 1 - def ensure_deployed(self): + def ensure_deployed(self) -> bool: """Make sure we've deployed the latest version. :returns: False if a change was needed, True otherwise @@ -826,8 +856,7 @@ class RenewableCert(interfaces.RenewableCert): return False return True - - def has_pending_deployment(self): + def has_pending_deployment(self) -> bool: """Is there a later version of all of the managed items? :returns: ``True`` if there is a complete version of this @@ -836,12 +865,18 @@ class RenewableCert(interfaces.RenewableCert): :rtype: bool """ + all_versions: List[int] = [] + for item in ALL_FOUR: + version = self.current_version(item) + if version is None: + raise errors.Error(f"{item} is required but missing for this certificate.") + all_versions.append(version) # TODO: consider whether to assume consistency or treat # inconsistent/consistent versions differently - smallest_current = min(self.current_version(x) for x in ALL_FOUR) + smallest_current = min(all_versions) return smallest_current < self.latest_common_version() - def _update_link_to(self, kind, version): + def _update_link_to(self, kind: str, version: int) -> None: """Make the specified item point at the specified version. (Note that this method doesn't verify that the specified version @@ -867,7 +902,7 @@ class RenewableCert(interfaces.RenewableCert): os.unlink(link) os.symlink(os.path.join(target_directory, filename), link) - def update_all_links_to(self, version): + def update_all_links_to(self, version: int) -> None: """Change all member objects to point to the specified version. :param int version: the desired version @@ -876,7 +911,10 @@ class RenewableCert(interfaces.RenewableCert): with error_handler.ErrorHandler(self._fix_symlinks): previous_links = self._previous_symlinks() for kind, link in previous_links: - os.symlink(self.current_target(kind), link) + target = self.current_target(kind) + if not target: + raise errors.Error(f"Target {kind} does not exist!") + os.symlink(target, link) for kind in ALL_FOUR: self._update_link_to(kind, version) @@ -884,7 +922,7 @@ class RenewableCert(interfaces.RenewableCert): for _, link in previous_links: os.unlink(link) - def names(self): + def names(self) -> List[str]: """What are the subject names of this certificate? :returns: the subject names @@ -895,11 +933,10 @@ class RenewableCert(interfaces.RenewableCert): target = self.current_target("cert") if target is None: raise errors.CertStorageError("could not find the certificate file") - with open(target) as f: - # TODO: Remove the cast once certbot package is fully typed - return crypto_util.get_names_from_cert(cast(bytes, f.read())) + with open(target, "rb") as f: + return crypto_util.get_names_from_cert(f.read()) - def ocsp_revoked(self, version): + def ocsp_revoked(self, version: int) -> bool: """Is the specified cert version revoked according to OCSP? Also returns True if the cert version is declared as revoked @@ -927,7 +964,7 @@ class RenewableCert(interfaces.RenewableCert): logger.debug(str(e)) return False - def autorenewal_is_enabled(self): + def autorenewal_is_enabled(self) -> bool: """Is automatic renewal enabled for this cert? If autorenew is not specified, defaults to True. @@ -939,7 +976,7 @@ class RenewableCert(interfaces.RenewableCert): return ("autorenew" not in self.configuration["renewalparams"] or self.configuration["renewalparams"].as_bool("autorenew")) - def should_autorenew(self): + def should_autorenew(self) -> bool: """Should we now try to autorenew the most recent cert version? This is a policy question and does not only depend on whether @@ -977,7 +1014,8 @@ class RenewableCert(interfaces.RenewableCert): return False @classmethod - def new_lineage(cls, lineagename, cert, privkey, chain, cli_config): + def new_lineage(cls, lineagename: str, cert: bytes, privkey: bytes, chain: bytes, + cli_config: configuration.NamespaceConfig) -> "RenewableCert": """Create a new certificate lineage. Attempts to create a certificate lineage -- enrolled for @@ -1072,7 +1110,7 @@ class RenewableCert(interfaces.RenewableCert): return cls(new_config.filename, cli_config) @property - def private_key_type(self): + def private_key_type(self) -> str: """ :returns: The type of algorithm for the private, RSA or ECDSA :rtype: str @@ -1088,8 +1126,8 @@ class RenewableCert(interfaces.RenewableCert): else: return "ECDSA" - def save_successor(self, prior_version, new_cert, - new_privkey, new_chain, cli_config): + def save_successor(self, prior_version: int, new_cert: bytes, new_privkey: bytes, + new_chain: bytes, cli_config: configuration.NamespaceConfig) -> int: """Save new cert and chain as a successor of a prior version. Returns the new version number that was created. diff --git a/certbot/certbot/_internal/updater.py b/certbot/certbot/_internal/updater.py index 6494f392e..214087550 100644 --- a/certbot/certbot/_internal/updater.py +++ b/certbot/certbot/_internal/updater.py @@ -1,14 +1,19 @@ """Updaters run at renewal""" import logging +from certbot import configuration from certbot import errors from certbot import interfaces +from certbot._internal import storage +from certbot._internal.plugins import disco as plugin_disco from certbot._internal.plugins import selection as plug_sel from certbot.plugins import enhancements logger = logging.getLogger(__name__) -def run_generic_updaters(config, lineage, plugins): + +def run_generic_updaters(config: configuration.NamespaceConfig, lineage: storage.RenewableCert, + plugins: plugin_disco.PluginsRegistry) -> None: """Run updaters that the plugin supports :param config: Configuration object @@ -35,7 +40,9 @@ def run_generic_updaters(config, lineage, plugins): _run_updaters(lineage, installer, config) _run_enhancement_updaters(lineage, installer, config) -def run_renewal_deployer(config, lineage, installer): + +def run_renewal_deployer(config: configuration.NamespaceConfig, lineage: storage.RenewableCert, + installer: interfaces.Installer) -> None: """Helper function to run deployer interface method if supported by the used installer plugin. @@ -60,7 +67,9 @@ def run_renewal_deployer(config, lineage, installer): installer.renew_deploy(lineage) _run_enhancement_deployers(lineage, installer, config) -def _run_updaters(lineage, installer, config): + +def _run_updaters(lineage: storage.RenewableCert, installer: interfaces.Installer, + config: configuration.NamespaceConfig) -> None: """Helper function to run the updater interface methods if supported by the used installer plugin. @@ -77,7 +86,9 @@ def _run_updaters(lineage, installer, config): if isinstance(installer, interfaces.GenericUpdater): installer.generic_updates(lineage) -def _run_enhancement_updaters(lineage, installer, config): + +def _run_enhancement_updaters(lineage: storage.RenewableCert, installer: interfaces.Installer, + config: configuration.NamespaceConfig) -> None: """Iterates through known enhancement interfaces. If the installer implements an enhancement interface and the enhance interface has an updater method, the updater method gets run. @@ -99,7 +110,8 @@ def _run_enhancement_updaters(lineage, installer, config): getattr(installer, enh["updater_function"])(lineage) -def _run_enhancement_deployers(lineage, installer, config): +def _run_enhancement_deployers(lineage: storage.RenewableCert, installer: interfaces.Installer, + config: configuration.NamespaceConfig) -> None: """Iterates through known enhancement interfaces. If the installer implements an enhancement interface and the enhance interface has an deployer method, the deployer method gets run. diff --git a/certbot/certbot/compat/filesystem.py b/certbot/certbot/compat/filesystem.py index faeb493df..bd832f7a2 100644 --- a/certbot/certbot/compat/filesystem.py +++ b/certbot/certbot/compat/filesystem.py @@ -5,15 +5,18 @@ import errno import os # pylint: disable=os-module-forbidden import stat import sys +from typing import Any +from typing import Dict from typing import List +from typing import Optional try: import ntsecuritycon - import win32security - import win32con - import win32api - import win32file import pywintypes + import win32api + import win32con + import win32file + import win32security import winerror except ImportError: POSIX_MODE = True @@ -28,7 +31,7 @@ else: # that could happen with this kind of pattern. class _WindowsUmask: """Store the current umask to apply on Windows""" - def __init__(self): + def __init__(self) -> None: self.mask = 0o022 @@ -531,7 +534,7 @@ def has_min_permissions(path: str, min_mode: int) -> bool: return True -def _win_is_executable(path): +def _win_is_executable(path: str) -> bool: if not os.path.isfile(path): return False @@ -547,7 +550,7 @@ def _win_is_executable(path): return mode & ntsecuritycon.FILE_GENERIC_EXECUTE == ntsecuritycon.FILE_GENERIC_EXECUTE -def _apply_win_mode(file_path, mode): +def _apply_win_mode(file_path: str, mode: int) -> None: """ This function converts the given POSIX mode into a Windows ACL list, and applies it to the file given its path. If the given path is a symbolic link, it will resolved to apply the @@ -566,7 +569,7 @@ def _apply_win_mode(file_path, mode): win32security.SetFileSecurity(file_path, win32security.DACL_SECURITY_INFORMATION, security) -def _generate_dacl(user_sid, mode, mask=None): +def _generate_dacl(user_sid: Any, mode: int, mask: Optional[int] = None) -> Any: if mask: mode = mode & (0o777 - mask) analysis = _analyze_mode(mode) @@ -602,7 +605,7 @@ def _generate_dacl(user_sid, mode, mask=None): return dacl -def _analyze_mode(mode): +def _analyze_mode(mode: int) -> Dict[str, Dict[str, int]]: return { 'user': { 'read': mode & stat.S_IRUSR, @@ -617,7 +620,7 @@ def _analyze_mode(mode): } -def _copy_win_ownership(src, dst): +def _copy_win_ownership(src: str, dst: str) -> None: # Resolve symbolic links src = realpath(src) @@ -632,7 +635,7 @@ def _copy_win_ownership(src, dst): win32security.SetFileSecurity(dst, win32security.OWNER_SECURITY_INFORMATION, security_dst) -def _copy_win_mode(src, dst): +def _copy_win_mode(src: str, dst: str) -> None: # Resolve symbolic links src = realpath(src) @@ -645,7 +648,7 @@ def _copy_win_mode(src, dst): win32security.SetFileSecurity(dst, win32security.DACL_SECURITY_INFORMATION, security_dst) -def _generate_windows_flags(rights_desc): +def _generate_windows_flags(rights_desc: Dict[str, int]) -> int: # Some notes about how each POSIX right is interpreted. # # For the rights read and execute, we have a pretty bijective relation between @@ -676,7 +679,7 @@ def _generate_windows_flags(rights_desc): return flag -def _check_win_mode(file_path, mode): +def _check_win_mode(file_path: str, mode: int) -> bool: # Resolve symbolic links file_path = realpath(file_path) # Get current dacl file @@ -698,7 +701,7 @@ def _check_win_mode(file_path, mode): return _compare_dacls(dacl, ref_dacl) -def _compare_dacls(dacl1, dacl2): +def _compare_dacls(dacl1: Any, dacl2: Any) -> bool: """ This method compare the two given DACLs to check if they are identical. Identical means here that they contains the same set of ACEs in the same order. @@ -707,7 +710,7 @@ def _compare_dacls(dacl1, dacl2): [dacl2.GetAce(index) for index in range(dacl2.GetAceCount())]) -def _get_current_user(): +def _get_current_user() -> Any: """ Return the pySID corresponding to the current user. """ diff --git a/certbot/certbot/compat/misc.py b/certbot/certbot/compat/misc.py index 3932981ac..8ca876962 100644 --- a/certbot/certbot/compat/misc.py +++ b/certbot/certbot/compat/misc.py @@ -8,17 +8,18 @@ import logging import select import subprocess import sys -import warnings from typing import Optional from typing import Tuple +import warnings from certbot import errors from certbot.compat import os try: - from win32com.shell import shell as shellwin32 - from win32console import GetStdHandle, STD_OUTPUT_HANDLE from pywintypes import error as pywinerror + from win32com.shell import shell as shellwin32 + from win32console import GetStdHandle + from win32console import STD_OUTPUT_HANDLE POSIX_MODE = False except ImportError: # pragma: no cover POSIX_MODE = True @@ -61,7 +62,7 @@ def prepare_virtual_console() -> None: logger.debug("Failed to set console mode", exc_info=True) -def readline_with_timeout(timeout: float, prompt: str) -> str: +def readline_with_timeout(timeout: float, prompt: Optional[str]) -> str: """ Read user input to return the first line entered, or raise after specified timeout. @@ -79,7 +80,7 @@ def readline_with_timeout(timeout: float, prompt: str) -> str: rlist, _, _ = select.select([sys.stdin], [], [], timeout) if not rlist: raise errors.Error( - "Timed out waiting for answer to prompt '{0}'".format(prompt)) + "Timed out waiting for answer to prompt '{0}'".format(prompt if prompt else "")) return rlist[0].readline() except OSError: # Windows specific diff --git a/certbot/certbot/configuration.py b/certbot/certbot/configuration.py index 4648cbda1..1a72cbce7 100644 --- a/certbot/certbot/configuration.py +++ b/certbot/certbot/configuration.py @@ -293,6 +293,13 @@ class NamespaceConfig: return os.path.join(self.renewal_hooks_dir, constants.RENEWAL_POST_HOOKS_DIR) + @property + def issuance_timeout(self) -> int: + """This option specifies how long (in seconds) Certbot will wait + for the server to issue a certificate. + """ + return self.namespace.issuance_timeout + # Magic methods def __deepcopy__(self, _memo: Any) -> 'NamespaceConfig': diff --git a/certbot/certbot/display/ops.py b/certbot/certbot/display/ops.py index 9cd458c3b..34b441c69 100644 --- a/certbot/certbot/display/ops.py +++ b/certbot/certbot/display/ops.py @@ -1,9 +1,18 @@ """Contains UI methods for LE user operations.""" import logging from textwrap import indent +from typing import Any +from typing import Callable +from typing import Iterable +from typing import List +from typing import Optional +from typing import Sequence +from typing import Tuple from certbot import errors +from certbot import interfaces from certbot import util +from certbot._internal import account from certbot._internal.display import util as internal_display_util from certbot.compat import os from certbot.display import util as display_util @@ -11,7 +20,7 @@ from certbot.display import util as display_util logger = logging.getLogger(__name__) -def get_email(invalid=False, optional=True): +def get_email(invalid: bool = False, optional: bool = True) -> str: """Prompt for valid email address. :param bool invalid: True if an invalid address was provided by the user @@ -65,7 +74,7 @@ def get_email(invalid=False, optional=True): invalid = bool(email) -def choose_account(accounts): +def choose_account(accounts: List[account.Account]) -> Optional[account.Account]: """Choose an account. :param list accounts: Containing at least one @@ -81,22 +90,25 @@ def choose_account(accounts): return None -def choose_values(values, question=None): +def choose_values(values: List[str], question: Optional[str] = None) -> List[str]: """Display screen to let user pick one or multiple values from the provided list. :param list values: Values to select from + :param str question: Question to ask to user while choosing values :returns: List of selected values :rtype: list """ - code, items = display_util.checklist(question, tags=values, force_interactive=True) + code, items = display_util.checklist(question if question else "", tags=values, + force_interactive=True) if code == display_util.OK and items: return items return [] -def choose_names(installer, question=None): +def choose_names(installer: Optional[interfaces.Installer], + question: Optional[str] = None) -> List[str]: """Display screen to select domains to validate. :param installer: An installer object @@ -125,7 +137,7 @@ def choose_names(installer, question=None): return [] -def get_valid_domains(domains): +def get_valid_domains(domains: Iterable[str]) -> List[str]: """Helper method for choose_names that implements basic checks on domain names @@ -133,7 +145,7 @@ def get_valid_domains(domains): :return: List of valid domains :rtype: list """ - valid_domains = [] + valid_domains: List[str] = [] for domain in domains: try: valid_domains.append(util.enforce_domain_sanity(domain)) @@ -142,7 +154,7 @@ def get_valid_domains(domains): return valid_domains -def _sort_names(FQDNs): +def _sort_names(FQDNs: Iterable[str]) -> List[str]: """Sort FQDNs by SLD (and if many, by their subdomains) :param list FQDNs: list of domain names @@ -153,7 +165,8 @@ def _sort_names(FQDNs): return sorted(FQDNs, key=lambda fqdn: fqdn.split('.')[::-1][1:]) -def _filter_names(names, override_question=None): +def _filter_names(names: Iterable[str], + override_question: Optional[str] = None) -> Tuple[str, List[str]]: """Determine which names the user would like to select from a list. :param list names: domain names @@ -175,7 +188,7 @@ def _filter_names(names, override_question=None): return code, [str(s) for s in names] -def _choose_names_manually(prompt_prefix=""): +def _choose_names_manually(prompt_prefix: str = "") -> List[str]: """Manually input names for those without an installer. :param str prompt_prefix: string to prepend to prompt for domains @@ -229,7 +242,7 @@ def _choose_names_manually(prompt_prefix=""): return [] -def success_installation(domains): +def success_installation(domains: Sequence[str]) -> None: """Display a box confirming the installation of HTTPS. :param list domains: domain names which were enabled @@ -241,7 +254,7 @@ def success_installation(domains): ) -def success_renewal(unused_domains): +def success_renewal(unused_domains: Sequence[str]) -> None: """Display a box confirming the renewal of an existing certificate. :param list domains: domain names which were renewed @@ -253,7 +266,7 @@ def success_renewal(unused_domains): ) -def success_revocation(cert_path): +def success_revocation(cert_path: str) -> None: """Display a message confirming a certificate has been revoked. :param list cert_path: path to certificate which was revoked. @@ -283,7 +296,7 @@ def report_executed_command(command_name: str, returncode: int, stdout: str, std logger.warning("%s ran with error output:\n%s", command_name, indent(err_s, ' ')) -def _gen_https_names(domains): +def _gen_https_names(domains: Sequence[str]) -> str: """Returns a string of the https domains. Domains are formatted nicely with ``https://`` prepended to each. @@ -304,7 +317,9 @@ def _gen_https_names(domains): return "" -def _get_validated(method, validator, message, default=None, **kwargs): +def _get_validated(method: Callable[..., Tuple[str, str]], + validator: Callable[[str], Any], message: str, + default: Optional[str] = None, **kwargs: Any) -> Tuple[str, str]: if default is not None: try: validator(default) @@ -331,7 +346,8 @@ def _get_validated(method, validator, message, default=None, **kwargs): return code, raw -def validated_input(validator, *args, **kwargs): +def validated_input(validator: Callable[[str], Any], + *args: Any, **kwargs: Any) -> Tuple[str, str]: """Like `~certbot.display.util.input_text`, but with validation. :param callable validator: A method which will be called on the @@ -345,7 +361,8 @@ def validated_input(validator, *args, **kwargs): return _get_validated(display_util.input_text, validator, *args, **kwargs) -def validated_directory(validator, *args, **kwargs): +def validated_directory(validator: Callable[[str], Any], + *args: Any, **kwargs: Any) -> Tuple[str, str]: """Like `~certbot.display.util.directory_select`, but with validation. :param callable validator: A method which will be called on the diff --git a/certbot/certbot/display/util.py b/certbot/certbot/display/util.py index ae4945c62..06c64b56e 100644 --- a/certbot/certbot/display/util.py +++ b/certbot/certbot/display/util.py @@ -11,6 +11,7 @@ Other messages can use the `logging` module. See `log.py`. """ import sys from types import ModuleType +from typing import Any from typing import cast from typing import List from typing import Optional @@ -18,7 +19,7 @@ from typing import Tuple from typing import Union import warnings - +from certbot._internal.display import obj # These specific imports from certbot._internal.display.obj and # certbot._internal.display.util are done to not break the public API of this # module. @@ -28,8 +29,6 @@ from certbot._internal.display.obj import SIDE_FRAME # pylint: disable=unused-i from certbot._internal.display.util import input_with_timeout # pylint: disable=unused-import from certbot._internal.display.util import separate_list_input # pylint: disable=unused-import from certbot._internal.display.util import summarize_domain_list # pylint: disable=unused-import -from certbot._internal.display import obj - # These constants are defined this way to make them easier to document with # Sphinx and to not couple our public docstrings to our internal ones. @@ -77,7 +76,7 @@ def notification(message: str, pause: bool = True, wrap: bool = True, force_interactive=force_interactive, decorate=decorate) -def menu(message: str, choices: Union[List[str], Tuple[str, str]], +def menu(message: str, choices: Union[List[str], List[Tuple[str, str]]], default: Optional[int] = None, cli_flag: Optional[str] = None, force_interactive: bool = False) -> Tuple[str, int]: """Display a menu. @@ -89,7 +88,7 @@ def menu(message: str, choices: Union[List[str], Tuple[str, 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 default: default value to return (if one exists) + :param default: default value to return, if interaction is not possible :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 @@ -110,7 +109,7 @@ def input_text(message: str, default: Optional[str] = None, cli_flag: Optional[s """Accept input from the user. :param str message: message to display to the user - :param default: default value to return (if one exists) + :param default: default value to return, if interaction is not possible :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 @@ -136,7 +135,7 @@ def yesno(message: str, yes_label: str = "Yes", no_label: str = "No", :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 default: default value to return, if interaction is not possible :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 @@ -149,14 +148,14 @@ def yesno(message: str, yes_label: str = "Yes", no_label: str = "No", cli_flag=cli_flag, force_interactive=force_interactive) -def checklist(message: str, tags: List[str], default: Optional[str] = None, +def checklist(message: str, tags: List[str], default: Optional[List[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 default: default value to return, if interaction is not possible :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 @@ -172,11 +171,11 @@ def checklist(message: str, tags: List[str], default: Optional[str] = None, def directory_select(message: str, default: Optional[str] = None, cli_flag: Optional[str] = None, - force_interactive: bool = False) -> Tuple[int, str]: + force_interactive: bool = False) -> Tuple[str, str]: """Display a directory selection screen. :param str message: prompt to give the user - :param default: default value to return (if one exists) + :param default: default value to return, if interaction is not possible :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 @@ -190,7 +189,7 @@ def directory_select(message: str, default: Optional[str] = None, cli_flag: Opti force_interactive=force_interactive) -def assert_valid_call(prompt, default, cli_flag, force_interactive): +def assert_valid_call(prompt: str, default: str, cli_flag: str, force_interactive: bool) -> None: """Verify that provided arguments is a valid display call. :param str prompt: prompt for the user @@ -215,10 +214,10 @@ class _DisplayUtilDeprecationModule: Internal class delegating to a module, and displaying warnings when attributes related to deprecated attributes in the certbot.display.util module. """ - def __init__(self, module): + def __init__(self, module: ModuleType) -> None: self.__dict__['_module'] = module - def __getattr__(self, attr): + def __getattr__(self, attr: str) -> Any: if attr in ('FileDisplay', 'NoninteractiveDisplay', 'SIDE_FRAME', 'input_with_timeout', 'separate_list_input', 'summarize_domain_list', 'WIDTH', 'HELP', 'ESC'): warnings.warn('{0} attribute in certbot.display.util module is deprecated ' @@ -226,13 +225,13 @@ class _DisplayUtilDeprecationModule: DeprecationWarning, stacklevel=2) return getattr(self._module, attr) - def __setattr__(self, attr, value): # pragma: no cover + def __setattr__(self, attr: str, value: Any) -> None: # pragma: no cover setattr(self._module, attr, value) - def __delattr__(self, attr): # pragma: no cover + def __delattr__(self, attr: str) -> None: # pragma: no cover delattr(self._module, attr) - def __dir__(self): # pragma: no cover + def __dir__(self) -> List[str]: # pragma: no cover return ['_module'] + dir(self._module) diff --git a/certbot/certbot/ocsp.py b/certbot/certbot/ocsp.py index 84068ebf1..51d486b6b 100644 --- a/certbot/certbot/ocsp.py +++ b/certbot/certbot/ocsp.py @@ -14,6 +14,7 @@ from cryptography.exceptions import UnsupportedAlgorithm from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives import serialization +from cryptography.x509 import ocsp import pytz import requests @@ -23,14 +24,6 @@ from certbot import util from certbot.compat.os import getenv from certbot.interfaces import RenewableCert # pylint: disable=unused-import -try: - # Only cryptography>=2.5 has ocsp module - # and signature_hash_algorithm attribute in OCSPResponse class - from cryptography.x509 import ocsp # pylint: disable=ungrouped-imports - getattr(ocsp.OCSPResponse, 'signature_hash_algorithm') -except (ImportError, AttributeError): # pragma: no cover - ocsp = None # type: ignore - logger = logging.getLogger(__name__) @@ -40,7 +33,7 @@ class RevocationChecker: def __init__(self, enforce_openssl_binary_usage: bool = False) -> None: self.broken = False - self.use_openssl_binary = enforce_openssl_binary_usage or not ocsp + self.use_openssl_binary = enforce_openssl_binary_usage if self.use_openssl_binary: if not util.exe_exists("openssl"): diff --git a/certbot/certbot/plugins/common.py b/certbot/certbot/plugins/common.py index 2a73c310a..99d059371 100644 --- a/certbot/certbot/plugins/common.py +++ b/certbot/certbot/plugins/common.py @@ -1,16 +1,25 @@ """Plugin common functions.""" from abc import ABCMeta from abc import abstractmethod +import argparse import logging import re import shutil import tempfile +from typing import Any +from typing import Callable +from typing import Iterable from typing import List +from typing import Optional +from typing import Set +from typing import Tuple import pkg_resources from certbot import achallenges +from certbot import configuration from certbot import crypto_util +from certbot import interfaces from certbot import errors from certbot import reverter from certbot._internal import constants @@ -23,12 +32,12 @@ from certbot.plugins.storage import PluginStorage logger = logging.getLogger(__name__) -def option_namespace(name): +def option_namespace(name: str) -> str: """ArgumentParser options namespace (prefix of all options).""" return name + "-" -def dest_namespace(name): +def dest_namespace(name: str) -> str: """ArgumentParser dest namespace (prefix of all destinations).""" return name.replace("-", "_") + "_" @@ -43,14 +52,14 @@ hostname_regex = re.compile( class Plugin(AbstractPlugin, metaclass=ABCMeta): """Generic plugin.""" - def __init__(self, config, name): + def __init__(self, config: configuration.NamespaceConfig, name: str) -> None: super().__init__(config, name) self.config = config self.name = name @classmethod @abstractmethod - def add_parser_arguments(cls, add): + def add_parser_arguments(cls, add: Callable[..., None]) -> None: """Add plugin arguments to the CLI argument parser. :param callable add: Function that proxies calls to @@ -60,40 +69,40 @@ class Plugin(AbstractPlugin, metaclass=ABCMeta): """ @classmethod - def inject_parser_options(cls, parser, name): + def inject_parser_options(cls, parser: argparse.ArgumentParser, name: str) -> None: """Inject parser options. See `~.certbot.interfaces.Plugin.inject_parser_options` for docs. """ # dummy function, doesn't check if dest.startswith(self.dest_namespace) - def add(arg_name_no_prefix, *args, **kwargs): - return parser.add_argument( + def add(arg_name_no_prefix: str, *args: Any, **kwargs: Any) -> None: + parser.add_argument( "--{0}{1}".format(option_namespace(name), arg_name_no_prefix), *args, **kwargs) return cls.add_parser_arguments(add) @property - def option_namespace(self): + def option_namespace(self) -> str: """ArgumentParser options namespace (prefix of all options).""" return option_namespace(self.name) - def option_name(self, name): + def option_name(self, name: str) -> str: """Option name (include plugin namespace).""" return self.option_namespace + name @property - def dest_namespace(self): + def dest_namespace(self) -> str: """ArgumentParser dest namespace (prefix of all destinations).""" return dest_namespace(self.name) - def dest(self, var): + def dest(self, var: str) -> str: """Find a destination for given variable ``var``.""" # this should do exactly the same what ArgumentParser(arg), # does to "arg" to compute "dest" return self.dest_namespace + var.replace("-", "_") - def conf(self, var): + def conf(self, var: str) -> Any: """Find a configuration value for variable ``var``.""" return getattr(self.config, self.dest(var)) @@ -130,12 +139,13 @@ class Installer(AbstractInstaller, Plugin, metaclass=ABCMeta): Installer plugins do not have to inherit from this class. """ - def __init__(self, *args, **kwargs): + def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) self.storage = PluginStorage(self.config, self.name) self.reverter = reverter.Reverter(self.config) - def add_to_checkpoint(self, save_files, save_notes, temporary=False): + def add_to_checkpoint(self, save_files: Set[str], save_notes: str, + temporary: bool = False) -> None: """Add files to a checkpoint. :param set save_files: set of filepaths to save @@ -157,7 +167,7 @@ class Installer(AbstractInstaller, Plugin, metaclass=ABCMeta): except errors.ReverterError as err: raise errors.PluginError(str(err)) - def finalize_checkpoint(self, title): + def finalize_checkpoint(self, title: str) -> None: """Timestamp and save changes made through the reverter. :param str title: Title describing checkpoint @@ -170,7 +180,7 @@ class Installer(AbstractInstaller, Plugin, metaclass=ABCMeta): except errors.ReverterError as err: raise errors.PluginError(str(err)) - def recovery_routine(self): + def recovery_routine(self) -> None: """Revert all previously modified files. Reverts all modified files that have not been saved as a checkpoint @@ -183,7 +193,7 @@ class Installer(AbstractInstaller, Plugin, metaclass=ABCMeta): except errors.ReverterError as err: raise errors.PluginError(str(err)) - def revert_temporary_config(self): + def revert_temporary_config(self) -> None: """Rollback temporary checkpoint. :raises .errors.PluginError: when unable to revert config @@ -194,7 +204,7 @@ class Installer(AbstractInstaller, Plugin, metaclass=ABCMeta): except errors.ReverterError as err: raise errors.PluginError(str(err)) - def rollback_checkpoints(self, rollback=1): + def rollback_checkpoints(self, rollback: int = 1) -> None: """Rollback saved checkpoints. :param int rollback: Number of checkpoints to revert @@ -209,24 +219,31 @@ class Installer(AbstractInstaller, Plugin, metaclass=ABCMeta): raise errors.PluginError(str(err)) @property - def ssl_dhparams(self): + def ssl_dhparams(self) -> str: """Full absolute path to ssl_dhparams file.""" return os.path.join(self.config.config_dir, constants.SSL_DHPARAMS_DEST) @property - def updated_ssl_dhparams_digest(self): + def updated_ssl_dhparams_digest(self) -> str: """Full absolute path to digest of updated ssl_dhparams file.""" return os.path.join(self.config.config_dir, constants.UPDATED_SSL_DHPARAMS_DIGEST) - def install_ssl_dhparams(self): + def install_ssl_dhparams(self) -> None: """Copy Certbot's ssl_dhparams file into the system's config dir if required.""" - return install_version_controlled_file( + install_version_controlled_file( self.ssl_dhparams, self.updated_ssl_dhparams_digest, constants.SSL_DHPARAMS_SRC, constants.ALL_SSL_DHPARAMS_HASHES) +class Configurator(Installer, interfaces.Authenticator, metaclass=ABCMeta): + """ + A plugin that extends certbot.plugins.common.Installer + and implements certbot.interfaces.Authenticator + """ + + class Addr: r"""Represents an virtual host address. @@ -234,12 +251,12 @@ class Addr: :param str port: port number or \*, or "" """ - def __init__(self, tup, ipv6=False): + def __init__(self, tup: Tuple[str, str], ipv6: bool = False): self.tup = tup self.ipv6 = ipv6 @classmethod - def fromstring(cls, str_addr): + def fromstring(cls, str_addr: str) -> 'Addr': """Initialize Addr from string.""" if str_addr.startswith('['): # ipv6 addresses starts with [ @@ -253,19 +270,19 @@ class Addr: tup = str_addr.partition(':') return cls((tup[0], tup[2])) - def __str__(self): + def __str__(self) -> str: if self.tup[1]: return "%s:%s" % self.tup return self.tup[0] - def normalized_tuple(self): + def normalized_tuple(self) -> Tuple[str, str]: """Normalized representation of addr/port tuple """ if self.ipv6: - return (self.get_ipv6_exploded(), self.tup[1]) + return self.get_ipv6_exploded(), self.tup[1] return self.tup - def __eq__(self, other): + def __eq__(self, other: Any) -> bool: if isinstance(other, self.__class__): # compare normalized to take different # styles of representation into account @@ -273,34 +290,34 @@ class Addr: return False - def __hash__(self): + def __hash__(self) -> int: return hash(self.tup) - def get_addr(self): + def get_addr(self) -> str: """Return addr part of Addr object.""" return self.tup[0] - def get_port(self): + def get_port(self) -> str: """Return port.""" return self.tup[1] - def get_addr_obj(self, port): + def get_addr_obj(self, port: str) -> 'Addr': """Return new address object with same addr and new port.""" return self.__class__((self.tup[0], port), self.ipv6) - def _normalize_ipv6(self, addr): + def _normalize_ipv6(self, addr: str) -> List[str]: """Return IPv6 address in normalized form, helper function""" addr = addr.lstrip("[") addr = addr.rstrip("]") return self._explode_ipv6(addr) - def get_ipv6_exploded(self): + def get_ipv6_exploded(self) -> str: """Return IPv6 in normalized form""" if self.ipv6: return ":".join(self._normalize_ipv6(self.tup[0])) return "" - def _explode_ipv6(self, addr): + def _explode_ipv6(self, addr: str) -> List[str]: """Explode IPv6 address for comparison""" result = ['0', '0', '0', '0', '0', '0', '0', '0'] addr_list = addr.split(":") @@ -337,12 +354,13 @@ class ChallengePerformer: """ - def __init__(self, configurator): + def __init__(self, configurator: Configurator): self.configurator = configurator self.achalls: List[achallenges.KeyAuthorizationAnnotatedChallenge] = [] self.indices: List[int] = [] - def add_chall(self, achall, idx=None): + def add_chall(self, achall: achallenges.KeyAuthorizationAnnotatedChallenge, + idx: Optional[int] = None) -> None: """Store challenge to be performed when perform() is called. :param .KeyAuthorizationAnnotatedChallenge achall: Annotated @@ -354,7 +372,7 @@ class ChallengePerformer: if idx is not None: self.indices.append(idx) - def perform(self): + def perform(self) -> List[achallenges.KeyAuthorizationAnnotatedChallenge]: """Perform all added challenges. :returns: challenge responses @@ -365,7 +383,8 @@ class ChallengePerformer: raise NotImplementedError() -def install_version_controlled_file(dest_path, digest_path, src_path, all_hashes): +def install_version_controlled_file(dest_path: str, digest_path: str, src_path: str, + all_hashes: Iterable[str]) -> None: """Copy a file into an active location (likely the system's config dir) if required. :param str dest_path: destination path for version controlled file @@ -375,11 +394,11 @@ def install_version_controlled_file(dest_path, digest_path, src_path, all_hashes """ current_hash = crypto_util.sha256sum(src_path) - def _write_current_hash(): - with open(digest_path, "w") as f: - f.write(current_hash) + def _write_current_hash() -> None: + with open(digest_path, "w") as file_h: + file_h.write(current_hash) - def _install_current_file(): + def _install_current_file() -> None: shutil.copyfile(src_path, dest_path) _write_current_hash() @@ -415,9 +434,9 @@ def install_version_controlled_file(dest_path, digest_path, src_path, all_hashes # "pragma: no cover") TODO: this might quickly lead to dead code (also # c.f. #383) -def dir_setup(test_dir, pkg): # pragma: no cover +def dir_setup(test_dir: str, pkg: str) -> Tuple[str, str, str]: # pragma: no cover """Setup the directories necessary for the configurator.""" - def expanded_tempdir(prefix): + def expanded_tempdir(prefix: str) -> str: """Return the real path of a temp directory with the specified prefix Some plugins rely on real paths of symlinks for working correctly. For diff --git a/certbot/certbot/plugins/dns_common.py b/certbot/certbot/plugins/dns_common.py index 3455e8682..9d5b2e614 100644 --- a/certbot/certbot/plugins/dns_common.py +++ b/certbot/certbot/plugins/dns_common.py @@ -1,12 +1,19 @@ """Common code for DNS Authenticator Plugins.""" - import abc import logging from time import sleep +from typing import Callable +from typing import Iterable +from typing import List +from typing import Mapping +from typing import Optional +from typing import Type import configobj from acme import challenges +from certbot import achallenges +from certbot import configuration from certbot import errors from certbot import interfaces from certbot.compat import filesystem @@ -21,20 +28,21 @@ logger = logging.getLogger(__name__) class DNSAuthenticator(common.Plugin, interfaces.Authenticator, metaclass=abc.ABCMeta): """Base class for DNS Authenticators""" - def __init__(self, config, name): + def __init__(self, config: configuration.NamespaceConfig, name: str) -> None: super().__init__(config, name) self._attempt_cleanup = False @classmethod - def add_parser_arguments(cls, add, default_propagation_seconds=10): # pylint: disable=arguments-differ + def add_parser_arguments(cls, add: Callable[..., None], # pylint: disable=arguments-differ + default_propagation_seconds: int = 10) -> None: add('propagation-seconds', default=default_propagation_seconds, type=int, help='The number of seconds to wait for DNS to propagate before asking the ACME server ' 'to verify the DNS record.') - def auth_hint(self, failed_achalls): + def auth_hint(self, failed_achalls: List[achallenges.AnnotatedChallenge]) -> str: """See certbot.plugins.common.Plugin.auth_hint.""" delay = self.conf('propagation-seconds') return ( @@ -44,16 +52,17 @@ class DNSAuthenticator(common.Plugin, interfaces.Authenticator, metaclass=abc.AB .format(name=self.name, secs=delay, suffix='s' if delay != 1 else '') ) - def get_chall_pref(self, unused_domain): # pylint: disable=missing-function-docstring + def get_chall_pref(self, unused_domain: str) -> Iterable[Type[challenges.Challenge]]: # pylint: disable=missing-function-docstring return [challenges.DNS01] - def prepare(self): # pylint: disable=missing-function-docstring + def prepare(self) -> None: # pylint: disable=missing-function-docstring pass def more_info(self) -> str: # pylint: disable=missing-function-docstring raise NotImplementedError() - def perform(self, achalls): # pylint: disable=missing-function-docstring + def perform(self, achalls: List[achallenges.AnnotatedChallenge] + ) -> List[challenges.ChallengeResponse]: # pylint: disable=missing-function-docstring self._setup_credentials() self._attempt_cleanup = True @@ -76,7 +85,7 @@ class DNSAuthenticator(common.Plugin, interfaces.Authenticator, metaclass=abc.AB return responses - def cleanup(self, achalls): # pylint: disable=missing-function-docstring + def cleanup(self, achalls: List[achallenges.AnnotatedChallenge]) -> None: # pylint: disable=missing-function-docstring if self._attempt_cleanup: for achall in achalls: domain = achall.domain @@ -86,14 +95,15 @@ class DNSAuthenticator(common.Plugin, interfaces.Authenticator, metaclass=abc.AB self._cleanup(domain, validation_domain_name, validation) @abc.abstractmethod - def _setup_credentials(self): # pragma: no cover + def _setup_credentials(self) -> None: # pragma: no cover """ Establish credentials, prompting if necessary. """ raise NotImplementedError() @abc.abstractmethod - def _perform(self, domain, validation_name, validation): # pragma: no cover + def _perform(self, domain: str, validation_name: str, + validation: str) -> None: # pragma: no cover """ Performs a dns-01 challenge by creating a DNS TXT record. @@ -105,7 +115,8 @@ class DNSAuthenticator(common.Plugin, interfaces.Authenticator, metaclass=abc.AB raise NotImplementedError() @abc.abstractmethod - def _cleanup(self, domain, validation_name, validation): # pragma: no cover + def _cleanup(self, domain: str, validation_name: str, + validation: str) -> None: # pragma: no cover """ Deletes the DNS TXT record which would have been created by `_perform_achall`. @@ -117,7 +128,7 @@ class DNSAuthenticator(common.Plugin, interfaces.Authenticator, metaclass=abc.AB """ raise NotImplementedError() - def _configure(self, key, label): + def _configure(self, key: str, label: str) -> None: """ Ensure that a configuration value is available. @@ -133,7 +144,8 @@ class DNSAuthenticator(common.Plugin, interfaces.Authenticator, metaclass=abc.AB setattr(self.config, self.dest(key), new_value) - def _configure_file(self, key, label, validator=None): + def _configure_file(self, key: str, label: str, + validator: Optional[Callable[[str], None]] = None) -> None: """ Ensure that a configuration value is available for a path. @@ -149,8 +161,10 @@ class DNSAuthenticator(common.Plugin, interfaces.Authenticator, metaclass=abc.AB setattr(self.config, self.dest(key), os.path.abspath(os.path.expanduser(new_value))) - def _configure_credentials(self, key, label, required_variables=None, - validator=None) -> 'CredentialsConfiguration': + def _configure_credentials( + self, key: str, label: str, required_variables: Optional[Mapping[str, str]] = None, + validator: Optional[Callable[['CredentialsConfiguration'], None]] = None + ) -> 'CredentialsConfiguration': """ As `_configure_file`, but for a credential configuration file. @@ -167,14 +181,14 @@ class DNSAuthenticator(common.Plugin, interfaces.Authenticator, metaclass=abc.AB indicate any issue. """ - def __validator(filename): # pylint: disable=unused-private-member - configuration = CredentialsConfiguration(filename, self.dest) + def __validator(filename: str) -> None: # pylint: disable=unused-private-member + applied_configuration = CredentialsConfiguration(filename, self.dest) if required_variables: - configuration.require(required_variables) + applied_configuration.require(required_variables) if validator: - validator(configuration) + validator(applied_configuration) self._configure_file(key, label, __validator) @@ -188,7 +202,7 @@ class DNSAuthenticator(common.Plugin, interfaces.Authenticator, metaclass=abc.AB return credentials_configuration @staticmethod - def _prompt_for_data(label): + def _prompt_for_data(label: str) -> str: """ Prompt the user for a piece of information. @@ -197,7 +211,7 @@ class DNSAuthenticator(common.Plugin, interfaces.Authenticator, metaclass=abc.AB :rtype: str """ - def __validator(i): # pylint: disable=unused-private-member + def __validator(i: str) -> None: # pylint: disable=unused-private-member if not i: raise errors.PluginError('Please enter your {0}.'.format(label)) @@ -211,7 +225,7 @@ class DNSAuthenticator(common.Plugin, interfaces.Authenticator, metaclass=abc.AB raise errors.PluginError('{0} required to proceed.'.format(label)) @staticmethod - def _prompt_for_file(label, validator=None): + def _prompt_for_file(label: str, validator: Optional[Callable[[str], None]] = None) -> str: """ Prompt the user for a path. @@ -223,7 +237,7 @@ class DNSAuthenticator(common.Plugin, interfaces.Authenticator, metaclass=abc.AB :rtype: str """ - def __validator(filename): # pylint: disable=unused-private-member + def __validator(filename: str) -> None: # pylint: disable=unused-private-member if not filename: raise errors.PluginError('Please enter a valid path to your {0}.'.format(label)) @@ -247,7 +261,7 @@ class DNSAuthenticator(common.Plugin, interfaces.Authenticator, metaclass=abc.AB class CredentialsConfiguration: """Represents a user-supplied filed which stores API credentials.""" - def __init__(self, filename, mapper=lambda x: x): + def __init__(self, filename: str, mapper: Callable[[str], str] = lambda x: x) -> None: """ :param str filename: A path to the configuration file. :param callable mapper: A transformation to apply to configuration key names @@ -263,7 +277,7 @@ class CredentialsConfiguration: self.mapper = mapper - def require(self, required_variables): + def require(self, required_variables: Mapping[str, str]) -> None: """Ensures that the supplied set of variables are all present in the file. :param dict required_variables: Map of variable which must be present to error to display. @@ -288,7 +302,7 @@ class CredentialsConfiguration: ) ) - def conf(self, var): + def conf(self, var: str) -> str: """Find a configuration value for variable `var`, as transformed by `mapper`. :param str var: The variable to get. @@ -298,14 +312,14 @@ class CredentialsConfiguration: return self._get(var) - def _has(self, var): + def _has(self, var: str) -> bool: return self.mapper(var) in self.confobj - def _get(self, var): + def _get(self, var: str) -> str: return self.confobj.get(self.mapper(var)) -def validate_file(filename): +def validate_file(filename: str) -> None: """Ensure that the specified file exists.""" if not os.path.exists(filename): @@ -315,7 +329,7 @@ def validate_file(filename): raise errors.PluginError('Path is a directory: {0}'.format(filename)) -def validate_file_permissions(filename): +def validate_file_permissions(filename: str) -> None: """Ensure that the specified file exists and warn about unsafe permissions.""" validate_file(filename) @@ -324,7 +338,7 @@ def validate_file_permissions(filename): logger.warning('Unsafe permissions on credentials configuration file: %s', filename) -def base_domain_name_guesses(domain): +def base_domain_name_guesses(domain: str) -> List[str]: """Return a list of progressively less-specific domain names. One of these will probably be the domain name known to the DNS provider. diff --git a/certbot/certbot/plugins/dns_common_lexicon.py b/certbot/certbot/plugins/dns_common_lexicon.py index cede6a4fd..2fd323184 100644 --- a/certbot/certbot/plugins/dns_common_lexicon.py +++ b/certbot/certbot/plugins/dns_common_lexicon.py @@ -2,6 +2,8 @@ import logging from typing import Any from typing import Dict +from typing import Mapping +from typing import Optional from typing import Union from requests.exceptions import HTTPError @@ -30,10 +32,10 @@ class LexiconClient: Encapsulates all communication with a DNS provider via Lexicon. """ - def __init__(self): + def __init__(self) -> None: self.provider: Provider - def add_txt_record(self, domain, record_name, record_content): + def add_txt_record(self, domain: str, record_name: str, record_content: str) -> None: """ Add a TXT record using the supplied information. @@ -50,7 +52,7 @@ class LexiconClient: logger.debug('Encountered error adding TXT record: %s', e, exc_info=True) raise errors.PluginError('Error adding TXT record: {0}'.format(e)) - def del_txt_record(self, domain, record_name, record_content): + def del_txt_record(self, domain: str, record_name: str, record_content: str) -> None: """ Delete a TXT record using the supplied information. @@ -71,7 +73,7 @@ class LexiconClient: except RequestException as e: logger.debug('Encountered error deleting TXT record: %s', e, exc_info=True) - def _find_domain_id(self, domain): + def _find_domain_id(self, domain: str) -> None: """ Find the domain_id for a given domain. @@ -94,24 +96,24 @@ class LexiconClient: return # If `authenticate` doesn't throw an exception, we've found the right name except HTTPError as e: - result = self._handle_http_error(e, domain_name) + result1 = self._handle_http_error(e, domain_name) - if result: - raise result + if result1: + raise result1 except Exception as e: # pylint: disable=broad-except - result = self._handle_general_error(e, domain_name) + result2 = self._handle_general_error(e, domain_name) - if result: - raise result # pylint: disable=raising-bad-type + if result2: + raise result2 # pylint: disable=raising-bad-type raise errors.PluginError('Unable to determine zone identifier for {0} using zone names: {1}' .format(domain, domain_name_guesses)) - def _handle_http_error(self, e, domain_name): + def _handle_http_error(self, e: HTTPError, domain_name: str) -> errors.PluginError: return errors.PluginError('Error determining zone identifier for {0}: {1}.' .format(domain_name, e)) - def _handle_general_error(self, e, domain_name): + def _handle_general_error(self, e: Exception, domain_name: str) -> Optional[errors.PluginError]: if not str(e).startswith('No domain found'): return errors.PluginError('Unexpected error determining zone identifier for {0}: {1}' .format(domain_name, e)) @@ -119,8 +121,8 @@ class LexiconClient: def build_lexicon_config(lexicon_provider_name: str, - lexicon_options: Dict, provider_options: Dict - ) -> Union[ConfigResolver, Dict]: + lexicon_options: Mapping[str, Any], provider_options: Mapping[str, Any] + ) -> Union[ConfigResolver, Dict[str, Any]]: """ Convenient function to build a Lexicon 2.x/3.x config object. :param str lexicon_provider_name: the name of the lexicon provider to use @@ -129,14 +131,14 @@ def build_lexicon_config(lexicon_provider_name: str, :return: configuration to apply to the provider :rtype: ConfigurationResolver or dict """ - config: Dict[str, Any] = {'provider_name': lexicon_provider_name} + config: Union[ConfigResolver, Dict[str, Any]] = {'provider_name': lexicon_provider_name} config.update(lexicon_options) if not ConfigResolver: # Lexicon 2.x config.update(provider_options) else: # Lexicon 3.x - provider_config = {} + provider_config: Dict[str, Any] = {} provider_config.update(provider_options) config[lexicon_provider_name] = provider_config config = ConfigResolver().with_dict(config).with_env() diff --git a/certbot/certbot/plugins/dns_test_common.py b/certbot/certbot/plugins/dns_test_common.py index 64e07d063..ae8a69b79 100644 --- a/certbot/certbot/plugins/dns_test_common.py +++ b/certbot/certbot/plugins/dns_test_common.py @@ -1,5 +1,7 @@ """Base test class for DNS authenticators.""" -import typing +from typing import Any +from typing import Mapping +from typing import TYPE_CHECKING import configobj import josepy as jose @@ -11,13 +13,12 @@ from certbot.plugins.dns_common import DNSAuthenticator from certbot.tests import acme_util from certbot.tests import util as test_util -if typing.TYPE_CHECKING: +if TYPE_CHECKING: from typing_extensions import Protocol else: Protocol = object - try: import mock except ImportError: # pragma: no cover @@ -32,14 +33,14 @@ class _AuthenticatorCallableTestCase(Protocol): """Protocol describing a TestCase able to call a real DNSAuthenticator instance.""" auth: DNSAuthenticator - def assertTrue(self, *unused_args) -> None: + def assertTrue(self, *unused_args: Any) -> None: """ See https://docs.python.org/3/library/unittest.html#unittest.TestCase.assertTrue """ ... - def assertEqual(self, *unused_args) -> None: + def assertEqual(self, *unused_args: Any) -> None: """ See https://docs.python.org/3/library/unittest.html#unittest.TestCase.assertEqual @@ -59,20 +60,20 @@ class BaseAuthenticatorTest: achall = achallenges.KeyAuthorizationAnnotatedChallenge( challb=acme_util.DNS01, domain=DOMAIN, account_key=KEY) - def test_more_info(self: _AuthenticatorCallableTestCase): + def test_more_info(self: _AuthenticatorCallableTestCase) -> None: self.assertTrue(isinstance(self.auth.more_info(), str)) # pylint: disable=no-member - def test_get_chall_pref(self: _AuthenticatorCallableTestCase): - self.assertEqual(self.auth.get_chall_pref(None), [challenges.DNS01]) # pylint: disable=no-member + def test_get_chall_pref(self: _AuthenticatorCallableTestCase) -> None: + self.assertEqual(self.auth.get_chall_pref("example.org"), [challenges.DNS01]) # pylint: disable=no-member - def test_parser_arguments(self: _AuthenticatorCallableTestCase): + def test_parser_arguments(self: _AuthenticatorCallableTestCase) -> None: m = mock.MagicMock() self.auth.add_parser_arguments(m) # pylint: disable=no-member m.assert_any_call('propagation-seconds', type=int, default=mock.ANY, help=mock.ANY) -def write(values, path): +def write(values: Mapping[str, Any], path: str) -> None: """Write the specified values to a config file. :param dict values: A map of values to write. diff --git a/certbot/certbot/plugins/dns_test_common_lexicon.py b/certbot/certbot/plugins/dns_test_common_lexicon.py index f4beee64e..df91dac1f 100644 --- a/certbot/certbot/plugins/dns_test_common_lexicon.py +++ b/certbot/certbot/plugins/dns_test_common_lexicon.py @@ -1,5 +1,6 @@ """Base test class for DNS authenticators built on Lexicon.""" -import typing +from typing import Any +from typing import TYPE_CHECKING from unittest.mock import MagicMock import josepy as jose @@ -17,14 +18,11 @@ try: import mock except ImportError: # pragma: no cover from unittest import mock # type: ignore -if typing.TYPE_CHECKING: +if TYPE_CHECKING: from typing_extensions import Protocol else: Protocol = object - - - DOMAIN = 'example.com' KEY = jose.JWKRSA.load(test_util.load_vector("rsa512_key.pem")) @@ -54,7 +52,7 @@ class _LexiconAwareTestCase(Protocol): LOGIN_ERROR: Exception UNKNOWN_LOGIN_ERROR: Exception - def assertRaises(self, *unused_args) -> None: + def assertRaises(self, *unused_args: Any) -> None: """ See https://docs.python.org/3/library/unittest.html#unittest.TestCase.assertRaises @@ -68,13 +66,14 @@ class _LexiconAwareTestCase(Protocol): class BaseLexiconAuthenticatorTest(dns_test_common.BaseAuthenticatorTest): @test_util.patch_display_util() - def test_perform(self: _AuthenticatorCallableLexiconTestCase, unused_mock_get_utility): + def test_perform(self: _AuthenticatorCallableLexiconTestCase, + unused_mock_get_utility: Any) -> None: self.auth.perform([self.achall]) expected = [mock.call.add_txt_record(DOMAIN, '_acme-challenge.'+DOMAIN, mock.ANY)] self.assertEqual(expected, self.mock_client.mock_calls) - def test_cleanup(self: _AuthenticatorCallableLexiconTestCase): + def test_cleanup(self: _AuthenticatorCallableLexiconTestCase) -> None: self.auth._attempt_cleanup = True # _attempt_cleanup | pylint: disable=protected-access self.auth.cleanup([self.achall]) @@ -92,14 +91,14 @@ class BaseLexiconClientTest: record_name = record_prefix + "." + DOMAIN record_content = "bar" - def test_add_txt_record(self: _LexiconAwareTestCase): + def test_add_txt_record(self: _LexiconAwareTestCase) -> None: self.client.add_txt_record(DOMAIN, self.record_name, self.record_content) self.provider_mock.create_record.assert_called_with(rtype='TXT', name=self.record_name, content=self.record_content) - def test_add_txt_record_try_twice_to_find_domain(self: _LexiconAwareTestCase): + def test_add_txt_record_try_twice_to_find_domain(self: _LexiconAwareTestCase) -> None: self.provider_mock.authenticate.side_effect = [self.DOMAIN_NOT_FOUND, ''] self.client.add_txt_record(DOMAIN, self.record_name, self.record_content) @@ -108,7 +107,7 @@ class BaseLexiconClientTest: name=self.record_name, content=self.record_content) - def test_add_txt_record_fail_to_find_domain(self: _LexiconAwareTestCase): + def test_add_txt_record_fail_to_find_domain(self: _LexiconAwareTestCase) -> None: self.provider_mock.authenticate.side_effect = [self.DOMAIN_NOT_FOUND, self.DOMAIN_NOT_FOUND, self.DOMAIN_NOT_FOUND,] @@ -117,64 +116,66 @@ class BaseLexiconClientTest: self.client.add_txt_record, DOMAIN, self.record_name, self.record_content) - def test_add_txt_record_fail_to_authenticate(self: _LexiconAwareTestCase): + def test_add_txt_record_fail_to_authenticate(self: _LexiconAwareTestCase) -> None: self.provider_mock.authenticate.side_effect = self.LOGIN_ERROR self.assertRaises(errors.PluginError, self.client.add_txt_record, DOMAIN, self.record_name, self.record_content) - def test_add_txt_record_fail_to_authenticate_with_unknown_error(self: _LexiconAwareTestCase): + def test_add_txt_record_fail_to_authenticate_with_unknown_error( + self: _LexiconAwareTestCase) -> None: self.provider_mock.authenticate.side_effect = self.UNKNOWN_LOGIN_ERROR self.assertRaises(errors.PluginError, self.client.add_txt_record, DOMAIN, self.record_name, self.record_content) - def test_add_txt_record_error_finding_domain(self: _LexiconAwareTestCase): + def test_add_txt_record_error_finding_domain(self: _LexiconAwareTestCase) -> None: self.provider_mock.authenticate.side_effect = self.GENERIC_ERROR self.assertRaises(errors.PluginError, self.client.add_txt_record, DOMAIN, self.record_name, self.record_content) - def test_add_txt_record_error_adding_record(self: _LexiconAwareTestCase): + def test_add_txt_record_error_adding_record(self: _LexiconAwareTestCase) -> None: self.provider_mock.create_record.side_effect = self.GENERIC_ERROR self.assertRaises(errors.PluginError, self.client.add_txt_record, DOMAIN, self.record_name, self.record_content) - def test_del_txt_record(self: _LexiconAwareTestCase): + def test_del_txt_record(self: _LexiconAwareTestCase) -> None: self.client.del_txt_record(DOMAIN, self.record_name, self.record_content) self.provider_mock.delete_record.assert_called_with(rtype='TXT', name=self.record_name, content=self.record_content) - def test_del_txt_record_fail_to_find_domain(self: _LexiconAwareTestCase): + def test_del_txt_record_fail_to_find_domain(self: _LexiconAwareTestCase) -> None: self.provider_mock.authenticate.side_effect = [self.DOMAIN_NOT_FOUND, self.DOMAIN_NOT_FOUND, self.DOMAIN_NOT_FOUND, ] self.client.del_txt_record(DOMAIN, self.record_name, self.record_content) - def test_del_txt_record_fail_to_authenticate(self: _LexiconAwareTestCase): + def test_del_txt_record_fail_to_authenticate(self: _LexiconAwareTestCase) -> None: self.provider_mock.authenticate.side_effect = self.LOGIN_ERROR self.client.del_txt_record(DOMAIN, self.record_name, self.record_content) - def test_del_txt_record_fail_to_authenticate_with_unknown_error(self: _LexiconAwareTestCase): + def test_del_txt_record_fail_to_authenticate_with_unknown_error( + self: _LexiconAwareTestCase) -> None: self.provider_mock.authenticate.side_effect = self.UNKNOWN_LOGIN_ERROR self.client.del_txt_record(DOMAIN, self.record_name, self.record_content) - def test_del_txt_record_error_finding_domain(self: _LexiconAwareTestCase): + def test_del_txt_record_error_finding_domain(self: _LexiconAwareTestCase) -> None: self.provider_mock.authenticate.side_effect = self.GENERIC_ERROR self.client.del_txt_record(DOMAIN, self.record_name, self.record_content) - def test_del_txt_record_error_deleting_record(self: _LexiconAwareTestCase): + def test_del_txt_record_error_deleting_record(self: _LexiconAwareTestCase) -> None: self.provider_mock.delete_record.side_effect = self.GENERIC_ERROR self.client.del_txt_record(DOMAIN, self.record_name, self.record_content) diff --git a/certbot/certbot/plugins/enhancements.py b/certbot/certbot/plugins/enhancements.py index 3f02b7272..4bd272ea3 100644 --- a/certbot/certbot/plugins/enhancements.py +++ b/certbot/certbot/plugins/enhancements.py @@ -1,9 +1,15 @@ """New interface style Certbot enhancements""" import abc from typing import Any +from typing import Callable from typing import Dict +from typing import Generator +from typing import Iterable from typing import List +from typing import Optional +from certbot import configuration +from certbot import interfaces from certbot._internal import constants ENHANCEMENTS = ["redirect", "ensure-http-header", "ocsp-stapling"] @@ -17,7 +23,9 @@ List of expected options parameters: """ -def enabled_enhancements(config): + +def enabled_enhancements( + config: configuration.NamespaceConfig) -> Generator[Dict[str, Any], None, None]: """ Generator to yield the enabled new style enhancements. @@ -28,7 +36,8 @@ def enabled_enhancements(config): if getattr(config, enh["cli_dest"]): yield enh -def are_requested(config): + +def are_requested(config: configuration.NamespaceConfig) -> bool: """ Checks if one or more of the requested enhancements are those of the new enhancement interfaces. @@ -38,7 +47,9 @@ def are_requested(config): """ return any(enabled_enhancements(config)) -def are_supported(config, installer): + +def are_supported(config: configuration.NamespaceConfig, + installer: Optional[interfaces.Installer]) -> bool: """ Checks that all of the requested enhancements are supported by the installer. @@ -57,7 +68,10 @@ def are_supported(config, installer): return False return True -def enable(lineage, domains, installer, config): + +def enable(lineage: Optional[interfaces.RenewableCert], domains: Iterable[str], + installer: Optional[interfaces.Installer], + config: configuration.NamespaceConfig) -> None: """ Run enable method for each requested enhancement that is supported. @@ -73,10 +87,12 @@ def enable(lineage, domains, installer, config): :param config: Configuration. :type config: certbot.configuration.NamespaceConfig """ - for enh in enabled_enhancements(config): - getattr(installer, enh["enable_function"])(lineage, domains) + if installer: + for enh in enabled_enhancements(config): + getattr(installer, enh["enable_function"])(lineage, domains) -def populate_cli(add): + +def populate_cli(add: Callable[..., None]) -> None: """ Populates the command line flags for certbot._internal.cli.HelpfulParser @@ -116,7 +132,7 @@ class AutoHSTSEnhancement(object, metaclass=abc.ABCMeta): """ @abc.abstractmethod - def update_autohsts(self, lineage, *args, **kwargs): + def update_autohsts(self, lineage: interfaces.RenewableCert, *args: Any, **kwargs: Any) -> None: """ Gets called for each lineage every time Certbot is run with 'renew' verb. Implementation of this method should increase the max-age value. @@ -130,7 +146,7 @@ class AutoHSTSEnhancement(object, metaclass=abc.ABCMeta): """ @abc.abstractmethod - def deploy_autohsts(self, lineage, *args, **kwargs): + def deploy_autohsts(self, lineage: interfaces.RenewableCert, *args: Any, **kwargs: Any) -> None: """ Gets called for a lineage when its certificate is successfully renewed. Long max-age value should be set in implementation of this method. @@ -140,7 +156,8 @@ class AutoHSTSEnhancement(object, metaclass=abc.ABCMeta): """ @abc.abstractmethod - def enable_autohsts(self, lineage, domains, *args, **kwargs): + def enable_autohsts(self, lineage: Optional[interfaces.RenewableCert], domains: Iterable[str], + *args: Any, **kwargs: Any) -> None: """ Enables the AutoHSTS enhancement, installing Strict-Transport-Security header with a low initial value to be increased @@ -153,6 +170,7 @@ class AutoHSTSEnhancement(object, metaclass=abc.ABCMeta): :type domains: `list` of `str` """ + # This is used to configure internal new style enhancements in Certbot. These # enhancement interfaces need to be defined in this file. Please do not modify # this list from plugin code. diff --git a/certbot/certbot/plugins/storage.py b/certbot/certbot/plugins/storage.py index 602c62d1e..7b9bb864c 100644 --- a/certbot/certbot/plugins/storage.py +++ b/certbot/certbot/plugins/storage.py @@ -4,6 +4,7 @@ import logging from typing import Any from typing import Dict +from certbot import configuration from certbot import errors from certbot.compat import filesystem from certbot.compat import os @@ -14,7 +15,7 @@ logger = logging.getLogger(__name__) class PluginStorage: """Class implementing storage functionality for plugins""" - def __init__(self, config, classkey): + def __init__(self, config: configuration.NamespaceConfig, classkey: str) -> None: """Initializes PluginStorage object storing required configuration options. @@ -29,7 +30,7 @@ class PluginStorage: self._data: Dict self._storagepath: str - def _initialize_storage(self): + def _initialize_storage(self) -> None: """Initializes PluginStorage data and reads current state from the disk if the storage json exists.""" @@ -37,7 +38,7 @@ class PluginStorage: self._load() self._initialized = True - def _load(self): + def _load(self) -> None: """Reads PluginStorage content from the disk to a dict structure :raises .errors.PluginStorageError: when unable to open or read the file @@ -67,7 +68,7 @@ class PluginStorage: raise errors.PluginStorageError(errmsg) self._data = data - def save(self): + def save(self) -> None: """Saves PluginStorage content to disk :raises .errors.PluginStorageError: when unable to serialize the data @@ -97,7 +98,7 @@ class PluginStorage: logger.error(errmsg) raise errors.PluginStorageError(errmsg) - def put(self, key, value): + def put(self, key: str, value: Any) -> None: """Put configuration value to PluginStorage :param str key: Key to store the value to @@ -110,7 +111,7 @@ class PluginStorage: self._data[self._classkey] = {} self._data[self._classkey][key] = value - def fetch(self, key): + def fetch(self, key: str) -> Any: """Get configuration value from PluginStorage :param str key: Key to get value from the storage diff --git a/certbot/certbot/plugins/util.py b/certbot/certbot/plugins/util.py index 04a741da6..147e40960 100644 --- a/certbot/certbot/plugins/util.py +++ b/certbot/certbot/plugins/util.py @@ -1,5 +1,6 @@ """Plugin utilities.""" import logging +from typing import List from certbot import util from certbot.compat import os @@ -8,7 +9,7 @@ from certbot.compat.misc import STANDARD_BINARY_DIRS logger = logging.getLogger(__name__) -def get_prefixes(path): +def get_prefixes(path: str) -> List[str]: """Retrieves all possible path prefixes of a path, in descending order of length. For instance: @@ -21,7 +22,7 @@ def get_prefixes(path): :rtype: `list` of `str` """ prefix = os.path.normpath(path) - prefixes = [] + prefixes: List[str] = [] while prefix: prefixes.append(prefix) prefix, _ = os.path.split(prefix) @@ -31,7 +32,7 @@ def get_prefixes(path): return prefixes -def path_surgery(cmd): +def path_surgery(cmd: str) -> bool: """Attempt to perform PATH surgery to find cmd Mitigates https://github.com/certbot/certbot/issues/1833 diff --git a/certbot/certbot/util.py b/certbot/certbot/util.py index 50661f183..242dfef5a 100644 --- a/certbot/certbot/util.py +++ b/certbot/certbot/util.py @@ -123,7 +123,7 @@ def run_script(params: List[str], log: Callable[[str], None]=logger.error) -> Tu return proc.stdout, proc.stderr -def exe_exists(exe: Optional[str]) -> bool: +def exe_exists(exe: str) -> bool: """Determine whether path/name refers to an executable. :param str exe: Executable path or name @@ -132,9 +132,6 @@ def exe_exists(exe: Optional[str]) -> bool: :rtype: bool """ - if exe is None: - return False - path, _ = os.path.split(exe) if path: return filesystem.is_executable(exe) diff --git a/certbot/docs/cli-help.txt b/certbot/docs/cli-help.txt index b1e4252fb..b84921786 100644 --- a/certbot/docs/cli-help.txt +++ b/certbot/docs/cli-help.txt @@ -39,70 +39,6 @@ optional arguments: -c CONFIG_FILE, --config CONFIG_FILE path to config file (default: /etc/letsencrypt/cli.ini 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: - 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. - Setting this flag to 0 disables log rotation entirely, - causing Certbot to always append to the same log file. - (default: 1000) - -n, --non-interactive, --noninteractive - Run without ever asking for user input. This may - require additional command line flags; the client will - try to explain which ones are required if it finds one - missing (default: False) - --force-interactive Force Certbot to be interactive even if it detects - it's not being run in a terminal. This flag cannot be - used with the renew subcommand. (default: False) - -d DOMAIN, --domains DOMAIN, --domain DOMAIN - Domain names to apply. For multiple domains you can - use multiple -d flags or enter a comma separated list - of domains as a parameter. The first domain provided - will be the subject CN of the certificate, and all - domains will be Subject Alternative Names on the - certificate. The first domain will also be used in - some software user interfaces and as the file paths - for the certificate and related material unless - otherwise specified or you already have a certificate - with the same name. In the case of a name collision it - will append a number like 0001 to the file path name. - (default: Ask) - --eab-kid EAB_KID Key Identifier for External Account Binding (default: - None) - --eab-hmac-key EAB_HMAC_KEY - HMAC key for External Account Binding (default: None) - --cert-name CERTNAME Certificate name to apply. This name is used by - Certbot for housekeeping and in file paths; it doesn't - affect the content of the certificate itself. To see - certificate names, run 'certbot certificates'. When - creating a new certificate, specifies the new - certificate's name. (default: the first provided - domain or the name of an existing certificate on your - system for the same domains) - --dry-run Perform a test run of the client, obtaining test - (invalid) certificates but not saving them to disk. - This can currently only be used with the 'certonly' - and 'renew' subcommands. Note: Although --dry-run - tries to avoid making any persistent changes on a - system, it is not completely side-effect free: if used - with webserver authenticator plugins like apache and - nginx, it makes and then reverts temporary config - changes in order to obtain test certificates, and - reloads webservers to deploy and then roll back those - changes. It also calls --pre-hook and --post-hook - commands if they are defined because they may be - necessary to accurately simulate renewal. --deploy- - hook commands are not called. (default: False) - --debug-challenges After setting up challenges, wait for user input - before submitting to CA (default: False) - --preferred-chain PREFERRED_CHAIN - Set the preferred certificate chain. 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. (default: None) --preferred-challenges PREF_CHALLS A sorted, comma delimited list of the preferred challenge to use during authorization with the most @@ -112,25 +48,6 @@ optional arguments: for details. ACME Challenges are versioned, but if you pick "http" rather than "http-01", Certbot will select the latest version automatically. (default: []) - --user-agent USER_AGENT - Set a custom user agent string for the client. User - agent strings allow the CA to collect high level - statistics about success rates by OS, plugin and use - 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.21.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, - --force-renew, --allow-subset-of-names, -n, and - whether any hooks are set. - --user-agent-comment USER_AGENT_COMMENT - Add a comment to the default user agent string. May be - used when repackaging Certbot or calling it from - another tool to allow additional statistical data to - be collected. Ignored if --user-agent is set. - (Example: Foo-Wrapper/1.0) (default: None) automation: Flags for automating execution & other tweaks diff --git a/certbot/setup.py b/certbot/setup.py index 5a6823053..0fb00c058 100644 --- a/certbot/setup.py +++ b/certbot/setup.py @@ -50,7 +50,7 @@ install_requires = [ # in which we added 2.6 support (see #2243), so we relax the requirement. 'ConfigArgParse>=0.9.3', 'configobj>=5.0.6', - 'cryptography>=2.1.4', + 'cryptography>=2.5.0', 'distro>=1.0.1', 'josepy>=1.9.0', 'parsedatetime>=2.4', diff --git a/certbot/tests/client_test.py b/certbot/tests/client_test.py index 330fd2852..8d8073f6f 100644 --- a/certbot/tests/client_test.py +++ b/certbot/tests/client_test.py @@ -1,4 +1,5 @@ """Tests for certbot._internal.client.""" +import datetime import contextlib import platform import shutil @@ -12,6 +13,7 @@ from certbot import errors from certbot import util from certbot._internal.display import obj as display_obj from certbot._internal import account +from certbot._internal import constants from certbot.compat import os import certbot.tests.util as test_util @@ -320,6 +322,24 @@ class ClientTest(ClientTestCommon): "some issuer", True) self.config.preferred_chain = None + # Test for default issuance_timeout + expected_deadline = \ + datetime.datetime.now() + datetime.timedelta( + seconds=constants.CLI_DEFAULTS["issuance_timeout"]) + self.client.obtain_certificate_from_csr(test_csr, orderr=orderr) + ((_, deadline), _) = self.client.acme.finalize_order.call_args + self.assertTrue( + abs(expected_deadline - deadline) <= datetime.timedelta(seconds=1)) + + # Test for specific issuance_timeout (300 seconds) + expected_deadline = \ + datetime.datetime.now() + datetime.timedelta(seconds=300) + self.config.issuance_timeout = 300 + self.client.obtain_certificate_from_csr(test_csr, orderr=orderr) + ((_, deadline), _) = self.client.acme.finalize_order.call_args + self.assertTrue( + abs(expected_deadline - deadline) <= datetime.timedelta(seconds=1)) + # Test for orderr=None self.assertEqual( (mock.sentinel.cert, mock.sentinel.chain), diff --git a/certbot/tests/display/ops_test.py b/certbot/tests/display/ops_test.py index e7936a6f7..e00eeb086 100644 --- a/certbot/tests/display/ops_test.py +++ b/certbot/tests/display/ops_test.py @@ -474,7 +474,7 @@ class ChooseValuesTest(unittest.TestCase): result = self._call(items, None) self.assertEqual(result, [items[2]]) self.assertIs(mock_util().checklist.called, True) - self.assertIsNone(mock_util().checklist.call_args[0][0]) + self.assertEqual(mock_util().checklist.call_args[0][0], "") @test_util.patch_display_util() def test_choose_names_success_question(self, mock_util): diff --git a/certbot/tests/ocsp_test.py b/certbot/tests/ocsp_test.py index e47b99f3b..c102667bc 100644 --- a/certbot/tests/ocsp_test.py +++ b/certbot/tests/ocsp_test.py @@ -10,6 +10,7 @@ from cryptography.exceptions import InvalidSignature from cryptography.exceptions import UnsupportedAlgorithm from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes +from cryptography.x509 import ocsp as ocsp_lib import pytz from certbot import errors @@ -21,15 +22,6 @@ except ImportError: # pragma: no cover from unittest import mock -try: - # Only cryptography>=2.5 has ocsp module - # and signature_hash_algorithm attribute in OCSPResponse class - from cryptography.x509 import ocsp as ocsp_lib # pylint: disable=import-error - getattr(ocsp_lib.OCSPResponse, 'signature_hash_algorithm') -except (ImportError, AttributeError): # pragma: no cover - ocsp_lib = None # type: ignore - - out = """Missing = in header key=value ocsp: Use -help for summary. """ @@ -139,8 +131,6 @@ class OCSPTestOpenSSL(unittest.TestCase): self.assertEqual(mock_log.info.call_count, 1) -@unittest.skipIf(not ocsp_lib, - reason='This class tests functionalities available only on cryptography>=2.5.0') class OSCPTestCryptography(unittest.TestCase): """ OCSP revokation tests using Cryptography >= 2.4.0 diff --git a/letstest/scripts/test_sdists.sh b/letstest/scripts/test_sdists.sh index 562169524..addc59095 100755 --- a/letstest/scripts/test_sdists.sh +++ b/letstest/scripts/test_sdists.sh @@ -29,7 +29,6 @@ if [ -f /etc/redhat-release ] && [ "$(. /etc/os-release 2> /dev/null && echo "$V sed -i 's|pyopenssl==.*|pyopenssl==19.1.0|g' "$CONSTRAINTS" fi - PLUGINS="certbot-apache certbot-nginx" # build sdists for pkg_dir in acme certbot $PLUGINS; do diff --git a/linter_plugin.py b/linter_plugin.py index a19bf7df9..1eb13ffcf 100644 --- a/linter_plugin.py +++ b/linter_plugin.py @@ -6,12 +6,18 @@ deprecated modules. You can check its behavior as a reference to what is coded h See https://github.com/PyCQA/pylint/blob/b20a2984c94e2946669d727dbda78735882bf50a/pylint/checkers/imports.py#L287 See https://docs.pytest.org/en/latest/writing_plugins.html """ +import os.path +import re + from pylint.checkers import BaseChecker from pylint.interfaces import IAstroidChecker -# Modules in theses packages can import the os module. -WHITELIST_PACKAGES = [ - 'acme', 'certbot_integration_tests', 'certbot_compatibility_test', 'lock_test' +# Modules whose file is matching one of these paths can import the os module. +WHITELIST_PATHS = [ + '/acme/acme/', + '/certbot-ci/', + '/certbot-compatibility-test/', + '/tests/lock_test', ] @@ -50,5 +56,5 @@ def register(linter): def _check_disabled(node): module = node.root() - return any(package for package in WHITELIST_PACKAGES - if module.name.startswith(package + '.') or module.name == package) + return any(path for path in WHITELIST_PATHS + if os.path.normpath(path) in os.path.normpath(module.file)) diff --git a/tools/oldest_constraints.txt b/tools/oldest_constraints.txt index 0594bf91b..7ce1689cb 100644 --- a/tools/oldest_constraints.txt +++ b/tools/oldest_constraints.txt @@ -2,7 +2,7 @@ # that script. apacheconfig==0.3.2 asn1crypto==0.24.0 -astroid==2.8.5; python_version >= "3.6" and python_version < "4.0" +astroid==2.8.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" backports.entry-points-selectable==1.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" @@ -17,7 +17,7 @@ colorama==0.4.4; python_version >= "3.6" and python_full_version < "3.0.0" and s configargparse==0.10.0 configobj==5.0.6 coverage==6.1.2; python_version >= "3.6" or python_version >= "3.6" -cryptography==2.1.4 +cryptography==3.2.1; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") cython==0.29.24; (python_version >= "2.6" and python_full_version < "3.0.0") or (python_full_version >= "3.3.0") distlib==0.3.3; 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.0.1 @@ -28,7 +28,7 @@ docker-pycreds==0.4.0; python_version >= "3.6" and python_full_version < "3.0.0" docker==3.7.3; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.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.18; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" +docutils==0.18.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" filelock==3.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" or python_version >= "3.6" funcsigs==0.4 @@ -53,7 +53,7 @@ mypy==0.910; python_version >= "3.6" ndg-httpsclient==0.3.2 oauth2client==4.0.0 packaging==21.2; python_version >= "3.6" -paramiko==2.4.3; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" +paramiko==2.8.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" parsedatetime==2.4 pbr==1.8.0 pip==21.3.1; python_version >= "3.6" @@ -83,7 +83,7 @@ pyyaml==3.13; python_version >= "3.6" and python_full_version < "3.0.0" or pytho requests-file==1.5.1; python_version >= "3.6" requests-toolbelt==0.9.1; python_version >= "3.6" requests==2.14.2 -rsa==4.7.2; python_version >= "3.6" and python_version < "4" +rsa==4.8; python_version >= "3.6" and python_version < "4" s3transfer==0.1.13; python_version >= "3.6" setuptools==39.0.1; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.3.0") six==1.11.0 diff --git a/tools/pinning/oldest/pyproject.toml b/tools/pinning/oldest/pyproject.toml index 70ec89729..601b4d185 100644 --- a/tools/pinning/oldest/pyproject.toml +++ b/tools/pinning/oldest/pyproject.toml @@ -60,7 +60,7 @@ cffi = "1.9.1" chardet = "2.2.1" cloudflare = "1.5.1" configobj = "5.0.6" -cryptography = "2.1.4" +cryptography = "3.2.1" distro = "1.0.1" dns-lexicon = "3.2.1" dnspython = "1.15.0" diff --git a/tox.ini b/tox.ini index 554ff5b68..23e373bd7 100644 --- a/tox.ini +++ b/tox.ini @@ -20,8 +20,8 @@ install_and_test = python {toxinidir}/tools/install_and_test.py 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 win_all_packages = acme[test] certbot[test] {[base]dns_packages} certbot-nginx all_packages = {[base]win_all_packages} certbot-apache -fully_typed_source_paths = acme/acme -partially_typed_source_paths = 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 +fully_typed_source_paths = acme/acme certbot/certbot certbot-ci/certbot_integration_tests certbot-ci/snap_integration_tests certbot-ci/windows_installer_integration_tests +partially_typed_source_paths = 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] passenv =