mirror of
https://github.com/certbot/certbot.git
synced 2026-05-28 04:34:11 -04:00
Merge branch 'master' into typed-jose-fields
This commit is contained in:
commit
0a707096ea
115 changed files with 1801 additions and 1196 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"""
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
# type: ignore
|
||||
"""
|
||||
General conftest for pytest execution of all integration tests lying
|
||||
in the certbot_integration tests package.
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
"""
|
||||
|
|
|
|||
0
certbot-ci/certbot_integration_tests/py.typed
Normal file
0
certbot-ci/certbot_integration_tests/py.typed
Normal 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')
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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.')
|
||||
|
|
|
|||
|
|
@ -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:]
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
0
certbot-ci/snap_integration_tests/py.typed
Normal file
0
certbot-ci/snap_integration_tests/py.typed
Normal 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.')
|
||||
|
|
|
|||
0
certbot-ci/windows_installer_integration_tests/py.typed
Normal file
0
certbot-ci/windows_installer_integration_tests/py.typed
Normal 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)
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 "
|
||||
|
|
|
|||
|
|
@ -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 "
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:]``
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 "
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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':
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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"):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in a new issue