Merge branch 'master' into typed-jose-fields

This commit is contained in:
Adrien Ferrand 2021-12-09 23:03:48 +01:00 committed by GitHub
commit 0a707096ea
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
115 changed files with 1801 additions and 1196 deletions

View file

@ -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)

View file

@ -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.

View file

@ -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',

View file

@ -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.

View file

@ -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

View file

@ -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"""

View file

@ -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,

View file

@ -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:

View file

@ -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

View file

@ -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):
</Location>
"""
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")

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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]

View file

@ -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)

View file

@ -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)

View file

@ -1,3 +1,4 @@
# type: ignore
"""
General conftest for pytest execution of all integration tests lying
in the certbot_integration tests package.

View file

@ -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()

View file

@ -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

View file

@ -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.
"""

View file

@ -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')

View file

@ -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:

View file

@ -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.')

View file

@ -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:]

View file

@ -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()

View file

@ -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.

View file

@ -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({

View file

@ -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())

View file

@ -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]

View file

@ -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)

View file

@ -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)

View file

@ -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.')

View file

@ -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)

View file

@ -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',

View file

@ -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',

View file

@ -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',

View file

@ -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

View file

@ -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

View file

@ -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',

View file

@ -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',

View file

@ -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',

View file

@ -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

View file

@ -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',

View file

@ -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',

View file

@ -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',

View file

@ -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',

View file

@ -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',

View file

@ -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',

View file

@ -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',

View file

@ -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,

View file

@ -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")

View file

@ -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

View file

@ -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

View file

@ -1,3 +1,3 @@
"""Certbot client."""
# version number like 1.2.3a0, must have at least 2 parts, like 1.2
__version__ = '1.22.0.dev0'
__version__ = '1.23.0.dev0'

View file

@ -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())

View file

@ -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:

View file

@ -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

View file

@ -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:

View file

@ -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

View file

@ -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",

View file

@ -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

View file

@ -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 "

View file

@ -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 "

View file

@ -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"),

View file

@ -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
"""

View file

@ -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,

View file

@ -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

View file

@ -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."""

View file

@ -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,

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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:

View file

@ -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

View file

@ -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:]``

View file

@ -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())

View file

@ -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()

View file

@ -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

View file

@ -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

View file

@ -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 "

View file

@ -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

View file

@ -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,

View file

@ -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

View file

@ -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()

View file

@ -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.

View file

@ -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.

View file

@ -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.
"""

View file

@ -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

View file

@ -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':

View file

@ -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

View file

@ -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)

View file

@ -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"):

View file

@ -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

View file

@ -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.

View file

@ -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()

View file

@ -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.

Some files were not shown because too many files have changed in this diff Show more